Build Traefik on a Proxmox VM with Docker Compose with the help of AI

the Goal

We are building a clean Docker gateway VM inside Proxmox.

This VM will later run:

Docker
Traefik
Portainer
n8n
PostgreSQL
Cloudflare wildcard SSL

Important design rule:

Do not install Docker, Traefik, directly on the Proxmox host.
Keep the Proxmox host clean.
Run services inside VMs.

Architecture

Hetzner Dedicated Server
        โ†“
Proxmox Host
        โ†“
VM 100: docker-gateway
        โ†“
Docker Engine
        โ†“
Traefik container
        

VM 100 uses a private network:

Proxmox vmbr1: 10.10.100.1/24
VM 100:        10.10.100.100/24
Gateway:       10.10.100.1

Traefik runs inside VM 100, not on the Proxmox host.


Security Strategy

For the first setup:

Hetzner firewall: ON
Proxmox firewall: OFF
Traefik: reverse proxy only
Public TCP 80/443: do not open until Traefik is ready

Traefik will later control:

HTTP 80
HTTPS 443
domain routing
SSL certificates
redirects
middlewares
dashboard protection

Traefik does not control:

SSH
Proxmox firewall
Hetzner firewall
Jitsi UDP 10000
TURN ports
Linux kernel firewall

Step 1 โ€” Create VM 100 in Proxmox

Recommended VM settings:

VM ID: 100
Name: docker-gateway
OS: Debian 13 minimal
CPU: 4 cores
RAM: 8 GB
Disk: 200 GB
Storage: local-zfs
Network: vmbr1
Model: VirtIO
Firewall: OFF

Use Debian minimal install.

During install, select only:

standard system utilities

If there is no DHCP, continue without network and configure static networking later.


Step 2 โ€” Create Private Bridge vmbr1 on Proxmox

In Proxmox UI:

pve1 โ†’ System โ†’ Network โ†’ Create โ†’ Linux Bridge

Use:

Name: vmbr1
IPv4/CIDR: 10.10.100.1/24
Gateway: blank
Bridge ports: blank
Autostart: checked
VLAN aware: unchecked
Comment: Private VM NAT network

Do not edit vmbr0.

Apply configuration.


Step 3 โ€” Connect VM 100 to vmbr1

In Proxmox:

VM 100 โ†’ Hardware โ†’ Network Device โ†’ Edit

Set:

Bridge: vmbr1
Model: VirtIO
Firewall: OFF

Reboot VM 100.


Step 4 โ€” Configure Static IP inside VM 100

RUN THIS ON: VM 100 console

Become root:

su -

Edit network config:

nano /etc/network/interfaces

Use:

auto lo
iface lo inet loopback

auto ens18
iface ens18 inet static
    address 10.10.100.100/24
    gateway 10.10.100.1
    dns-nameservers 1.1.1.1 8.8.8.8

Save:

CTRL + O
Enter
CTRL + X

Reboot:

reboot

Test private gateway:

ping -c 3 10.10.100.1

Success:

3 packets transmitted, 3 received

Step 5 โ€” Add Proxmox NAT for VM Internet

RUN THIS ON: Proxmox host over SSH

From Mac Terminal:

ssh proxmox

Then on Proxmox:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.10.100.0/24 -o vmbr0 -j MASQUERADE
iptables -A FORWARD -i vmbr1 -o vmbr0 -s 10.10.100.0/24 -j ACCEPT
iptables -A FORWARD -i vmbr0 -o vmbr1 -d 10.10.100.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT

Test from VM 100:

ping -c 3 1.1.1.1

Success:

3 packets transmitted, 3 received

Step 6 โ€” Fix DNS inside VM 100

Important lesson learned:

The correct file is:

/etc/resolv.conf

Not:

/etc/resolve.conf

If DNS fails, check:

RUN THIS ON: VM 100 console

ls -la /etc/resolv.conf /etc/resolve.conf 2>&1

Correct DNS file:

nano /etc/resolv.conf

Use:

nameserver 1.1.1.1
nameserver 8.8.8.8

Test:

ping -c 3 deb.debian.org

Success:

3 packets transmitted, 3 received

