Use one private network and explicit service names
Put cloudflared, Nginx and the application on a dedicated Docker network. cloudflared should address Nginx as http://nginx:80, not localhost. Nginx can then proxy to an application service such as http://app:3000.
Docker's internal DNS keeps the route stable when containers are recreated. Avoid hard-coded container IP addresses because they are implementation details, not service identities.
A minimal Compose pattern
Keep the tunnel token outside the Compose file and inject it through your deployment secret mechanism. The example below omits application volumes and Nginx configuration so the network boundary is easy to see.
services:
nginx:
image: nginx:alpine
expose:
- "80"
networks: [edge]
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN}
depends_on:
- nginx
restart: unless-stopped
networks: [edge]
networks:
edge:
driver: bridge
Let Nginx own the application proxy boundary
Nginx can provide application routing, body-size limits, timeouts and a stable upstream name. The Cloudflare edge remains responsible for the public TLS connection, while the cloudflared-to-origin hop uses the scheme defined in the ingress service.
Preserve the intended Host and client forwarding headers, then configure the application to trust only the proxy chain you control. Blindly trusting every X-Forwarded-For header allows clients to forge their address.
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Choose the origin TLS boundary deliberately
HTTP between cloudflared and Nginx can be reasonable when both share an isolated host network. HTTPS to Nginx may be appropriate across hosts or when the internal threat model requires it. Match the ingress scheme to the service and keep certificate verification enabled wherever possible.
Do not add a second redirect at every layer. A public HTTPS redirect at Cloudflare plus an origin that interprets the request as HTTP can produce loops unless trusted proxy headers are configured correctly.
Production checks: secrets, health and replicas
The token should come from a protected environment or secret store and must not be committed with the Compose file. Pin or deliberately schedule image upgrades so a restart does not silently change the connector version during an unrelated deployment.
A process-level restart policy is useful, but application checks are more meaningful. For higher availability, run a connector replica on an independent host that can still reach a healthy origin.
- ▸ Keep tunnel credentials out of Git and container logs
- ▸ Add an Nginx or application health endpoint
- ▸ Set memory and log rotation limits appropriate to the host
- ▸ Test container recreation and host reboot
- ▸ Document how to rotate the token without losing the only connector
Three common Docker failures
First, localhost points to the wrong container. Second, cloudflared starts before the origin is actually ready; depends_on controls order but does not always prove application health. Third, a token exists on the host but was never passed into the container.
Inspect the effective Compose configuration, container environment names and network membership without printing secret values. Then request http://nginx:80 from a temporary container on the same network.