I recently needed to run a dockerized application over HTTPS locally. I use Ubuntu, so I don't have the conveniences of Valet or Herd. But don't sweat; we can combine Traefik and mkcert.
Instead of binding each application to local port `:80`, we'll bind a global Traefik container to `:80`, `:443`, and `:8080` (the latter is just where the Traefik dashboard will be available if we want to inspect something). We'll configure the Traefik proxy to use the certificates issued with mkcert.
Let's install mkcert and issue certificates for `*.docker.localhost` domains:
Instead of binding each application to local port `:80`, we'll bind a global Traefik container to `:80`, `:443`, and `:8080` (the latter is just where the Traefik dashboard will be available if we want to inspect something). We'll configure the Traefik proxy to use the certificates issued with mkcert.
Let's install mkcert and issue certificates for `*.docker.localhost` domains:
# Install the root CA as a trusted one... mkcert -install # Generate the local cert... mkcert docker.localhost "*.docker.localhost"
This will create two files:
- The `docker.localhost+1.pem`, which holds the certificate
- The `docker.localhost+1-key.pem`, which holds our private key
We'll rename them to `cert.pem` and `privkey.pem`, respectively, then move them to a global config folder:
mkdir -p ~/.config/sail-proxy/certs mv docker.localhost+1.pem ~/.config/sail-proxy/certs/cert.pem mv docker.localhost+1-key.pem ~/.config/sail-proxy/certs/privkey.pem
Next, we'll create a `~/.config/sail-proxy/tls.yml` file to tell the Traefik container where to look for the certs:
tls: stores: default: defaultCertificate: certFile: /etc/traefik/certs/cert.pem keyFile: /etc/traefik/certs/privkey.pem certificates: - certFile: /etc/traefik/certs/cert.pem keyFile: /etc/traefik/certs/privkey.pem
These paths referenced in the YAML files will be from inside the Traefik container. We'll map this global config folder to the container soon. Now, let's create a `~/.config/sail-proxy/traefik.yml` file to hold some Traefik configs:
logLevel: DEBUG api: insecure: true dashboard: true entryPoints: http: address: ":80" https: address: ":443" providers: file: filename: /etc/traefik/tls.yml docker: endpoint: unix:///var/run/docker.sock watch: true exposedByDefault: true
For Traefik to serve as a proxy for our app containers, they must be inside the same network, so we'll create a global network, which we should add as an external network to our containers. Then we can spin up the Traefik container, binding the ports and mapping the volumes:
docker network create proxy docker run -d \ --restart "unless-stopped" \ -p 80:80 \ -p 8080:8080 \ -p 443:443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "${HOME}/.config/sail-proxy/:/etc/traefik" \ --network proxy \ --name "sail-proxy" \ traefik:v2.10 \ --api.insecure=true \ --providers.docker=true
The proxy container should be running. If you access the dashboard at localhost:8080:
Now, we need to tweak our application's `docker-compose.yml` file. First, add the external network we just created:
# ... networks: sail: driver: bridge proxy: external: true
Then, we can remove port `:80` from our app's ports; we won't need it. Then, we can configure the labels:
services: laravel.test: image: 'serversideup/php:8.2-fpm-nginx' extra_hosts: - 'host.docker.internal:host-gateway' ports: - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: SSL_MODE: 'off' PUID: '${UID:-1000}' PGID: '${GID:-1000}' volumes: - '.:/var/www/html' networks: sail: proxy: aliases: - "turbo-chat.docker.localhost" depends_on: - mysql - soketi labels: - "traefik.enable=true" - "traefik.http.routers.turbo-chat.rule=Host(`turbo-chat.docker.localhost`)" - "traefik.http.routers.turbo-chat.tls=true" - "traefik.http.services.turbo-chat.loadbalancer.server.port=80"
Notice a few things:
- We're not binding port `:80`. This container will receive requests via our Traefik proxy
- We're adding the `proxy` network. We're also adding an alias for this container container inside this network. This is so we can reach this container from other containers running inside this network. When reaching for it from other containers, we gotta do that without HTTPS since that's only served via Traefik
- We're also setting some labels. That's how Traefik knows it should forward requests to this container. We're setting the Host (our local domain), then we're configuring that it should use TLS, and then we're telling it always to send requests to port `:80` (otherwise, it would send requests to `:443`)
That's all the Docker tweaks we need. But we still need to make one config. Laravel, by default, doesn't trust the forwarded headers from proxies. For local dev, that's normally not an issue, but we now have a proxy running locally, so we need to configure it to trust all proxies, which we can do by setting the proxies property to `*` in the TrustProxies.php middleware:
<?php namespace App\Http\Middleware; use Illuminate\Http\Middleware\TrustProxies as Middleware; use Illuminate\Http\Request; class TrustProxies extends Middleware { /** * The trusted proxies for this application. * * @var array<int, string>|string|null */ protected $proxies = '*'; /** * The headers that should be used to detect proxies. * * @var int */ protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB; }
I'm not sure if Laravel should enforce that locally by default or not. My gut says it shouldn't, but this might cause surprises in production. Don't know.
But that's it! If you open the domain you chose in the browser, you should see a valid certificate!
I chose `docker.localhost` because that's already handled automatically (because the `*.localhost` domain is always mapped to `127.0.0.1` automatically), so I don't have to run something like dnsmasq to route a configured TLD to localhost or manually edit my `/etc/hosts` file.
I'm mostly documenting this setup for myself. I've compiled this setup into a single `sail-proxy` script, which you can find here if you're curious.