Step 7 โ€” Fix Debian APT Sources

Because Debian was installed without a network mirror, fix:

RUN THIS ON: VM 100 console

nano /etc/apt/sources.list

Use:

# deb cdrom:[Debian GNU/Linux 13.5.0 _Trixie_ - Official amd64 NETINST with firmware]/ trixie contrib main non-free-firmware

deb http://deb.debian.org/debian trixie main contrib non-free-firmware
deb http://deb.debian.org/debian trixie-updates main contrib non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free-firmware

Run:

apt update

Then:

apt upgrade -y

Step 8 โ€” Install Basic Tools

RUN THIS ON: VM 100 console

apt install -y sudo openssh-server curl ca-certificates gnupg qemu-guest-agent

Enable services:

systemctl enable --now ssh
systemctl enable --now qemu-guest-agent
usermod -aG sudo dockeradmin

Reboot:

reboot

Log in again and test:

sudo whoami

Success:

root

Step 9 โ€” SSH into VM 100

From your Mac:

RUN THIS ON: Mac Terminal

ssh proxmox

Then:

RUN THIS ON: Proxmox host over SSH

ssh [email protected]

Success prompt:

dockeradmin@docker-gatway:~$

Step 10 โ€” Install Official Docker Engine

RUN THIS ON: VM 100 over SSH

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/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Add Docker repo:

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

Update:

sudo apt update

Install Docker:

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

Test:

sudo docker run hello-world

Success:

Hello from Docker!

Step 11 โ€” Use Docker Without Sudo

RUN THIS ON: VM 100 over SSH

sudo usermod -aG docker dockeradmin

Refresh session:

newgrp docker

Test:

docker ps

Success:

CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

Test Compose:

docker compose version

Step 12 โ€” Create Docker Folder Structure

RUN THIS ON: VM 100 over SSH

sudo mkdir -p /opt/docker/{traefik,portainer,n8n,postgres}
sudo chown -R dockeradmin:dockeradmin /opt/docker

Create Traefik network:

docker network create proxy

Confirm:

docker network ls

Success:

proxy

Step 13 โ€” Make Proxmox NAT Persistent

RUN THIS ON: Proxmox host over SSH

Create sysctl file:

cat > /etc/sysctl.d/99-vmbr1-nat.conf << 'EOF'
net.ipv4.ip_forward=1
EOF

sysctl --system

Create NAT script:

cat > /usr/local/sbin/vmbr1-nat.sh << 'EOF'
#!/bin/sh
set -eu

LAN_CIDR="10.10.100.0/24"
LAN_IF="vmbr1"
WAN_IF="vmbr0"

case "${1:-start}" in
  start)
    sysctl -w net.ipv4.ip_forward=1 >/dev/null

    iptables -t nat -C POSTROUTING -s "$LAN_CIDR" -o "$WAN_IF" -j MASQUERADE 2>/dev/null || \
      iptables -t nat -A POSTROUTING -s "$LAN_CIDR" -o "$WAN_IF" -j MASQUERADE

    iptables -C FORWARD -s "$LAN_CIDR" -i "$LAN_IF" -o "$WAN_IF" -j ACCEPT 2>/dev/null || \
      iptables -A FORWARD -s "$LAN_CIDR" -i "$LAN_IF" -o "$WAN_IF" -j ACCEPT

    iptables -C FORWARD -d "$LAN_CIDR" -i "$WAN_IF" -o "$LAN_IF" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
      iptables -A FORWARD -d "$LAN_CIDR" -i "$WAN_IF" -o "$LAN_IF" -m state --state RELATED,ESTABLISHED -j ACCEPT
    ;;

  stop)
    iptables -t nat -D POSTROUTING -s "$LAN_CIDR" -o "$WAN_IF" -j MASQUERADE 2>/dev/null || true
    iptables -D FORWARD -s "$LAN_CIDR" -i "$LAN_IF" -o "$WAN_IF" -j ACCEPT 2>/dev/null || true
    iptables -D FORWARD -d "$LAN_CIDR" -i "$WAN_IF" -o "$LAN_IF" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
    ;;

  *)
    echo "Usage: $0 {start|stop}"
    exit 1
    ;;
esac
EOF

