What This Stack Provides

  • Docker: Container runtime for running isolated applications
  • Traefik: Reverse proxy with automatic SSL certificates (Let’s Encrypt)
  • Portainer: Web UI for managing Docker containers and stacks
  • Full domain management: Each service gets its own subdomain with automatic HTTPS

Prerequisites

  • Ubuntu 24.04 server (or similar)
  • Domain name configured with DNS provider (this guide uses Cloudflare)
  • SSH access to your server
  • Sudo privileges

Part 1: Install Docker

Important: Don’t use apt install docker.io - it’s outdated!

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker using official script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Add your user to docker group (no sudo needed)
sudo usermod -aG docker $USER

# Apply group changes
newgrp docker

# Verify installation
docker --version
docker compose version

You should see Docker version 27.x.x and Docker Compose v2.x.x

Known Issue: Docker 27.4.0

If you encounter API version errors with Traefik, downgrade to 27.3.1:

# Remove current version
sudo apt-get remove docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Install specific version
sudo apt-get update
sudo apt-get install -y \
    docker-ce=5:27.3.1-1~ubuntu.24.04~noble \
    docker-ce-cli=5:27.3.1-1~ubuntu.24.04~noble \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

# Prevent auto-updates
sudo apt-mark hold docker-ce docker-ce-cli

# Verify version
docker --version

Part 2: Get Cloudflare API Token

  1. Go to https://dash.cloudflare.com/profile/api-tokens
  2. Click “Create Token”
  3. Use template: “Edit zone DNS”
  4. Set permissions:
    • Zone → DNS → Edit
    • Zone → Zone → Read
  5. Zone Resources: Include → Specific zone → yourdomain.com
  6. Click “Continue to summary”“Create Token”
  7. Copy and save the token

Part 3: Install Traefik

Create Docker Network

docker network create traefik

Setup Traefik Directory

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

Create Environment File

nano .env

Add (replace with your values):

DOMAIN=yourdomain.com
CF_API_EMAIL=your-cloudflare-email@example.com
CF_DNS_API_TOKEN=your_cloudflare_api_token_here

Save: Ctrl+X, Y, Enter

Create Traefik Configuration

nano traefik.yml

Add:

api:
  dashboard: true
  debug: true

entryPoints:
  http:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
  https:
    address: ":443"

serversTransport:
  insecureSkipVerify: true

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false

certificatesResolvers:
  cloudflare:
    acme:
      email: your-email@example.com
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

Important: Change your-email@example.com to your actual email

Create Docker Compose File

nano docker-compose.yml

Add:

