Introduction: why?
As I was comparing NPM, Zoraxy and Traefik, Authentik pricing and automatic SSL management I realized this was error-prone, complex, and require maintenance. After chatting with my favorite AI, I found that Cloudflare offers everything built-in for free. This solutions is daunting at first, but when you have a good tutorial, its easy. Since SSL, Auth and Reverse Proxy are all in Cloudflare, the diagram is simply: VM ←Cloudflared tunnel →Cloudflare←Users. Please note this tutorial expect your services to be on Docker, and will be very Docker centric.
Step 0: What is?
Reverse Proxy: Its a mailman of the internet. Your VM may have 20 services: a fitness tracking app, a finance app, home assistant, etc. Those services (docker container) does not give you a “fitness.yoursite.com” just by starting it. Usually they open a HTTP access to a port, and you need to do the rest of the stack. A reverse proxy is a type of software that receive web connection from users, and dispatch it to the right docker container. To do this, it needs a list: if a user seeks fitness.yoursite.com, forward it to localhost:4592, if a user wants private.yoursite.com, ask for password, but then forward it to localhost:8513. A reverse proxy can feature a SSL component, to make your site HTTPS (required these days) (otherwise you would have to get it another way), and can feature some access control (password protecting or something fancier)
Docker: If you never played with Docker before, you should find other tutorials on that. Docker allows you to run anything with mobility: if you create a docker for your self-hosted fitness tracking app on your Windows machine, you can then migrate it to your new macbook, raspberry pi, or AWS VM with very minimal setup.
Step 1: Zero trust tunnel / cloudflared
This tunnel will hide our VM. The VM still has access to internet (egress), but all ingress will be from that tunnel. That makes it impossible for hackers to have direct access, and Cloudflare is great at blocking hacker. You will also benefit from cache and other optimization.
If docker is not in your VM, install it.
Go to your Cloudflare profile and add MFA. After all, what’s the point of all of this if your account is easy to compromise.
You will need a domain name. Transfer yours, or simply buy a 6–9 digit domain with “.xyz” directly on cloudflare for 90 cents.
Then go in your Cloudflare, Zero Trust, Network, Connectors, Create a tunnel, Cloudflared, name, next, Docker, copy the key, remote the fluff before the key and keep it for .env below.
Add your first connector application. It doesn’t exists yet, but we will enter one anyway: test.yourdomain.com on top, http://host.docker.internal:3000 at bottom.
(Who is host.docker.internal? Below, we add it to the docker compose file. It tells Docker to add a DNS entry in its internal DNS to containers that maps this fake domain name (not an internet domain) to the IP address of the host. When we will launch other container, publishing a port, the service can be accessed from bash on the VM (curl localhost:3000), or from the cloudflared container (host.docker.internal:3000), ergo, by the tunnel)
Then create a directory with 2 files. First, “docker-compose.yaml”
version: '3.8'
services:
tunnel:
image: cloudflare/cloudflared:latest
command: tunnel run
restart: unless-stopped
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
extra_hosts:
- "host.docker.internal:host-gateway"
Second, “.env”
TUNNEL_TOKEN=Please enter here the key provided by cloudflare
In that directory, type:
sudo docker compose up -d
sudo docker container ls
This should setup your tunnel, and list all containers, which should include your tunnel.
Lets try this quickly by typing this command in your VM:
docker run -p 3000:80 hashicorp/http-echo -text=Welcome
Then your website should work. Ctrl+C when your test is done. Congratulation, you now have a reverse proxy and SSL working!
(Some would argue that you should create a docker network. I disagree. Some pre-made docker compose file contain a network for itself, and that makes integration of our new network harder. In addition, it allows the tunnel container to connect to any port of any service, published or not. Many containers have a postgres or other unprotected internal container that doesn’t expose any port… well now they are exposed (yes to cloudflared only. but when you give unrestricted access from one service to other service, a compromised system compromise the whole chain). Doing what I recommend is easier, and safer)
Step 2: Protecting
My stack recommends using Dockhand, which is a web Docker manager, like Portainer but free and better. That would grant admin access to strangers on the internet! So before we jump to that step, we need to discover Cloudflare Access Application.
Cloudflare Connector Application: Found in Networks->Connectors->Your connector->Published application routes. It tells Cloudflare which public hostname (test.yoursite.com) go to which internal service (like the Welcome site we made on internal port 3000)
Cloudflare Access Application: Found in Access Control->Applications. You will have to re-type the public hostname because it is not synced with the above. It will tell Cloudflare which hostname or partial url is private, authentication methods and their exceptions.
For this step, let’s setup authentication method first. Head to Integrations, Identity Providers, Add an Identity Provider and setup some of them. The easiest is One time password. For the rest, follow carefully the steps on the right side of your screen. They are complete and up to date.
If you’re using Groups with OIDC, add the extra permission as stated in the page, but I personally added those OIDC claims: acct groups ctry. I also added “Add Group Claims” in Token configuration in Azure App Registration.
Then go to Access Control, Policies, and create a policy. The first one should be “Empty Bypass”. The type is Bypass and the include selector is Everyone.

