Easy self-hosting websites with Cloudflare and Docker Compose
Prerequisites
This guide assumes you have the following:
- A Cloudflare account (free)
- A domain name (bought from Cloudflare or elsewhere; $10/yr), set up in Cloudflare
- A linux server with docker
The general idea
Web users connect to Cloudflare, and instead of Cloudflare reaching into our network, we run a service inside the network that reaches out to Cloudflare:
We are not opening any ports, e.g. http/https from our server to the local network or internet. We are not messing with dynamic dns stuff. We don’t need to set up TLS.
An example with Docker Compose
Create the tunnel config at Cloudflare
First create your tunnel in the Cloudflare dashboard > Zero Trust > Networks > Tunnels:
- Choose
cloudflared
- Name it whatever you want, e.g.
homelab
- Click the “docker” installation instructions just to grab the token
- Don’t actually install anything
Copy that token for the next section.
On your homelab server
Create docker-compose.yaml
like this, with the token you copied above:
version: "3.3"
services:
tunnel:
image: cloudflare/cloudflared
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=***insert CF Tunnel token here***
web:
image: nginx:latest
Start up the containers:
docker compose up -d
Tail the logs:
docker compose logs -f
Back in the Cloudflare dashboard
At this point, the Cloudflare dashboard should show your tunnel as healthy. If not, read those logs carefully for errors and fix it before continuing.
Add a public hostname to your tunnel:
- subdomain: anything you want (this DNS record will be added for you)
- domain: pick the domain you already set up in Cloudflare
- service: choose
http
, and seturl
to the name of your docker service. In the above example it’sweb
- additional application settings: none
With that done, you should be able to browse to the subdomain you set up, and the response will be tunneled over from your homelab server. Good job ✨
Adding another website
You can add another container to the docker compose file like this:
version: "3.3"
services:
tunnel:
image: cloudflare/cloudflared
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=<insert sensitive token here>
web:
image: nginx:latest
restart: unless-stopped
whoami:
image: traefik/whoami
restart: unless-stopped
And add a public hostname mapping in your tunnel:
- subdomain: anything you want (this DNS record will be added for you)
- domain: pick the domain you already set up in Cloudflare
- service: choose
http
, and seturl
to the name of your docker service. This time it’swhoami
- additional application settings: none
Do I need to do TLS, HTTP compression, etc.?
No. Cloudflare handles TLS and HTTP compression for you. The tunnel is encrypted so no additional TLS is necessary for that leg.
I suggest setting up a global redirect rule in Cloudflare to handle http->https.
Can I build my own containers?
Sure, that’s pretty easy. Your docker compose service can just point to a folder that itself contains a Dockerfile like this:
# ...
web:
build: ./web
restart: unless-stopped
When you do docker compose up -d
it will build the image for you and then start it.
…and run this to rebuild the image as needed: docker compose up -d --build web
Why don’t I need a load balancer like Traefik?
Often we’d carefully expose containers within docker to something outside of docker by configuring ports and proxying traffic through a load balancer like nginx or traefik. We’re not actually accepting connections to our containers from outside of docker so we don’t need to do that.
By default, docker compose containers can see each other and connect by service name (i.e. web
, whoami
, etc.). When
you configure the tunnel to resolve a public hostname to the internal docker service name, the tunnel container just
uses the docker network to talk to it. And the tunnel container can reach out of the docker network to connect to
Cloudflare. If your container is listening on port 80, you’re done—you don’t need to expose it to the host network.
Troubleshooting
See logs from your containers, including the cloudflare tunnel (read those error messages carefully!):
docker compose logs -f
See if the tunnel state is healthy in the Cloudflare dashboard
See if your containers are running:
docker container ls