Moving Docker Services from Port Forwarding to Cloudflare Tunnel

Recommendation: run one remotely managed cloudflared container on the Docker host, attach it only to the Docker networks containing the services it must reach, publish hostnames through Cloudflare, and put Cloudflare Access in front of anything private or administrative.

Research current as of July 2026.

Executive summary

Cloudflare Tunnel replaces router port forwarding with an outbound connection from your network to Cloudflare. The local cloudflared daemon connects out to Cloudflare over TCP/UDP port 7844; Cloudflare then sends requests back through that established connection to the selected Docker service. There is no inbound public port to forward and no need to expose the home connection's public IP.

For a small Docker host, the best default is:

Important boundary: a tunnel removes direct origin exposure, but it does not make an insecure application safe. A public hostname is still public unless Access, application authentication, or another policy restricts it.

How it works

  1. DNS and hostname: a hostname such as photos.example.com is connected to the tunnel. Cloudflare routes the request to its edge.
  2. Outbound connector: cloudflared creates persistent outbound-only connections to Cloudflare. Cloudflare documents four long-lived connections to two data centers for a healthy connector.
  3. Ingress match: the tunnel maps the hostname to an origin service, for example http://photos:8080.
  4. Docker delivery: because cloudflared and the app share a user-defined Docker network, Docker DNS resolves photos to the app container. The app need not publish a host port.

The normal transport is QUIC over UDP 7844, with HTTP/2 over TCP 7844 as fallback. Cloudflare's firewall guidance recommends allowing outbound 7844 and blocking unsolicited inbound traffic.

Recommended Docker layout

Prefer a remotely managed tunnel for this use case: the dashboard/API owns hostnames and routing, while the container receives a scoped tunnel token. A minimal pattern is:

services:
  cloudflared:
    image: cloudflare/cloudflared:2026.5.0
    command: tunnel --no-autoupdate run --token $${TUNNEL_TOKEN}
    restart: unless-stopped
    env_file: [.env.tunnel]
    networks: [edge, photos_net]
    read_only: true
    security_opt: [no-new-privileges:true]

  photos:
    image: your-image
    expose: ["8080"]
    networks: [photos_net]

networks:
  edge:
  photos_net:
    internal: true

Treat that as a pattern, not a copy-paste production file. Pin a tested image tag or digest rather than latest; store .env.tunnel outside version control with mode 600, or use Docker/host secret management; and give cloudflared access only to the networks it needs. If several unrelated stacks share one tunnel, a dedicated shared proxy network or an explicit reverse proxy can be easier to audit than attaching the tunnel to every application network.

For a locally managed tunnel, the equivalent config.yml uses ordered ingress rules and must end with a catch-all:

tunnel: YOUR-TUNNEL-UUID
credentials-file: /etc/cloudflared/credentials.json
ingress:
  - hostname: photos.example.com
    service: http://photos:8080
  - hostname: admin.example.com
    service: http://admin:3000
  - service: http_status:404

Cloudflare supports both approaches. Remote management is simpler for a small homelab and avoids distributing a credentials JSON file; local management is useful when you want routing fully declarative in Git. Whichever you choose, validate ingress rules before deploying.

Cost

ItemExpected costNotes
Cloudflare Tunnel$0Cloudflare documents Tunnel as available on all plans.
Cloudflare Zero Trust Free$0Current plan page lists up to 50 users, with community/Discord support and up to 24 hours of standard log retention.
Zero Trust paid$7/user/monthUseful when exceeding the free user limit or needing paid support/SLA and longer log retention; confirm current billing before purchase.
DomainExisting registrar costA Cloudflare-managed domain is required to publish normal application hostnames.
Optional servicesVariableAdvanced WAF, Load Balancing, log delivery, and other products are separate considerations.

For a personal Docker host with a few users, the likely incremental Cloudflare cost is zero beyond the domain you already own. The trade is that traffic and Access policy enforcement pass through Cloudflare, so review application compatibility, privacy, and any provider terms for sensitive or unusual workloads.

Security best practices

Migration plan

  1. Inventory current forwarded ports and classify services as public, private, admin, or machine-to-machine.
  2. Put one low-risk web service behind a new tunnel first. Add a dedicated hostname and verify normal browsing, uploads, redirects, WebSockets, and login.
  3. Create Access policies before exposing private/admin hostnames. Test both allowed and denied users in a private browser window.
  4. Move cloudflared into Compose and connect it to only the required app network. Prefer internal Docker services without host port publishing.
  5. Monitor the service and tunnel for a day or two, then remove the corresponding router forward. Repeat service by service.
  6. Keep a documented rollback: temporarily restore the old forward only if needed, and do not leave both paths enabled longer than necessary.

Bottom line

For this migration, Cloudflare Tunnel is a strong fit for HTTP-based Docker services: low cost, no inbound router exposure, DNS-based hostnames, and optional identity-aware access. I would use one tunnel per host or trust boundary, a remotely managed configuration, a dedicated cloudflared container, private Docker networks, and Access in front of every service that is not intentionally public. I would not treat the tunnel as a universal replacement for a VPN or as a reason to publish databases, Docker control planes, or unauthenticated admin UIs.

Sources