The second policy will be your regular access control.
If you want to use Azure AD group (and did what I suggested), the following should works (use the group ID provided by Azure). Otherwise, you can use email or make your own policies.
The last step is to go to Access Control, Applications, and create one self-hosted for your restricted access and one for the bypass. For the restricted access, put the domain name you’re trying to secure and your normal policy you created.
You will need one for bypass. Same thing, but add URL that the public needs access too. For tests, you can add /api/*.
Now test your site using incognito (you can reuse the docker run command from step 1 to re-create the HTTP site temporarily). You should have access to test.yoursite.com/api/ but when going to test.yoursite.com Cloudflare should ask you to authenticate. When you do, you should have see your site!
Step 3: Dockhand
For this, you will have to create a Cloudflare Connector Application and a Cloudflare Access Application, as you did in the previous steps. The only difference is that the port should be different, lets say 5823. Considering this gives root access to everything to anyone on the internet with the URL, now is the time to test if the access is restricted.
Then create a directory for dockhand, a subdirectory compose, a subdirectory data, and the file dockhand/compose/docker-compose.yaml:
services:
dockhand:
image: fnsys/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- 5823:3000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- FULL_PATH_TO_DATA:/app/data
(don’t forget to replace FULL_PATH_TO_DATA)
Bring it up by doing “sudo docker compose up -d”. Go to the url, and you should have access to Dockhand. Go in settings and add a user/password, enable authentication, then if you want, enable SSO. Please note that SSO won’t work with an obscure error message if you don’t do the 2 other steps first. It seems unnecessary since we’re protected by Cloudflare Access, but I always recommend 2 layers of security in case we make a small mistake. Personally, I unlocked both (individually) by accident: CloudFlare Access with a URL typo, and Dockhand by messing up the volume path (created a new empty directory, thus started fresh, and Dockhand doesn’t have a password by default). Be safe.
Now we will explore a bit of Dockhand. Create an environement called local with no further settings — default will be localhost (our own machine). Then go to Stack, Create, add this:
version: '3.6'
services:
http-echo:
image: hashicorp/http-echo:latest
command: ["-text", "Hello, world!"]
ports:
- "5678:5678"
Press Create And Start. Create a Cloudflare Connector Application with http://host.docker.internal:5678. Test the site you created, but this time, with Dockhand.

Now you have all the knowledge to deploy any self-hosted app you want! Go to https://github.com/awesome-selfhosted/awesome-selfhosted and explore all the things you can do.
Step 4: The last firewall rule — SSH
In this journey we created a simple stack for deploying and securing our web applications, and as much as SSH is secure, I wasn’t happy with the idea of letting SSH access open to the public. Maybe it’s just paranoia after the famous ssh backdoor (xz) was found before catastrophe. After doing this step, we will be able to change the firewall settings of our VM (in AWS or Azure) to delete all ingress rules.
First step is to create a Cloudflare Connector Application, with the type SSH and pointing to host.docker.internal:22.

Then we will need to go secure it by adding a Cloudflare Access Application. (feel free to add the path to an existing application).
In your linux machine, install cloudflared and add this to your ~/.ssh/config
Host MySecureSSH
ProxyCommand cloudflared access ssh --hostname ssh.mysite.com
User admin
IdentityFile ~/.ssh/id_rsa
Port 22
Of course, replace your identity file, username, and ssh url.
In your terminal, typing “ssh MySecureSSH” should now open a browser, ask you to authenticate, then once you did, your terminal will SSH handshake with your private key and let you in.
All you have to do now is to remove SSH from AWS firewall settings:
If you’re scared of losing access to your machine don’t worry — when you mess up you can re-create that inbound rule, connect to your VM normally, fix everything, then delete this rule again.
Advantages of using Cloudflare
- No update ever
- Free*
- SSL always work transparently.
- Reuses Azure/Office365 user management systems — no independent tool to add/remove people.
- For staff-only services, prevents unauthenticated security issue that the service might have.
- For public access services, if there’s a known vulnerability, Cloudflare blocks the http request that contains that vulnerability
- Caching & other optimization
- Attackers is unable to identify where the server is (real IP) and thus, cannot try to find unprotected service or bypass Cloudflare’s security
- The Cloudflared container itself has very little access, so even if someone found a way in, they wouldn’t go much further.
- Less compute load than having Traefik + GoAuthentik
Disadvantages
- If Cloudflare go down, you go down too.
- Cannot easily migrate to other vendors.
- No video streaming or high throughput
Well, that was a very long article. I hope it helped somebody. Have a wonderful day.





Top comments (0)