Deploy a Docker Reverse Proxy with Traefik v3 and Automatic Let’s Encrypt on Ubuntu 22.04/24.04

This guide shows how to deploy a modern reverse proxy using Traefik v3, Docker, and Docker Compose on Ubuntu 22.04 or 24.04. You will get automatic Let’s Encrypt SSL certificates, HTTP to HTTPS redirection, and a clean way to route multiple apps on the same server under different domains or subdomains. The steps are simple, repeatable, and safe for production.

Prerequisites

Before starting, ensure you have: (1) An Ubuntu 22.04 or 24.04 server with a public IP, (2) A domain or subdomain you can edit DNS for (e.g., example.com), and (3) Access to open TCP ports 80 and 443 on the server’s firewall and network provider. You should also have a non-root user with sudo access.

Step 1 — Install Docker Engine and Compose Plugin

Install the official Docker packages. This gives you the latest stable Docker Engine, Buildx, and the Compose plugin.

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

Step 2 — DNS and Firewall

Create DNS A records for your services. For example, point app.example.com and traefik.example.com to your server’s public IP. Then open the firewall for web traffic.

sudo ufw allow 80,443/tcp
sudo ufw reload

Step 3 — Prepare a Docker Network and Traefik Files

Create a dedicated Docker network for the proxy and a directory to hold Traefik files, including the ACME storage for certificates.

docker network create proxy
mkdir -p ~/traefik
cd ~/traefik
touch acme.json
chmod 600 acme.json

Step 4 — Create docker-compose.yml for Traefik v3

The compose file below configures Traefik v3 with automatic HTTPS via the HTTP-01 challenge (ports 80/443). It also binds the dashboard to localhost only for safety.

version: "3.9"

networks:
  proxy:
    external: true

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    command:
      - --api.dashboard=true
      - --api.insecure=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - [email protected]
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
      - --log.level=INFO
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:8080:8080" # Dashboard on localhost
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/letsencrypt/acme.json
    networks:
      - proxy
    restart: unless-stopped

Replace [email protected] with an email you control. Traefik uses it to register with Let’s Encrypt. The dashboard is available on http://localhost:8080 and can be reached via SSH tunneling when needed.

Step 5 — Start Traefik

Launch the reverse proxy and confirm it is running.

docker compose up -d
docker ps

You should see the traefik container healthy and listening on ports 80 and 443.

Step 6 — Add a Test App Behind Traefik

Deploy a simple test service (whoami) to verify automatic HTTPS, routing, and headers. Replace app.example.com with your real hostname that points to the server.

docker run -d --name whoami --network proxy --restart unless-stopped \
  -l "traefik.enable=true" \
  -l "traefik.http.routers.who.rule=Host(`app.example.com`)" \
  -l "traefik.http.routers.who.entrypoints=websecure" \
  -l "traefik.http.routers.who.tls.certresolver=le" \
  -l "traefik.http.middlewares.secHeaders.headers.stsSeconds=31536000" \
  -l "traefik.http.middlewares.secHeaders.headers.stsIncludeSubdomains=true" \
  -l "traefik.http.middlewares.secHeaders.headers.stsPreload=true" \
  -l "traefik.http.middlewares.secHeaders.headers.frameDeny=true" \
  -l "traefik.http.middlewares.ratelimit.rateLimit.average=100" \
  -l "traefik.http.middlewares.ratelimit.rateLimit.burst=50" \
  -l "traefik.http.routers.who.middlewares=secHeaders@docker,ratelimit@docker" \
  traefik/whoami:v1.10

Open https://app.example.com in a browser. The first request may take a few seconds while Traefik obtains a certificate. You should see a basic whoami page over HTTPS with a valid lock icon.

Optional: Use DNS-01 Challenge (Wildcard Certificates)

If your ISP or host blocks port 80, or you want wildcard certificates like *.example.com, switch to the DNS-01 challenge. The example below uses Cloudflare. Create a token in Cloudflare with DNS edit permissions for the zone and store it in an .env file.

cd ~/traefik
printf "CF_DNS_API_TOKEN=YOUR_CLOUDFLARE_DNS_TOKEN\n" > .env

Edit docker-compose.yml and replace the ACME lines for HTTP challenge with DNS challenge:

      - --certificatesresolvers.le.acme.dnschallenge=true
      - --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare

Add the environment variable to the traefik service:

    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

Then reload Traefik:

docker compose up -d

For a wildcard, use routers or certificate domains that match *.example.com. With DNS challenge, Traefik can issue certificates without exposing port 80.

Secure the Dashboard (Optional but Recommended)

By default we bound the dashboard to localhost. To access it securely from your workstation, use an SSH tunnel: ssh -L 8080:localhost:8080 user@your_server and then open http://localhost:8080. If you must expose it on a domain, add Basic Auth and IP allowlisting via labels and serve it over HTTPS. For most setups, keeping it local is safer.

Troubleshooting

If certificates do not issue, confirm that your DNS A/AAAA records point to the server and that ports 80 and 443 are reachable from the internet. Check logs with docker logs -f traefik. Rate limits from Let’s Encrypt can apply if you redeploy too often; use a single domain during testing and switch to the production domain when stable.

