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:
- Create one named, remotely managed tunnel in the Cloudflare dashboard.
- Run
cloudflare/cloudflaredas a small Compose service using a tunnel token stored as a Docker secret or protected environment file. - Use Docker service names such as
http://app:8080, not host-published ports. - Use one hostname per service and a final deny/catch-all route.
- Use Cloudflare Access policies for dashboards, admin tools, and anything not intended for anonymous public access.
- Remove the router's inbound forwards after testing; retain only the outbound firewall permission required by the tunnel.
How it works
- DNS and hostname: a hostname such as
photos.example.comis connected to the tunnel. Cloudflare routes the request to its edge. - Outbound connector:
cloudflaredcreates persistent outbound-only connections to Cloudflare. Cloudflare documents four long-lived connections to two data centers for a healthy connector. - Ingress match: the tunnel maps the hostname to an origin service, for example
http://photos:8080. - Docker delivery: because
cloudflaredand the app share a user-defined Docker network, Docker DNS resolvesphotosto 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
| Item | Expected cost | Notes |
|---|---|---|
| Cloudflare Tunnel | $0 | Cloudflare documents Tunnel as available on all plans. |
| Cloudflare Zero Trust Free | $0 | Current 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/month | Useful when exceeding the free user limit or needing paid support/SLA and longer log retention; confirm current billing before purchase. |
| Domain | Existing registrar cost | A Cloudflare-managed domain is required to publish normal application hostnames. |
| Optional services | Variable | Advanced 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
- Delete inbound forwards: after the tunnel works, remove WAN-to-LAN port forwards and disable router UPnP. Also remove unnecessary public DNS records pointing at the old IP.
- Use Access by default for private services: create a self-hosted Access application with an explicit Allow policy. Access applications are deny-by-default; use SSO/MFA and short session durations for admin interfaces.
- Do not expose management planes: keep Docker Engine, Portainer, router admin, SSH, databases, and metrics private unless there is a specific, well-tested reason. For machine-to-machine access, use short-lived/rotated Access service tokens rather than human credentials.
- Minimize Docker reach: attach
cloudflaredonly to required networks; do not mount the Docker socket; use read-only filesystem and no-new-privileges where compatible. - Protect the token: never commit tunnel tokens, credentials JSON, or API tokens. Rotate/revoke a token if it appears in logs, shell history, CI output, or source control.
- Keep the connector maintained: pin versions, update on a schedule, and test rollback. If you disable auto-update for reproducibility, take ownership of updates.
- Use origin authentication where possible: Access protects the edge, but the application should still require its own authentication. For sensitive applications, validate the Access token at the origin or enable the tunnel's Access protection feature.
- Monitor health: watch tunnel status and logs, expose metrics only on a private network, and alert on Down/Degraded status. A tunnel is not a backup for a powered-off host or an unavailable ISP.
- Account for protocol limits: HTTP applications are the easiest fit. SSH/TCP and long-lived WebSockets may need keepalives and testing. Some applications assume direct client IPs, special headers, large uploads, or streaming semantics; verify them individually.
Migration plan
- Inventory current forwarded ports and classify services as public, private, admin, or machine-to-machine.
- Put one low-risk web service behind a new tunnel first. Add a dedicated hostname and verify normal browsing, uploads, redirects, WebSockets, and login.
- Create Access policies before exposing private/admin hostnames. Test both allowed and denied users in a private browser window.
- Move
cloudflaredinto Compose and connect it to only the required app network. Prefer internal Docker services without host port publishing. - Monitor the service and tunnel for a day or two, then remove the corresponding router forward. Repeat service by service.
- 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
- Cloudflare Tunnel overview — outbound-only architecture, encryption, redundancy, and supported use cases.
- Cloudflare Tunnel setup — prerequisites, dashboard/API setup, Docker command, routes, and Quick Tunnel limits.
- Tunnel configuration file — ingress matching, service URLs, catch-all rule, validation, and replicas.
- Tunnel with firewall — positive security model and outbound port 7844 requirements.
- Publish a self-hosted application — Access policies, MFA/session controls, token validation, and public-hostname behavior.
- Service tokens — machine authentication, expiry, renewal, and revocation.
- Cloudflare One account limits — current tunnel, replica, and Access-related limits.
- Cloudflare pricing — current Zero Trust Free and Pay-as-you-go plan information.
- Official cloudflared Docker image — image and container usage reference.