chmod +x /usr/local/sbin/vmbr1-nat.sh

Create service:

cat > /etc/systemd/system/vmbr1-nat.service << 'EOF'
[Unit]
Description=Persistent NAT for Proxmox private VM network vmbr1
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/vmbr1-nat.sh start
ExecStop=/usr/local/sbin/vmbr1-nat.sh stop
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

Enable:

systemctl daemon-reload
systemctl enable --now vmbr1-nat.service

Check:

systemctl status vmbr1-nat.service --no-pager

Success:

Active: active (exited)

Step 14 โ€” Create Traefik YAML Files

RUN THIS ON: VM 100 over SSH

mkdir -p /opt/docker/traefik/data

touch /opt/docker/traefik/docker-compose.yaml
touch /opt/docker/traefik/data/traefik.yml
touch /opt/docker/traefik/data/config.yml
touch /opt/docker/traefik/data/acme.json

chmod 600 /opt/docker/traefik/data/acme.json

Confirm:

ls -la /opt/docker/traefik
ls -la /opt/docker/traefik/data

Step 15 โ€” Create traefik.yml

RUN THIS ON: VM 100 over SSH

cat > /opt/docker/traefik/data/traefik.yml << 'EOF'
api:
  dashboard: true

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /config.yml
    watch: true

log:
  level: INFO

accessLog: {}
EOF

Step 16 โ€” Create config.yml

RUN THIS ON: VM 100 over SSH

cat > /opt/docker/traefik/data/config.yml << 'EOF'
http:
  middlewares:
    security-headers:
      headers:
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"
        customRequestHeaders:
          X-Forwarded-Proto: "https"

    https-redirect:
      redirectScheme:
        scheme: https
        permanent: true
EOF

Step 17 โ€” Create Traefik docker-compose.yaml

RUN THIS ON: VM 100 over SSH

cat > /opt/docker/traefik/docker-compose.yaml << 'EOF'
services:
  traefik:
    image: traefik:v3.6.2
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/traefik.yml:ro
      - ./data/config.yml:/config.yml:ro
      - ./data/acme.json:/acme.json

networks:
  proxy:
    external: true
EOF

Validate:

cd /opt/docker/traefik
docker compose config

Success:

No YAML error

Step 18 โ€” Start Traefik

RUN THIS ON: VM 100 over SSH

Make sure you are inside:

/opt/docker/traefik

Run:

docker compose up -d

Check:

docker ps

Success:

traefik   Up

Check logs:

docker logs traefik --tail=50

Success:

Traefik version 3.6.2
Starting provider *file.Provider
Starting provider *traefik.Provider
Starting provider *docker.Provider

Important Issue We Hit

Using:

traefik:v3.0

caused this error:

client version 1.24 is too old. Minimum supported API version is 1.40

Fix:

sed -i 's/image: traefik:v3.0/image: traefik:v3.6.2/' /opt/docker/traefik/docker-compose.yaml

Then:

cd /opt/docker/traefik
docker compose pull
docker compose up -d

After switching to:

traefik:v3.6.2

Traefik started cleanly.


Current Status

VM 100 installed
Private NAT works
Persistent NAT service active
DNS works
APT works
Docker installed
Docker Compose installed
Docker works without sudo
Traefik files created
Traefik running as container
Traefik v3.6.2 works cleanly

Traefik is listening inside VM 100 on:

80
443

But it is not publicly exposed yet because we have not opened public TCP 80/443 in Hetzner firewall.


Next Steps

After this tutorial stage:

1. Install Portainer with YAML
2. Add protected subdomain: portainer.[DOMAIN]
3. Add protected subdomain: traefik.[DOMAIN]
4. Add n8n with PostgreSQL
5. Add Cloudflare DNS challenge
6. Add wildcard SSL certificate
7. Open TCP 80/443 in Hetzner firewall only when ready
8. Add WireGuard later for private admin access

Recommended subdomains:

traefik.[DOMAIN]
portainer.[DOMAIN]
n8n.[DOMAIN]

Do not expose dashboards publicly without protection.

Use:

strong app passwords
basic auth where needed
Cloudflare protection if desired
VPN/IP allowlist later

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *