Cloudflare Tunnel lets you publish selected self-hosted applications without forwarding ports on your router. This guide explains a practical Docker-based setup, how to connect DNS and access policies, when to use private network routing, and what to check regularly so remote access remains secure and reliable.
Overview
A traditional home server setup often exposes an application by forwarding ports 80 or 443 from the router to a reverse proxy. That approach can work, but it creates an inbound path to your network and adds router, firewall, certificate, and changing-IP considerations. Cloudflare Tunnel uses an outbound connector on your server instead. The connector establishes the connection to Cloudflare, while your application can remain unreachable from the public internet at the network level.
The basic flow is:
- Run a
cloudflaredconnector on the Docker host. - Create or select a tunnel in the Cloudflare Zero Trust dashboard.
- Attach a hostname, such as
notes.example.com, to the tunnel. - Route that hostname to the correct Docker service, such as
http://notes:3000. - Apply an access policy when the application should not be open to everyone.
This arrangement is useful for a homelab, a self-hosted dashboard, an internal development tool, or a small web application. It does not replace application authentication, operating-system hardening, backups, or monitoring. Treat the tunnel as one layer in a broader security design.
Before you begin
You need a domain managed through Cloudflare, a server that can make outbound HTTPS connections, and a Docker installation. The example below assumes that the application and tunnel connector share a Docker network. Replace service names and ports with those used by your stack.
Create the tunnel in the Zero Trust dashboard and choose the Docker connector instructions. Cloudflare provides a tunnel token for the connector. Store it as a secret or in a protected environment file rather than committing it to a public repository.
services:
app:
image: example/app:stable
expose:
- "3000"
networks:
- tunnel
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
restart: unless-stopped
networks:
- tunnel
networks:
tunnel:
driver: bridge
In the tunnel's public hostname configuration, use the Docker service name rather than localhost. For the example above, the service URL is http://app:3000. Inside the connector container, localhost means the connector container itself, not the application container.
Using a version-pinned image instead of an unreviewed floating tag can make upgrades more predictable. Establish a routine for reviewing the connector image, testing it, and changing the tag deliberately. This is particularly important when the tunnel is part of a production or business-critical stack.
What to track
A tunnel is easy to create and easy to forget. Keep a small inventory for every published service. Record the public hostname, internal destination, owner, authentication method, data sensitivity, and intended audience. This turns remote access into something you can review instead of a collection of old dashboard entries.
1. Published hostnames and destinations
Check that every DNS name still points to a service you operate. Remove hostnames for retired containers and correct destinations after a Compose project is renamed or moved. A connector can be healthy while the application behind it is unavailable, so test the complete path from hostname to application.
2. Authentication and access policies
For administrative tools, monitoring panels, file management interfaces, and other sensitive services, add an Access policy rather than relying only on an obscure hostname. Review who can sign in, which identity provider is used, whether multifactor authentication is required by your organization, and whether broad rules such as “allow everyone” are still intentional.
Keep the application's own login enabled unless you have deliberately designed and tested an alternative. Layered authentication reduces the impact of a mistake in either the tunnel policy or the application.
3. Connector health and restart behavior
Track whether the cloudflared container is running, restarting, and reaching the tunnel service. Container logs should be part of your normal monitoring process. An external check, such as Uptime Kuma, can test the public hostname, while local monitoring can detect a stopped container even when an outage monitor has not yet alerted.
For broader monitoring guidance, see How to Build a Self-Hosted Monitoring Stack with Uptime Kuma, Prometheus, and Grafana.
4. Application behavior
Record whether the service needs WebSockets, long-lived connections, large uploads, background jobs, or a particular base URL. Browser applications may appear to load while live updates fail because the application has an incorrect external URL or proxy configuration. Test the functions users actually need, not only the landing page.
5. Private network routes
Public hostnames are appropriate for selected web applications. For SSH, private dashboards, or network services that should not have a public hostname, consider private network routing instead. This normally involves defining an internal network or CIDR route and using an approved client or access method to reach it. Keep private routes narrow: publish only the required subnet or host, not an entire home network by default.
Cadence and checkpoints
Use a simple schedule so tunnel access receives the same attention as the rest of your self-hosted server.
After every new service
- Confirm the Docker service listens on the expected port and is reachable from the connector network.
- Create the smallest required hostname or private route.
- Test login, redirects, WebSockets, uploads, and logout from an external network.
- Apply an access policy before sharing the address.
- Add the service to your inventory and monitoring system.
Monthly
- Review all public hostnames and remove unused entries.
- Check connector logs, restart counts, and failed health checks.
- Review Access applications, identity rules, and temporary exceptions.
- Confirm that exposed applications, Docker images, and the host operating system have a patching plan.
Quarterly or after a major change
- Test recovery by recreating the connector from its Compose definition and stored secrets.
- Verify that DNS, tunnel configuration, and application backups are documented.
- Review whether a public hostname should become a private route.
- Test access from a separate network and confirm that direct router access is not required.
For host-level controls, compare this process with the self-hosted server hardening checklist. For recovery planning, use the Docker backup strategy for self-hosted apps.
How to interpret changes
Most tunnel failures fall into one of three areas: the connector, the network path inside Docker, or the application itself. Isolating those layers prevents random configuration changes.
| Symptom | Likely area | Checks |
|---|---|---|
| Hostname returns a tunnel or gateway error | Connector or tunnel configuration | Check that the container is running, the token is valid, outbound connectivity works, and the hostname is attached to the intended tunnel. |
| Connector is healthy but the app is unreachable | Docker networking or destination | Confirm both containers share a network, use the Compose service name, and target the application's internal listening port. |
| Page loads but redirects to localhost or an internal name | Application proxy or base URL | Set the application's external URL and review trusted proxy or forwarded-header settings. |
| Live updates do not work | WebSockets or application configuration | Confirm the app uses the expected WebSocket path and that its proxy settings recognize the external HTTPS URL. |
| SSH or an internal service is exposed incorrectly | Access model | Prefer a private route and controlled client access for non-HTTP services; do not assume a public web hostname is the right transport. |
| Users can reach a sensitive app without the expected login | Access policy | Review application matching, policy order, identity rules, and any bypass or service-token configuration. |
Do not treat “no open inbound port” as proof that an application is safe. A vulnerable or poorly authenticated service can still be reached through its published hostname. Keep the server hardened, restrict administrative interfaces, use strong credentials, and maintain tested backups. If you prefer a conventional reverse proxy for local routing, compare the trade-offs in Nginx Proxy Manager vs Traefik vs Caddy.
When to revisit
Revisit this guide whenever you publish a new Docker app, move a service between hosts, change a domain, rotate a tunnel token, or alter your identity provider. Those events can change the destination, trust boundary, or recovery process even if the tunnel itself appears healthy.
Schedule a monthly review for small personal stacks and a more frequent review for services used by a team. Return after updates to the Cloudflare dashboard, connector image, Docker host, or application proxy behavior; interface names and recommended configuration paths can change over time. Keep the date of your last successful external test in the service inventory.
As a final checklist, confirm that:
- Only intended hostnames and routes are published.
- Each destination uses a Docker service name and an internal port.
- Sensitive services have an explicit access policy and application authentication.
- WebSockets, redirects, uploads, and logout have been tested where relevant.
- Connector logs and public availability are monitored.
- Router port forwarding is unnecessary and has not been left enabled by accident.
- Configuration, secrets, and application data can be restored independently.
With that routine, Cloudflare Tunnel becomes more than a convenient way to expose Docker apps without port forwarding. It becomes a reviewable access layer that can be reduced, tested, and removed when a service no longer needs remote access.