Maintenance

Traefik renews certificates automatically before expiry; no cron is required. Keep Docker images current by pulling updates periodically: docker compose pull and docker compose up -d. Back up the acme.json file; it contains your issued certificates and keys.

What You Achieved

You now have a modern, production-ready reverse proxy with Traefik v3, automatic HTTPS via Let’s Encrypt, and a clean Docker-based workflow. Adding new apps is as simple as attaching them to the proxy network and setting a few labels. This pattern scales well, stays secure, and keeps your server easy to manage.

How to Self‑Host Open WebUI and Ollama on Ubuntu with Docker, HTTPS, and NVIDIA GPU Support

Overview

This guide shows how to self-host a private AI chatbot with Open WebUI (a clean, ChatGPT-like interface) and Ollama (for running local large language models) on Ubuntu 22.04 or 24.04. Everything runs in Docker, secured with HTTPS via Caddy and optional Basic Auth. If you have an NVIDIA GPU, you can enable GPU acceleration to speed up model inference dramatically.

What you will need

- An Ubuntu 22.04/24.04 server with at least 8 GB RAM and 20 GB free disk space. For GPU acceleration, an NVIDIA GPU with recent drivers is recommended (e.g., 8 GB VRAM or more for larger models).

- A domain name pointing to your server’s public IP (A/AAAA record). Ports 80 and 443 should be open to the internet for Let’s Encrypt.

- A non-root user with sudo privileges.

Step 1 — Install Docker and Docker Compose plugin

Update your system and install Docker from the official repository:

sudo apt update && sudo apt install -y ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER && newgrp docker

Step 2 — (Optional) Enable NVIDIA GPU for containers

Install the NVIDIA driver (if not already installed) and the NVIDIA container toolkit so Docker can access your GPU.

sudo ubuntu-drivers install (or choose a specific driver, e.g., sudo apt install -y nvidia-driver-535)

sudo reboot

Install the container toolkit:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit.gpg

curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit.gpg] https://#' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt update && sudo apt install -y nvidia-container-toolkit

sudo nvidia-ctk runtime configure --runtime=docker

sudo systemctl restart docker

Test GPU access:

docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

Step 3 — Prepare Docker Compose and Caddy

Create a project folder and move into it:

mkdir -p ~/ai-stack && cd ~/ai-stack

Create a file named docker-compose.yml with the following content (replace your.domain.com later in Caddyfile):

services:
ollama:
image: ollama/ollama:latest
container_name: ollama
volumes:
- ollama:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=2h
ports:
- "127.0.0.1:11434:11434"
restart: unless-stopped
# Uncomment the next line if you enabled NVIDIA toolkit
# gpus: all

openwebui:
image: ghcr.io/open-webui/open-webui:main
container_name: openwebui
depends_on:
- ollama
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- WEBUI_AUTH=True
ports:
- "127.0.0.1:8080:8080"
volumes:
- openwebui:/app/backend/data
restart: unless-stopped

caddy:
image: caddy:2
container_name: caddy
depends_on:
- openwebui
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config

volumes:
ollama:
openwebui:
caddy_data:
caddy_config:

Create a file named Caddyfile in the same folder. Replace your.domain.com with your real domain and the email with yours:

your.domain.com {
encode zstd gzip
tls [email protected]
# Optional Basic Auth — generate a hashed password below and uncomment
# basicauth {
# admin <paste_hashed_password_here>
# }
reverse_proxy openwebui:8080
}

If you want Basic Auth, generate a hash:

docker run --rm caddy:2 caddy hash-password --plaintext "StrongPassword!"

Copy the hash output, paste it into the Caddyfile under basicauth, and uncomment the lines.

Step 4 — Start the stack and pull a model

Start the services:

docker compose up -d

Pull a model with Ollama. Llama 3.1 is a great default; you can also choose smaller variants if you have less VRAM:

docker exec -it ollama ollama pull llama3.1

For low VRAM systems, try a quantized build like llama3.1:8b-instruct-q4_0 or a compact model like mistral:7b-instruct:

docker exec -it ollama ollama pull mistral:latest

Verify Ollama is up:

curl -s http://127.0.0.1:11434/api/tags

Step 5 — Access Open WebUI over HTTPS

Wait 30–60 seconds for Caddy to obtain a Let’s Encrypt certificate. Then browse to https://your.domain.com. On the first visit, create your Open WebUI admin user. In Settings > Models, select the model you pulled with Ollama. You can now chat privately with your local LLM through a friendly web interface.

Step 6 — Security hardening (recommended)

- Keep Open WebUI behind Caddy only. We already published it on localhost (127.0.0.1) to prevent direct exposure.

- Enable Basic Auth in your Caddyfile if you plan to expose the site to the open internet. Use a long, unique password.

- Restrict admin features in Open WebUI to your own account. Disable public sign-ups if you do not need them.

- Consider a firewall rule to allow inbound 80/443 only, and block 8080/11434 from the WAN.

Step 7 — Backups and updates

Back up Open WebUI data:

