Behind nginx or another proxy
Run Dockflow on a server where nginx, Apache or another proxy already holds ports 80 and 443, with the external edge and a vhost that points at the switch.
If your server already runs nginx, Apache, HAProxy or another proxy on ports 80 and 443, use the external edge. By default, the switch listens on those ports and obtains certificates itself. With the external edge, it listens on one local address instead, and your proxy sends every routed domain to it. Your proxy keeps TLS; Dockflow keeps zero-downtime deploys.
dockflow server init [email protected] --edge external# /etc/nginx/sites-enabled/shop.conf (the essential part)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}You write the vhost once. Deploys and rollbacks move traffic inside the switch, so a deploy never reloads nginx. Its configuration never names a container or a port that changes.
How it fits together
client ──TLS──▶ nginx :443 ──HTTP──▶ switch 127.0.0.1:8080 ──▶ current containers of the app- nginx terminates TLS, applies whatever you configure in it, and forwards each request to the switch with its
Hostheader. - The switch routes by
Hostand path to the containers of the current release, as declared in each service'sx-dockflow.routes. During a deploy, it moves traffic to the new containers once they are healthy, and lets the old ones finish their requests. See The switch and the edges.
A request for a host name that no route declares gets a 404 from the switch.
Set up the external edge
-
Prepare the server with the switch in external mode, from an administrator's account (see the Quickstart):
dockflow server init [email protected] --edge externalThe switch then listens on
127.0.0.1:8080, reachable from the server only.On a server already prepared for an environment,
dockflow server init production --edge externaldoes the same through the deploy user. Runningserver initagain with another--edgechanges the mode of an installed server. -
Keep your routes as they are. Each routed service declares its domains, and the switch matches them against the
Hostheader nginx sends:compose.yml services: web: image: ghcr.io/acme/shop:1.4.2 healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"] x-dockflow: routes: - domains: [shop.example.com, www.shop.example.com] port: 3000With the external edge, the route's
tlsoption has no effect: TLS belongs to nginx. -
Add a vhost for those domains to nginx (below), check it with
sudo nginx -t, and reload nginx. -
Deploy:
dockflow deploy production
Options of the external edge
| Option | Default | Description |
|---|---|---|
--switch-listen <ip:port> | 127.0.0.1:8080 | The address the switch listens on, on the server. The host must be an IP address. Change the port if 8080 is taken; use a private address only when the proxy runs on another machine |
--trusted-proxy <cidr> | Loopback and private ranges | The networks whose X-Forwarded-For header the switch believes. Repeat the option for several ranges |
The default trusted ranges are 127.0.0.0/8, ::1/128, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and fd00::/8.
These ranges are needed even with nginx on the same host. nginx reaches the published port through Docker, so the switch sees a Docker bridge address rather than 127.0.0.1.
--trusted-proxy replaces this list rather than adding to it. Set it only when the proxy sits on a known network of its own. If the proxy also runs on the server, include the Docker bridge range.
The nginx vhost
A complete vhost, with TLS terminated by nginx and certificates from certbot:
# WebSocket upgrades. Define this map once per nginx: remove it here if another file has it.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name shop.example.com www.shop.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name shop.example.com www.shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
client_max_body_size 25m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
}
}What each part does:
| Directive | Why |
|---|---|
proxy_pass http://127.0.0.1:8080 | Sends the request to the switch: the address of --switch-listen |
proxy_set_header Host $host | Required. The switch routes by host name; without it, every request gets a 404 |
X-Forwarded-For | The client's address. The switch trusts it from the trusted ranges and passes it on, so your app sees the real client |
X-Forwarded-Proto $scheme | Tells the app the request arrived over HTTPS, for redirects and secure cookies |
proxy_http_version 1.1, Upgrade, Connection | WebSockets. The switch handles upgrades without configuration; nginx needs these headers to pass them on |
proxy_read_timeout | nginx closes a connection idle for 60 seconds by default, WebSockets included |
client_max_body_size | nginx refuses request bodies over 1 MB by default |
To get the certificate without letting certbot rewrite the vhost, use the certonly mode:
sudo certbot certonly --nginx -d shop.example.com -d www.shop.example.comsudo certbot --nginx also works, if you prefer certbot to add the TLS lines to the vhost itself. Do not combine it with a vhost versioned in your repository: the versioned file would overwrite those lines.
Apache, HAProxy, Traefik or a load balancer are configured the same way:
- forward every routed domain to
127.0.0.1:8080; - keep the
Hostheader; - set
X-Forwarded-ForandX-Forwarded-Proto.
Cloudflare in front of nginx
When Cloudflare proxies the domains (the orange cloud), nginx receives Cloudflare's requests. It must then take the client's address from Cloudflare, not from the connection.
The nginx plugin does this with cloudflare: true. It takes the client's address from CF-Connecting-IP, trusted from Cloudflare's ranges only, and the scheme from Cloudflare's X-Forwarded-Proto.
plugins:
- use: nginx
with: { domain: "${DOMAIN}", cloudflare: true }- With Cloudflare's Flexible SSL mode, the vhost listens on port 80 (the plugin's default,
tls: false). - With Full (strict), give nginx a certificate Cloudflare accepts, such as a Cloudflare origin certificate, and set
tls: true.
The port filter leaves alone the ports the host itself listens on. So, unlike the cloudflare edge for the switch, it does not restrict nginx's ports 80 and 443 to Cloudflare's ranges. To keep visitors from reaching the server around Cloudflare, limit those ports with the host's firewall, or use a Cloudflare Tunnel.
Checking it
On the server (with curl installed there), ask the switch directly, as nginx does:
dockflow ssh production -- curl -sI -H 'Host: shop.example.com' http://127.0.0.1:8080/A response from your app means the switch serves the route. A 404 means no route declares that host name in the current release.
Then check through nginx from your machine:
curl -I https://shop.example.com/A 502 from nginx usually means the switch is not listening where proxy_pass points: compare it with the --switch-listen you used. dockflow status production shows the current release and its containers.
Versioning the vhost in your repository
The simplest way is the built-in nginx plugin. It writes this vhost from your domains, then checks and reloads nginx when the vhost changed:
plugins:
- use: nginx
with:
domain: [shop.example.com, www.shop.example.com]
tls: trueIt also takes your own vhost, as a template: template: deploy/nginx/shop.conf.
Without the plugin, do the same with an upload and a host hook:
version: 1
app: shop
uploads:
- src: deploy/nginx/shop.conf
dest: /etc/nginx/sites-enabled/shop.conf
mode: "644"
hooks:
after_deploy:
- name: reload nginx
host: sudo nginx -t && sudo systemctl reload nginx
fatal: true
environments:
production: {}-
The upload replaces
/etc/nginx/sites-enabled/shop.confatomically, and only when the file changed. -
After the deploy, the hook checks the whole nginx configuration and reloads nginx.
-
With
fatal: true, a configuration that failsnginx -trolls the deploy back, and the rollback restores the previous file. Meanwhile nginx keeps serving its previous configuration, since it was never reloaded. -
The hook runs on the server as the environment's SSH user, without a terminal, so
sudomust not ask for a password for these two commands.server init --nginxinstalls nginx and this rule for you. Otherwise, add the rule withsudo visudo -f /etc/sudoers.d/dockflow-nginx, using your SSH user's name and the paths thatcommand -v nginx systemctlprints:dockflow ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/bin/systemctl reload nginx
To fill in the domain per environment, render the vhost from a template before it is uploaded.
after_deploy hooks run only when a deploy creates a release. A deploy whose plan is empty uploads nothing and reloads nothing.
Building images
How Dockflow builds the services that have a Compose build section, sends their images to the server or to a registry, and pins them by digest.
Health checks
What decides that a new release is healthy: Docker healthchecks, the grace period, internal HTTP checks before the switch, public checks after it, and on_failure.