Let's Encrypt HTTPS on an IP Address With FrankenPHP
Laravel PHP #FrankenPHP #Docker #HTTPS #Let's Encrypt #Caddy #PHP

Let's Encrypt HTTPS on an IP Address With FrankenPHP

4 min read Mohamed Said Mohamed Said

HTTPS for IP Addresses, No DNS Required

The serversideup/docker-php images shipped v4.6.0-beta1 with a new CADDY_ACME_PROFILE environment variable. Set it to shortlived and a FrankenPHP container reachable only by IP address can obtain a real, browser-trusted TLS certificate from Let's Encrypt — no domain name, no DNS record, no self-signed warning.

Let's Encrypt made 6-day certificates and IP-address certificates generally available in January 2026. The shortlived profile is the only one under which Let's Encrypt issues IP certificates, so the two features arrive together.

How the Short-Lived Certificate Profile Works

Setting CADDY_ACME_PROFILE=shortlived writes the following snippet into the Caddy configuration:

cert_issuer acme {
  profile shortlived
}

Caddy then requests certificates valid for 160 hours (just under seven days), renewing them every two days. That lifetime qualifies as a Short-Lived Subscriber Certificate under CA/Browser Forum rules, which means revocation information is less critical — a compromised key becomes useless in days rather than months.

A few operational trade-offs to keep in mind:

  • Using any explicit profile drops the ZeroSSL fallback the images normally keep.
  • If the container loses outbound access to Let's Encrypt's ACME API, you have roughly four and a half days before the certificate expires.
  • Let's Encrypt allows five certificates per identical identifier set every seven days, so repeated redeploys without persistent volumes can exhaust that quota quickly.

Configuring It in Docker Compose

The feature is only in the beta tags. Pin to v4.6.0-beta1 rather than the stable 8.4-frankenphp tag:

services:
  php:
    image: serversideup/php:8.4-frankenphp-v4.6.0-beta1
    ports:
      - "80:8080"
      - "443:8443"
    volumes:
      - caddy-config:/config
      - caddy-data:/data
    environment:
      SSL_MODE: "full"
      CADDY_AUTO_HTTPS: "on"
      CADDY_ACME_PROFILE: "shortlived"
      CADDY_HTTPS_SERVER_ADDRESS: "https://203.0.113.10"
      CADDY_GLOBAL_OPTIONS: "default_sni example.com"

volumes:
  caddy-config:
  caddy-data:

Key variables explained:

  • CADDY_AUTO_HTTPS: "on" — enables Caddy's automatic HTTPS (off by default).
  • SSL_MODE: "full" — serves the app over HTTPS and redirects HTTP with a 308.
  • CADDY_HTTPS_SERVER_ADDRESS — the public IP address Caddy should obtain a certificate for.
  • CADDY_GLOBAL_OPTIONS: "default_sni example.com" — provides a fallback SNI identity, because a client connecting to a bare IP sends no Server Name Indication field.

Both ports 80 and 443 (mapped from 8080/8443 inside the unprivileged container) must be reachable from the internet for the ACME challenge to complete.

Mount /config and /data as named volumes. Certificates and ACME account state live there. A container that discards them on every restart will request fresh certificates on every restart, burning through the rate limit fast.

IPv6 Note

Caddy fixed IPv6 handling for IP certificates in issue #7399, closed in April 2026. Caddy v2.11.4 (embedded in the current images) includes that fix, but IPv4 has a longer track record — test IPv6 thoroughly before relying on it in production.

Other Changes in v4.6.0-beta1

  • TRUSTED_PROXY now behaves consistently across FrankenPHP, NGINX, and Apache, so request()->ip() returns the visitor IP regardless of which server variant is running.
  • A new caddyfile-global.d directory lets you drop custom global Caddy config snippets.
  • healthcheck-nightwatch integrates Laravel Nightwatch status checks with Docker HEALTHCHECK.
  • AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND allows a container with AUTORUN_ENABLED=true to exit cleanly when Laravel is not yet installed.

Key Takeaways

  • CADDY_ACME_PROFILE=shortlived is the single variable that unlocks both short-lived and IP-address certificates.
  • No DNS is required — ideal for staging boxes, internal APIs, VPN-only servers, and demo droplets.
  • Certificates last 160 hours and renew every two days; persistent volumes are essential to avoid rate-limit exhaustion.
  • The feature is in a prerelease tag; do not use it in production environments you cannot afford to break.
  • ZeroSSL fallback is disabled when any explicit ACME profile is set.

Source: Let's Encrypt HTTPS on an IP Address With FrankenPHP — Laravel News

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can Let's Encrypt issue a TLS certificate for a bare IP address instead of a domain name?
Yes, as of January 2026 Let's Encrypt issues IP-address certificates, but only under the `shortlived` certificate profile. That profile produces certificates valid for 160 hours (just under seven days). The `serversideup/php` v4.6.0-beta1 images expose this through the `CADDY_ACME_PROFILE=shortlived` environment variable.
Q02 Why must /config and /data be mounted as persistent volumes when using IP certificates with FrankenPHP?
Caddy stores the issued certificate and the ACME account state in those directories. If they are lost on container restart, Caddy requests new certificates on every restart. Let's Encrypt allows only five certificates per identical identifier set every seven days, so a few redeploys without persistent volumes can exhaust the weekly quota.
Q03 What does CADDY_GLOBAL_OPTIONS: "default_sni example.com" do in this setup?
When a client connects to a bare IP address it sends no Server Name Indication (SNI) field, because there is no hostname to advertise. Without a fallback, Caddy cannot match a certificate to the connection. Setting `default_sni` gives Caddy a fallback identity to use for those SNI-less connections.

Continue reading

More Articles

View all