docker run --rm -v openwebui:/d -v $PWD:/b busybox tar czf /b/openwebui-backup.tgz -C /d .

Back up Ollama models (can be large):

docker run --rm -v ollama:/d -v $PWD:/b busybox tar czf /b/ollama-backup.tgz -C /d .

To update containers:

docker compose pull && docker compose up -d

To remove old images:

docker image prune -f

Troubleshooting

- Check logs if something fails to start: docker compose logs -f

- Verify DNS and port 80/443 reach the server; Let’s Encrypt must connect over HTTP/HTTPS the first time.

- If certificates fail, restart the stack after DNS propagates: docker compose down && docker compose up -d

- If the GPU is not detected, confirm nvidia-smi works on the host and that you added gpus: all under the Ollama service.

- Test the Ollama API locally: curl http://127.0.0.1:11434/api/generate -d '{"model":"llama3.1","prompt":"hi"}'

Where to go next

Explore model variants optimized for your hardware (Q4 for low VRAM, Q6/Q8 for higher quality, FP16 on strong GPUs). Add embeddings and RAG features in Open WebUI to chat over your documents. With this setup, you keep your data and traffic on your own server, with clean HTTPS, optional password protection, and fast local inference.

Expose Your Home Lab Securely: Cloudflare Tunnel + Nginx Proxy Manager (Docker Guide)

Publishing self-hosted apps on the internet without opening ports is now simple and safe with Cloudflare Tunnel and Nginx Proxy Manager (NPM). This guide shows you how to route traffic from your domain through Cloudflare’s global network to your internal services, using Docker on Linux. You will get HTTPS by default, Zero Trust access, and simple management for multiple apps.

Why use Cloudflare Tunnel + Nginx Proxy Manager?

Cloudflare Tunnel (cloudflared) creates an outbound-only connection from your server to Cloudflare, so you do not need port forwarding or a public IP. Nginx Proxy Manager provides a friendly UI to reverse-proxy multiple services, manage SSL, and handle redirects and headers. Together, they offer a secure and flexible edge-to-origin pipeline for home labs and small businesses.

Prerequisites

- A domain managed by Cloudflare (nameservers must point to Cloudflare)
- A Linux server (Ubuntu 22.04+ recommended) with Docker and Docker Compose installed
- Basic familiarity with terminal and Docker

Architecture overview

Cloudflare edge terminates TLS and forwards requests over an encrypted tunnel to the cloudflared container on your server. The cloudflared service forwards hostnames to Nginx Proxy Manager, which then routes requests to internal applications (e.g., Home Assistant, Portainer, Jellyfin) based on hostnames. This design centralizes access, certificates, logging, and security rules.

Step 1 — Install Docker and Compose (Ubuntu)

Run the following commands to set up Docker:

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

Step 2 — Create a Cloudflare Tunnel

1) In the Cloudflare dashboard, open Zero Trust (or Tunnels) and create a new Tunnel named “homelab”.
2) Choose “Docker” as the environment and copy the token-based command or credentials JSON for cloudflared. We will use the token method for simplicity.
3) Do not add routes yet—we will define them in the docker-compose file.

Step 3 — Create docker-compose.yml

Create a working directory (e.g., /opt/homelab) and add this compose file. Replace example.com with your domain and paste your Cloudflare tunnel token.

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:2024.8.3
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=<PASTE_YOUR_TUNNEL_TOKEN>
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "cloudflared", "version"]
      interval: 30s
      timeout: 10s
      retries: 3

  npm:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      - "81:81"        # NPM admin UI
      - "80:80"        # HTTP
      - "443:443"      # HTTPS
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - cloudflared

networks:
  default:
    name: homelab

Start the stack:

docker compose up -d
docker compose logs -f cloudflared

Step 4 — Map hostnames to Nginx Proxy Manager

Back in the Cloudflare dashboard, open your Tunnel and add public hostnames to route traffic to NPM. For each app, create a route like this:

Example routes:
- app1.example.com → http://npm:80
- media.example.com → http://npm:80
- portainer.example.com → http://npm:80

This means Cloudflare forwards incoming requests for those hostnames down the tunnel to the NPM container.

Step 5 — Configure Nginx Proxy Manager

1) Open http://YOUR_SERVER_IP:81 and log in (default admin credentials are shown on first boot; change them immediately).
2) For each internal service, create a “Proxy Host”:
- Domain Names: app1.example.com
- Scheme: http
- Forward Hostname/IP: the internal container name or IP (e.g., homeassistant or 127.0.0.1)
- Forward Port: the app port (e.g., 8123)
- Block Common Exploits: enabled
- Websockets Support: enabled (for apps like Home Assistant, Portainer, or anything real-time)

3) Under the SSL tab, select “Request a new SSL Certificate” and choose Let’s Encrypt. Enable “Force SSL” and “HTTP/2 Support”. NPM will fetch and auto-renew certificates for the hostname.

Step 6 — Enforce Zero Trust access (optional but recommended)