services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik
    ports:
      - "80:80"
      - "443:443"
    environment:
      - DOCKER_API_VERSION=1.44
    env_file:
      - .env
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./letsencrypt:/letsencrypt
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.${DOMAIN}`)"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
      - "traefik.http.routers.traefik-secure.tls.domains[0].main=${DOMAIN}"
      - "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.${DOMAIN}"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$xyz123$$abcdef"

networks:
  traefik:
    external: true

Secure the Dashboard

Generate password hash:

sudo apt install apache2-utils -y
echo $(htpasswd -nb admin yourpassword) | sed -e s/\\$/\\$\\$/g

Copy the output (e.g., admin:$$apr1$$xyz123$$abcdef) and replace the last label in docker-compose.yml:

- "traefik.http.middlewares.traefik-auth.basicauth.users=YOUR_HASH_HERE"

Start Traefik

# Set permissions
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

# Start Traefik
docker compose up -d

# Check logs
docker logs traefik -f

Add DNS Record

In Cloudflare DNS settings:

  • Type: A
  • Name: traefik
  • Content: your-server-ip
  • Proxy status: DNS only (grey cloud)

Wait 1-2 minutes, then visit: https://traefik.yourdomain.com/dashboard/

You should see a login prompt. Enter your username/password.


Part 4: Install Portainer

Setup Portainer Directory

mkdir -p ~/portainer
cd ~/portainer

nano templates.json
# paste this
{
  "version": "2",
  "templates": []
}

#save and exit

Create Docker Compose File

nano docker-compose.yml

Add (replace yourdomain.com):

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - portainer_data:/data
      - ./templates.json:/templates.json:ro          --- disable template fetching
    command: --templates file:///templates.json      --- disable template fetching
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.entrypoints=https"
      - "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
      - "traefik.http.routers.portainer.tls=true"
      - "traefik.http.routers.portainer.tls.certresolver=cloudflare"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"

volumes:
  portainer_data:

networks:
  traefik:
    external: true

Start Portainer

docker compose up -d

# Check logs
docker logs portainer -f

Add DNS Record

In Cloudflare DNS settings:

  • Type: A
  • Name: portainer
  • Content: your-server-ip
  • Proxy status: DNS only (grey cloud)

Wait 1-2 minutes, then visit: https://portainer.yourdomain.com

First-time setup:

  1. Create admin username and password
  2. Choose “Get Started” (local Docker environment)

Part 5: Deploy Your First App

Test Deployment

Create a test app to verify everything works:

mkdir -p ~/test-app
cd ~/test-app
nano docker-compose.yml

Add (replace yourdomain.com):

services:
  whoami:
    image: traefik/whoami
    container_name: test-app
    restart: unless-stopped
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.entrypoints=https"
      - "traefik.http.routers.whoami.rule=Host(`test.yourdomain.com`)"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.tls.certresolver=cloudflare"

networks:
  traefik:
    external: true

Deploy:

docker compose up -d

Add DNS in Cloudflare:

  • Name: test
  • Content: your-server-ip
  • Proxy: DNS only

Visit: https://test.yourdomain.com - You should see request details!


Deploying Apps from GitHub (Git Push = Auto Deploy)

1. Get GitHub Personal Access Token

  1. Go to: https://github.com/settings/tokens
  2. Generate new tokenClassic
  3. Name: Portainer Deploy
  4. Scopes: repo (full control)
  5. Generate and copy the token

2. Deploy via Portainer

  1. Go to https://portainer.yourdomain.com
  2. StacksAdd stack
  3. Name it: myapp
  4. Choose “Repository”
  5. Fill in:
    • Repository URL: https://github.com/username/repo
    • Repository reference: refs/heads/main
    • Compose path: docker-compose.yml
    • Authentication: Toggle ON
    • Username: GitHub username
    • Token: Paste token from step 1

3. Add docker-compose.yml to Your Repo

In your repo, create docker-compose.yml:

services:
  myapp:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        # Build-time variables (for frontend builds)
        - VITE_API_URL=${VITE_API_URL}
    restart: unless-stopped
    environment:
      # Runtime variables (for backend)
      - DATABASE_URL=${DATABASE_URL}
      - API_KEY=${API_KEY}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.entrypoints=https"
      - "traefik.http.routers.myapp.rule=Host(`myapp.yourdomain.com`)"
      - "traefik.http.routers.myapp.tls=true"
      - "traefik.http.routers.myapp.tls.certresolver=cloudflare"
      - "traefik.http.services.myapp.loadbalancer.server.port=8080"
    networks:
      - traefik

networks:
  traefik:
    external: true

4. Set Environment Variables in Portainer

When deploying, add your env vars in Portainer’s “Environment variables” section.

5. Setup Git Push Auto-Deploy

  1. After deploying, go to stack details in Portainer
  2. Enable “GitOps updates” → Choose “Webhook”
  3. Copy the webhook URL
  4. In GitHub: SettingsWebhooksAdd webhook
  5. Paste webhook URL, set content type to application/json
  6. Events: “Just the push event”
  7. Add webhook

Now every git push triggers auto-deployment!


Common Issues & Solutions

Traefik API Version Error

Error: client version 1.24 is too old
Solution: Downgrade Docker to 27.3.1 (see Part 1)

SSL Certificate Not Generated

Causes:

  • DNS not pointing to server (check with dig traefik.yourdomain.com)
  • Cloudflare proxy enabled (must be “DNS only” / grey cloud)
  • Firewall blocking ports 80/443

Fix:

docker logs traefik -f

Look for ACME certificate generation logs

Service Not Accessible

Checklist:

  1. DNS record exists and points to server IP
  2. Container is running: docker ps
  3. Container is on traefik network: docker inspect <container>
  4. Traefik labels are correct
  5. Port is correct in labels

Useful Commands

# View all running containers
docker ps

# View container logs
docker logs <container-name> -f

# Restart a service
cd ~/service-directory
docker compose restart

# Stop all containers
docker compose down

# Remove container and rebuild
docker compose down
docker compose up -d --build

# View Traefik routes
docker exec traefik cat /etc/traefik/traefik.yml

Switching DNS Providers

To switch from Cloudflare to another provider:

  1. Get API credentials for new provider
  2. Update traefik.yml:
certificatesResolvers:
  newprovider:
    acme:
      dnsChallenge:
        provider: namecheap # or route53, digitalocean, etc.
  1. Update environment variables in .env
  2. Restart Traefik

See full list: https://doc.traefik.io/traefik/https/acme/#providers


Migration to New Server

Backup:

# Backup volumes
docker run --rm -v /var/lib/docker/volumes:/volumes \
  -v $(pwd):/backup alpine \
  tar -czf /backup/volumes-backup.tar.gz /volumes

# Backup compose files (if not in git)
tar -czf compose-files.tar.gz ~/traefik ~/portainer ~/your-apps

Restore on New Server:

# Install Docker (Part 1)
# Install Traefik & Portainer (Parts 2-4)

# Restore volumes
tar -xzf volumes-backup.tar.gz -C /

# Restore compose files
tar -xzf compose-files.tar.gz -C ~/

# Start everything
cd ~/traefik && docker compose up -d
cd ~/portainer && docker compose up -d
# ... etc

# Update DNS to point to new server IP

Security Best Practices

  1. Always use secrets for passwords - never hardcode in compose files
  2. Use strong passwords for Traefik dashboard and Portainer
  3. Keep Docker updated (but avoid bleeding-edge versions)
  4. Regular backups of volumes and compose files
  5. Firewall: Only expose ports 80, 443, and SSH
  6. SSH key authentication instead of passwords
  7. Cloudflare proxy can be enabled for additional DDoS protection (after SSL works)

Resources


Summary

You now have:

  • ✅ Automatic SSL certificates for all services (wildcard support)
  • ✅ Easy domain management (just add DNS + labels)
  • ✅ Web UI for container management (Portainer)
  • ✅ Git push = auto deploy workflow
  • ✅ Complete control over your infrastructure
  • ✅ No service templates forced on you

Next steps: Deploy your applications using the patterns shown above!