NEXAFORGE.STUDIO

PRODUCTION GUIDE · DOCKER + NGINX

Cloudflare Tunnel with Docker and Nginx

The clean container pattern is simple: Cloudflare reaches cloudflared, cloudflared reaches Nginx by service name on a private Docker network, and Nginx proxies to the application. None of those application ports need to be published to the internet.

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.

Frequently asked questions

Should cloudflared and Nginx run in the same container?

Usually no. Separate containers keep lifecycle, logs and upgrades independent. They can communicate over a private Docker network using the Nginx service name.

Do I need to publish Nginx port 80 or 443?

Not for traffic that only arrives through cloudflared on the same Docker network. You may expose a local management or health path deliberately, but it is not required for the tunnel route.

Why does http://localhost fail inside cloudflared?

Inside the cloudflared container, localhost refers to that container. Use the Docker service name, such as http://nginx:80, when Nginx runs in another container.

Related service and guides

Need production help? Email [email protected] or contact @taoquan8 on Telegram.