Use Cloudflare Access to protect sensitive apps with identity-aware policies. In Zero Trust → Access → Applications, add an app for app1.example.com, choose “Self-hosted”, and require login with your IdP (Google, GitHub, Microsoft, etc.). You can restrict by email domain, group, or country. Access will challenge users at the edge before requests hit your tunnel.

Step 7 — Security hardening tips

- In Cloudflare DNS, keep your A/AAAA records orange-cloud (proxied).
- Enable WAF and Bot Fight Mode where appropriate.
- For APIs or admin panels, add Access policies and IP allowlists in Cloudflare, and enable “Block Common Exploits” in NPM.
- If you need end-to-end encryption to NPM, set up a trusted origin certificate from Cloudflare and configure NPM to use HTTPS upstreams.

Troubleshooting

502 Bad Gateway: Check the NPM “Forward Hostname/IP” and port. Ensure the target app is reachable from the NPM container network.

403 from Cloudflare Access: Confirm your email is allowed by the Access policy and that your device clock is correct.

WebSockets not working: Enable “Websockets Support” in NPM and verify the app uses the correct path. Most real-time dashboards require this.

Large uploads fail: In NPM, add a Custom Nginx config snippet such as client_max_body_size 100m; for the specific host.

SSL mismatch or loops: Use HTTP between Cloudflare and NPM if Cloudflare terminates TLS at the edge. If you enable origin TLS, make sure certificates and trust are configured properly.

Scaling and operations

- Add more hostnames in the Tunnel as you publish new services; just point them to http://npm:80 and configure the upstream in NPM.
- Use multiple cloudflared instances (on different servers) in the same Tunnel for high availability; Cloudflare will load-balance them.
- Monitor with docker compose logs -f and Cloudflare analytics. Schedule updates with watchtower or perform manual rolling updates.

Wrap-up

You have exposed internal services securely on your own domain without opening any inbound ports. Cloudflare Tunnel handles the edge and connectivity, while Nginx Proxy Manager gives you a clean UI for reverse proxy rules, SSL, and performance tweaks. With Access policies, WAF, and good Nginx hygiene, you can safely run public-facing apps from a single Docker host.

How to Deploy Traefik v3 as a Docker Reverse Proxy with Automatic HTTPS (Let’s Encrypt)

Overview

Traefik v3 is a modern reverse proxy and load balancer that integrates smoothly with Docker, automatically discovering containers and routing traffic to them. In this guide, you will deploy Traefik v3 as a Docker reverse proxy with automatic HTTPS using Let’s Encrypt via the DNS challenge (Cloudflare as an example). The setup is fast, reproducible, and ideal for hosting multiple apps on a single server with clean domain names and valid TLS certificates.

Prerequisites

Before you start, you will need: a Linux server (Ubuntu 22.04/24.04 works great) with root or sudo access, Docker and Docker Compose installed, a domain name you control (e.g., example.com), ports 80 and 443 open to the server, and an API token from your DNS provider (Cloudflare in this tutorial) with permission to manage DNS (Zone:DNS:Edit). Create A or AAAA records for the subdomains you plan to use (e.g., traefik.example.com, app.example.com) pointing to your server’s public IP.

Step 1: Create a dedicated Docker network

Why: Keeping a shared “proxy” network lets Traefik see and route to other containers without exposing them on the host. Run:

docker network create proxy

Step 2: Prepare your working directory

Create a folder for Traefik and a place to store Let’s Encrypt certificates. The ACME store must be persistent across restarts.

mkdir -p ~/traefik/letsencrypt
cd ~/traefik

Step 3: Create a .env file for secrets

To avoid hardcoding tokens in your Compose file, use a .env file in the same directory. Replace values with your own.

CF_DNS_API_TOKEN=your_cloudflare_dns_token_here
[email protected]
TRAEFIK_DOMAIN=traefik.example.com
APP_DOMAIN=app.example.com

Step 4: Write docker-compose.yml

The Compose file below launches Traefik v3 and a sample “whoami” app to test routing and TLS. It enables the Docker provider, sets HTTP to HTTPS redirection, configures Let’s Encrypt with the Cloudflare DNS challenge, and exposes the dashboard on a secure subdomain.

version: "3.9"

services:
  traefik:
   image: traefik:v3.1
   container_name: traefik
   command:
    - --providers.docker=true
    - --providers.docker.exposedbydefault=false
    - --entryPoints.web.address=:80
    - --entryPoints.websecure.address=:443
    - --entryPoints.web.http.redirections.entryPoint.to=websecure
    - --entryPoints.web.http.redirections.entryPoint.scheme=https
    - --certificatesResolvers.le.acme.dnsChallenge.provider=cloudflare
    - --certificatesResolvers.le.acme.email=${LETSENCRYPT_EMAIL}
    - --certificatesResolvers.le.acme.storage=/letsencrypt/acme.json
    - --log.level=INFO
    - --api.dashboard=true
   environment:
    - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
   ports:
    - "80:80"
    - "443:443"
   volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - ./letsencrypt:/letsencrypt
   networks:
    - proxy
   restart: unless-stopped
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DOMAIN}`)"
    - "traefik.http.routers.dashboard.entrypoints=websecure"
    - "traefik.http.routers.dashboard.tls.certresolver=le"
    - "traefik.http.routers.dashboard.service=api@internal"
    - "traefik.http.middlewares.secure.headers.stsSeconds=31536000"
    - "traefik.http.middlewares.secure.headers.stsIncludeSubdomains=true"
    - "traefik.http.middlewares.secure.headers.stsPreload=true"
    - "traefik.http.middlewares.secure.headers.contentTypeNosniff=true"
    - "traefik.http.middlewares.secure.headers.browserXssFilter=true"

  whoami:
   image: traefik/whoami:latest
   networks:
    - proxy
   labels:
    - "traefik.enable=true"
    - "traefik.http.routers.who.rule=Host(`${APP_DOMAIN}`)"
    - "traefik.http.routers.who.entrypoints=websecure"
    - "traefik.http.routers.who.tls.certresolver=le"
    - "traefik.http.routers.who.middlewares=secure@docker"
    - "traefik.http.services.who.loadbalancer.server.port=80"
   restart: unless-stopped

networks:
  proxy:
   external: true

Step 5: Start Traefik and verify certificates

Bring the stack up and watch the logs for the ACME flow. Traefik will create a DNS TXT record via the API token and obtain certificates automatically.

docker compose up -d
docker logs -f traefik

On first successful issuance, an acme.json file will be created under the letsencrypt folder. Set strict permissions on it to keep private keys safe.

chmod 600 ./letsencrypt/acme.json

Step 6: Test the routes

Open https://traefik.example.com to see the dashboard and https://app.example.com to reach the whoami test application. Both should show a valid TLS lock in your browser. If you use Cloudflare proxy (orange cloud), DNS challenge still works because the validation is done via DNS records, not HTTP.

Optional: Protect the dashboard

Do not leave the dashboard open. You can add basic auth using a middleware. Create a bcrypt or Apache MD5 hash and replace the placeholder below. For a quick hash, you can use the “htpasswd” utility.

labels:
- "traefik.http.middlewares.dash-auth.basicauth.users=admin:$$apr1$$replace$$hashvaluehere"
- "traefik.http.routers.dashboard.middlewares=dash-auth@docker,secure@docker"

Adding your own apps

To publish another container behind Traefik, attach it to the “proxy” network and add three labels: a Host rule for your domain, the HTTPS entrypoint, and the certresolver. Optionally apply the “secure” headers middleware. Example:

labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`blog.example.com`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=le"
- "traefik.http.routers.blog.middlewares=secure@docker"

Troubleshooting

Port 80/443 already in use: Stop any existing web servers (e.g., Nginx, Apache) or change their ports. Traefik must bind to 80 and 443 to handle redirection and TLS termination.

Certificate issuance fails: Check Traefik logs for ACME errors. Verify the Cloudflare token has Zone:DNS:Edit on the correct zone, and that your environment variable is loaded by Compose. If you are using multiple zones, the token must cover them too.

404 or 502 errors: Ensure your container has traefik.enable=true, the Host rule matches your requested domain, DNS records resolve to your server, and the service port label points to the right container port.

Slow or failed DNS propagation: Give it a minute after creating DNS records. If using split DNS on a LAN, make sure internal resolvers return the public IP.

Why DNS challenge?

The DNS challenge avoids opening custom ports for validation and supports wildcard certificates. It is reliable behind CDNs, in NAT environments, and when you want to cover many subdomains without adding each one by hand.

What you achieved

You now have a production-ready Traefik v3 reverse proxy on Docker with automatic HTTPS, security headers, and a pattern you can reuse for any containerized app. Add services, set Host rules, and Traefik will handle routing and certificate management for you. This setup cuts down on boilerplate, centralizes TLS, and keeps your stack maintainable as it grows.

How to Deploy Traefik v3 with Docker Compose and Automatic HTTPS via Cloudflare DNS-01

Overview

This tutorial shows how to deploy Traefik v3 as a modern reverse proxy with Docker Compose and automatic HTTPS certificates using Let’s Encrypt via the Cloudflare DNS-01 challenge. The DNS-01 method works even behind NAT, on residential ISPs, and with Cloudflare’s orange cloud (proxy) enabled. By the end, you will have a secure, production-ready reverse proxy and a sample container accessible over HTTPS.

Prerequisites

- A Linux server (Ubuntu 22.04/24.04 or similar) with Docker Engine and Docker Compose v2 installed.
- A domain managed by Cloudflare (nameservers pointing to Cloudflare).
- A Cloudflare API token with Zone.DNS:Edit and Zone:Read permissions for the zone you’ll use.
- Basic terminal access and a user with Docker privileges.

Step 1 — Verify Docker and Compose

Confirm your Docker setup is ready. Run: docker --version and docker compose version. If Compose v2 is not present, install the latest Docker Engine from the official repository. On Ubuntu, ensure the docker group exists and your user is a member: sudo usermod -aG docker $USER then re-log.

Step 2 — Create a Dedicated Docker Network

Create an external network so Traefik can share it with your app containers: docker network create proxy. Using a dedicated network helps isolate traffic and makes adding new services predictable.

Step 3 — Prepare Folders and Secrets

Make a working directory for Traefik and create a place to store ACME data (certificates):

mkdir -p ~/traefik/letsencrypt
cd ~/traefik
touch ./letsencrypt/acme.json
chmod 600 ./letsencrypt/acme.json

Create an .env file to hold your Cloudflare token securely. This file will be read by Docker Compose:

echo "CF_DNS_API_TOKEN=<paste_your_cloudflare_api_token>" > .env

The API token should include Zone.DNS:Edit and Zone.Zone:Read permissions for the domain. Restrict the token to the specific zone for better security.

Step 4 — Create docker-compose.yml

Create a docker-compose.yml file in ~/traefik with the following content. Replace [email protected] with your email and whoami.example.com with a real subdomain in your zone.

services:
  traefik:
    image: traefik:v3.1
    command:
     - --providers.docker=true
     - --providers.docker.exposedbydefault=false
     - --entrypoints.web.address=:80
     - --entrypoints.websecure.address=:443
     - --entrypoints.web.http.redirections.entrypoint.to=websecure
     - --entrypoints.web.http.redirections.entrypoint.scheme=https
     - [email protected]
     - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
     - --certificatesresolvers.letsencrypt.acme.dnschallenge=true
     - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
     - --api.dashboard=true
    ports:
     - "80:80"
     - "443:443"
    environment:
     - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    volumes:
     - /var/run/docker.sock:/var/run/docker.sock:ro
     - ./letsencrypt:/letsencrypt
    networks:
     - proxy

  whoami:
    image: traefik/whoami:v1.10
    labels:
     - "traefik.enable=true"
     - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
     - "traefik.http.routers.whoami.entrypoints=websecure"
     - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
    networks:
     - proxy

networks:
  proxy:
    external: true

Create a DNS A/AAAA record for whoami.example.com pointing to your server’s public IP in Cloudflare. The orange cloud (proxy) can be ON or OFF; DNS-01 works either way.

Step 5 — Launch and Test

Start the stack from the ~/traefik directory: docker compose up -d. Traefik will request a certificate from Let’s Encrypt using the Cloudflare DNS-01 challenge. Check logs with docker compose logs -f traefik to confirm issuance (look for “Server responded with a certificate”).

Open https://whoami.example.com in your browser. You should see the whoami test service showing headers and IP details over HTTPS.

Optional: Secure the Traefik Dashboard

The dashboard is enabled but not published by default in this setup. To expose it safely, add labels to a new service or to Traefik itself using a distinct host like traefik.example.com, require basic auth middleware, and keep it behind TLS. Always disable --api.insecure=true in production.

Troubleshooting Tips

- If certificates do not issue, verify the API token has Zone.DNS:Edit and is scoped to the correct zone. Also confirm the .env is loaded and the environment variable name matches.
- If you see rate-limit errors, you may have requested too many certificates; wait and try again or use the Let’s Encrypt staging endpoint during testing (--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory).
- Ensure your firewall allows TCP 80 and 443 inbound to the server.

Maintaining and Adding Services

To add a new app behind Traefik, place it on the proxy network and attach labels for rule, entrypoint, and TLS resolver. Example labels: traefik.enable=true, traefik.http.routers.app.rule=Host(`app.example.com`), traefik.http.routers.app.entrypoints=websecure, and traefik.http.routers.app.tls.certresolver=letsencrypt. Restart only the new service; Traefik hot-reloads routes automatically.

Conclusion

With Traefik v3, Docker Compose, and Cloudflare’s DNS-01 challenge, you can ship secure containers with automatic HTTPS and minimal friction. This setup scales cleanly, keeps certificates current, and works in challenging network environments. Add your apps with labels, keep tokens scoped and secret, and enjoy a tidy, TLS-by-default container platform.

3.

How to Publish Your Home Lab Apps with Cloudflare Tunnel and Zero Trust Access (No Port Forwarding)

Overview

Exposing self-hosted services to the internet usually means opening ports, managing dynamic DNS, and hardening firewalls. Cloudflare Tunnel (cloudflared) flips that model. Your server dials out to Cloudflare, creates an encrypted tunnel, and serves traffic from a Cloudflare edge without any inbound ports. Add Cloudflare Zero Trust Access on top, and you get identity‑aware filtering, one‑time PIN, and granular policies. This guide shows how to publish a web app from a Linux server using Cloudflare Tunnel and secure it with Zero Trust.

Prerequisites

- A Cloudflare account with your domain added and nameservers pointing to Cloudflare.

- A Linux host (Ubuntu/Debian examples below) running the service you want to expose (e.g., a dashboard on port 8080).

- Shell access with sudo privileges.

Step 1 — Install cloudflared

On Ubuntu/Debian, install the official package. The commands below add the repository, verify signatures, and install cloudflared.

# Add Cloudflare GPG key and repository
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/cloudflared.list

# Install cloudflared
sudo apt update
sudo apt install -y cloudflared

# Verify
cloudflared --version

On other distributions, you can use a static binary or Docker. The rest of the steps are identical.

Step 2 — Authenticate and create a named tunnel

Run an interactive login to authorize cloudflared with your Cloudflare account. Your browser will open and ask you to pick the domain.

cloudflared tunnel login

Create a tunnel with a friendly name. This command outputs a UUID and writes a credentials JSON file into ~/.cloudflared.

cloudflared tunnel create homelab-tunnel

Keep the UUID; you will need it in the configuration file.

Step 3 — Map a public hostname to your local service

Choose a subdomain such as app.example.com and route it to the tunnel. Cloudflared will create the necessary DNS record in Cloudflare automatically.

cloudflared tunnel route dns homelab-tunnel app.example.com

Now define the ingress rules that connect the hostname to your local service (for example, a web UI on http://localhost:8080). Create a config at ~/.cloudflared/config.yml:

tunnel: <your-tunnel-uuid>
credentials-file: /home/<your-user>/.cloudflared/<your-tunnel-uuid>.json

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - service: http_status:404

The final catch‑all rule returns a 404 for unmatched hostnames, which is safer than accidentally proxying everything.

Step 4 — Run cloudflared as a service

Install cloudflared as a system service so it survives reboots and restarts automatically.

# From within the directory containing ~/.cloudflared/config.yml
sudo cloudflared service install

# Start and enable on boot
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

If you prefer foreground mode for testing, you can run: cloudflared tunnel run homelab-tunnel, then switch to the service after you confirm it works.

Step 5 — Secure the app with Cloudflare Zero Trust Access

With the tunnel working, the app is reachable at your hostname. Next, lock it behind identity-aware access.

1) In the Cloudflare dashboard, go to Zero Trust → Access → Authentication and add an identity provider (Google, Microsoft, GitHub, or email OTP). Keep One-time PIN enabled for easy onboarding.

2) Go to Zero Trust → Access → Applications → Add an application → Self-hosted. Enter:

- Application name: Homelab App

- Domain: app.example.com

- Session duration: e.g., 12 hours

3) Create a policy: Include only your email(s) or a Google Workspace group. Optionally add country restrictions, device posture checks, or require MFA. Save the app.

From now on, anyone visiting app.example.com must authenticate. Cloudflare enforces the policy before traffic touches your origin.

Optional hardening

- Run your local service on 127.0.0.1 so it is not exposed on the LAN. Update your app config to bind to localhost.

- Use mTLS origin authentication (Cloudflare Origin Cert) to ensure only your tunnel can reach the service.

- Split production and testing into separate tunnels with distinct hostnames and policies.

- Enable HTTP/2 or WebSockets if your app needs them; cloudflared supports both.

Troubleshooting tips

- Check logs: journalctl -u cloudflared -f shows real-time output from the service.

journalctl -u cloudflared -n 200 --no-pager
cloudflared tunnel list
cloudflared tunnel info homelab-tunnel
cloudflared tunnel ingress validate

- DNS not resolving? Confirm the CNAME record for app.example.com exists and points to your tunnel. Clear local DNS cache or wait a few minutes for propagation.

- 502/504 errors? Ensure the local app is listening and reachable at the service URL in config.yml. Try curl http://localhost:8080 from the server.

- Multiple apps? Add more hostname blocks under ingress and create matching Access apps.

Maintenance and updates

Keep cloudflared current to receive security patches and new features.

sudo apt update
sudo apt install --only-upgrade cloudflared

Back up ~/.cloudflared/ (credentials and config) and your systemd unit files if customized. If you rotate the tunnel credentials, update the credentials-file path accordingly and restart the service.

Wrap-up

Cloudflare Tunnel gives you a secure, low-friction way to publish internal services without opening ports or managing a reverse proxy on the perimeter. Pairing it with Zero Trust Access means your apps live behind identity, not just IP addresses, and you can layer device posture, MFA, and short sessions. In a few commands, you can make your home lab or small business apps safer and easier to reach from anywhere.

How to Securely Publish Self‑Hosted Apps with Cloudflare Tunnel and Zero Trust (Docker How‑To)

Overview

Cloudflare Tunnel lets you expose private services to the internet without opening inbound firewall ports or managing a reverse proxy. It establishes an outbound-only, encrypted connection from your host to Cloudflare’s edge, and pairs perfectly with Cloudflare Zero Trust Access for SSO, device checks, and detailed auditing. In this tutorial, you will deploy Cloudflare Tunnel with Docker, route multiple apps under different subdomains, and protect them with Zero Trust policies. The steps are simple, reproducible, and friendly to environments behind NAT or CGNAT.

Prerequisites

- A Cloudflare account with a domain managed by Cloudflare DNS.

- Docker and Docker Compose v2 on a Linux host (Ubuntu/Debian/Alpine are fine).

- At least one internal web service running in Docker or on the host (e.g., Grafana on port 3000, Nextcloud on 80, Syncthing on 8384).

- Optional but recommended: an identity provider (Google, GitHub, Azure AD, Okta) to enforce SSO via Zero Trust Access.

Step 1 — Create a Tunnel in Cloudflare Zero Trust

1) Go to Cloudflare dashboard > Zero Trust > Access > Tunnels and click “Create a tunnel.” Name it (e.g., home-net).

2) Choose the Docker option. Cloudflare will generate a TUNNEL_TOKEN for you. Keep this token safe; it lets cloudflared authenticate the tunnel without storing local credentials.

3) You can add “Public Hostnames” later in the dashboard, or define routing via a local config file. This guide shows both approaches, starting with the fast token-only method.

Step 2 — Fast Deploy with Docker Compose (Token Method)

Create a directory (e.g., /opt/cloudflared) and a Docker Compose file. Replace TUNNEL_TOKEN_VALUE with the token you copied in Step 1.

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=TUNNEL_TOKEN_VALUE
    # Optional metrics for monitoring
    # ports:
    #   - "2000:2000"
    # command: tunnel --no-autoupdate run --metrics 0.0.0.0:2000

Bring it up:

docker compose up -d

Cloudflared will dial out to Cloudflare’s edge over QUIC/TLS. No inbound ports are needed. Next, from the Zero Trust dashboard, add “Public Hostnames” for each app:

- grafana.example.com → http://localhost:3000 (or your internal service URL)

- files.example.com → http://localhost:80

- sync.example.com → http://localhost:8384

Cloudflare automatically creates DNS records for these hostnames and routes traffic through the tunnel.

Step 3 — Config-Driven Ingress (Advanced, Reproducible)

If you prefer everything as code, create a named tunnel and a local config file. This offers better portability and version control. First, create the tunnel credentials once on any machine (can be the same host):

# Authenticate and create a named tunnel (locally)
docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel login

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel create home-net

# Show tunnels (note the Tunnel UUID)
docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel list

This creates a credentials file named after the tunnel UUID. Now write config.yml in the same directory:

# ~/.cloudflared/config.yml
tunnel: HOME_NET_TUNNEL_UUID
credentials-file: /home/nonroot/.cloudflared/HOME_NET_TUNNEL_UUID.json
ingress:
  - hostname: grafana.example.com
    service: http://grafana:3000
  - hostname: files.example.com
    service: http://nextcloud:80
  - hostname: sync.example.com
    service: http://syncthing:8384
  - service: http_status:404
protocol: quic
warp-routing:
  enabled: false

If your apps run in Docker, place cloudflared on the same user-defined network so it can reach them by container name. Example Compose:

version: "3.8"
networks:
  apps:
    driver: bridge

services:
  grafana:
    image: grafana/grafana:latest
    networks: [apps]
    expose:
      - "3000"
    environment:
      - GF_SERVER_ROOT_URL=http://grafana.example.com

  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    networks: [apps]
    volumes:
      - ~/.cloudflared:/home/nonroot/.cloudflared:ro

Finally, register DNS routes (one-time):

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net grafana.example.com

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net files.example.com

docker run --rm -it \
  -v ~/.cloudflared:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns home-net sync.example.com

Step 4 — Protect with Zero Trust Access

Go to Zero Trust > Access > Applications > Add an application > Self-hosted. For each hostname you exposed, create an app and a policy:

- Choose your subdomain (e.g., grafana.example.com) and path (/*).

- Set a policy that requires SSO (Google/GitHub/Azure AD/Okta) or One-Time Pin.

- Optionally restrict to emails ending with @yourcompany.com or specific GitHub teams.

- Enable device posture checks if you use Cloudflare WARP (e.g., only allow managed devices).

With Access policies enforced, even if a URL leaks, visitors must authenticate before the origin sees any traffic.

Troubleshooting Tips

- 502/504 on a hostname: verify the upstream address in config points to a reachable service (container name + port or localhost + port). If using Docker networks, ensure both services share the same network.

- DNS not resolving: confirm the tunnel is “Healthy” and that the DNS record exists in your Cloudflare DNS zone. Propagation is usually instant because the record is proxied.

- Large uploads: consider enabling chunked uploads or tuning upstream app limits (e.g., client_max_body_size for Nginx-based apps). Cloudflare supports HTTP/2/3 on the edge; the tunnel runs over QUIC by default.

- Conflicting ports: cloudflared does not require inbound ports. If you exposed metrics on 2000, make sure it doesn’t collide with other services.

Security and Operations Best Practices

- Principle of least privilege: restrict Access policies to known users or groups; avoid wildcard “Allow all.”

- Separate hostnames for admin panels and public apps. Apply stricter policies (MFA, device checks) to admin endpoints.

- Use config as code where possible. Commit your cloudflared config.yml to a private repo and use environment-specific files for staging/production.

- Monitor tunnel health: expose metrics (–metrics 0.0.0.0:2000) and scrape with Prometheus. Alert on disconnects or high reconnect counts.

- Keep images updated: pin to a recent cloudflared tag and schedule updates. Test changes in staging first.

Wrap-Up

You deployed Cloudflare Tunnel with Docker, routed multiple internal services under friendly subdomains, and enforced Zero Trust Access in front of them—without opening a single inbound port. This pattern scales from homelabs to production, simplifies TLS and DNS, and raises your security baseline with SSO, device posture, logging, and revocation. If you outgrow manual steps, codify everything with config.yml, Compose files, and IaC for a repeatable, auditable setup.

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...