Tag: portainer

  • How to Install WordPress on Portainer with Caddy Proxy (Automatic HTTPS)

    Running WordPress behind a modern, automatic HTTPS reverse proxy is easier than ever using Docker, Portainer, and Caddy.

    In this guide, you will deploy:

    • Caddy – reverse proxy and automatic TLS (Let’s Encrypt)
    • WordPress – application container
    • MariaDB – database container
    • Portainer – web UI to manage your Docker stack

    By the end, you will have a secure, production-ready WordPress site accessible at your domain with automatic certificate management and clean container isolation.


    Architecture Overview

    You will run three services:

    • Caddy
      • Public-facing
      • Listens on ports 80 and 443
      • Automatically provisions and renews HTTPS certificates
      • Reverse proxies traffic to WordPress
    • WordPress
      • Only reachable internally
      • Connected to Caddy via a shared network
      • Connected to MariaDB via a private network
    • MariaDB
      • Internal-only
      • Not exposed to the public internet

    Networking Model

    We use two Docker networks:

    • web → Shared by Caddy and WordPress (public-facing traffic)
    • wp → Private network between WordPress and MariaDB

    We also use persistent Docker volumes:

    • caddy_data → TLS certificates and Caddy state
    • db_data → MariaDB database files
    • wp_data → WordPress files and uploads

    This ensures container restarts or updates do not erase data.


    Prerequisites

    Before deploying anything, confirm the following:

    1. Domain & DNS

    Choose your canonical hostname:

    • example.com
    • or www.example.com

    Create DNS records:

    • A record → your server’s public IPv4
    • AAAA record (optional) → your server’s IPv6

    Confirm DNS resolves correctly:

    dig example.com
    

    Do not continue until DNS points to your server.


    2. Server Requirements

    • Linux server (Ubuntu/Debian recommended)
    • Public IP address
    • Open ports:
      • 80/tcp
      • 443/tcp
      • 9443/tcp (Portainer UI)

    Make sure no existing service (Apache/Nginx) is using ports 80 or 443.


    Step 1 – Install Docker

    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
    sudo usermod -aG docker $USER
    newgrp docker
    

    Verify installation:

    docker --version
    

    Step 2 – Install Portainer

    Create a volume:

    docker volume create portainer_data
    

    Run Portainer:

    docker run -d \
      -p 8000:8000 \
      -p 9443:9443 \
      --name portainer \
      --restart=always \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v portainer_data:/data \
      portainer/portainer-ce:latest
    

    Access Portainer:

    https://YOUR_SERVER_IP:9443
    

    Complete the admin setup and select the local Docker environment.


    Step 3 – Create Docker Networks and Volumes

    Create networks:

    docker network create web
    docker network create wp
    

    Create persistent volumes:

    docker volume create caddy_data
    docker volume create db_data
    docker volume create wp_data
    

    These must exist before deploying the stack.


    Step 4 – Create the Caddyfile

    Create directory:

    sudo mkdir -p /opt/caddy
    sudo nano /opt/caddy/Caddyfile
    

    Example configuration (redirect www → apex):

    {
      email you@example.com
    }
    
    www.example.com {
      redir https://example.com{uri} permanent
    }
    
    example.com {
      encode zstd gzip
    
      header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
      }
    
      reverse_proxy wordpress:80 {
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Host {host}
      }
    }
    

    Replace:

    • example.com
    • you@example.com

    Step 5 – Deploy WordPress Stack in Portainer

    Go to:

    Portainer → Stacks → Add Stack

    Name it:

    wordpress-with-caddy
    

    Paste this Docker Compose file:

    version: "3.8"
    
    services:
      caddy:
        image: caddy:2
        container_name: caddy
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - caddy_data:/data
          - /opt/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
        networks:
          - web
    
      db:
        image: mariadb:11
        container_name: wp-db
        restart: unless-stopped
        environment:
          MARIADB_DATABASE: wordpress
          MARIADB_USER: wp
          MARIADB_PASSWORD: CHANGE_ME_STRONG
          MARIADB_ROOT_PASSWORD: CHANGE_ME_VERY_STRONG
        volumes:
          - db_data:/var/lib/mysql
        networks:
          - wp
    
      wordpress:
        image: wordpress:6-php8.2-apache
        container_name: wordpress
        restart: unless-stopped
        depends_on:
          - db
        environment:
          WORDPRESS_DB_HOST: db:3306
          WORDPRESS_DB_USER: wp
          WORDPRESS_DB_PASSWORD: CHANGE_ME_STRONG
          WORDPRESS_DB_NAME: wordpress
        volumes:
          - wp_data:/var/www/html
        networks:
          - web
          - wp
    
    volumes:
      caddy_data:
        external: true
      db_data:
        external: true
      wp_data:
        external: true
    
    networks:
      web:
        external: true
      wp:
        external: true
    

    Replace all passwords before deploying.

    Click Deploy the Stack.


    Step 6 – Verify Certificate Issuance

    Go to:

    Containers → caddy → Logs

    You should see successful ACME certificate issuance.

    If you see errors:

    • Confirm DNS is correct
    • Confirm ports 80 and 443 are open
    • Restart only the Caddy container after fixing issues

    Step 7 – Fix WordPress Behind Reverse Proxy (Important)

    Because Caddy terminates TLS, WordPress must be told it is running behind HTTPS.

    Edit wp-config.php (inside the wp_data volume).

    Add this above:

    /* That's all, stop editing! Happy publishing. */
    
    if (
      (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
      || (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on')
    ) {
      $_SERVER['HTTPS'] = 'on';
      $_SERVER['SERVER_PORT'] = 443;
    }
    
    define('FORCE_SSL_ADMIN', true);
    

    This prevents:

    • Mixed content errors
    • Login redirect loops
    • Secure cookie problems

    Step 8 – Complete WordPress Setup

    Visit:

    https://example.com
    

    Complete the installer:

    • Choose language
    • Set site title
    • Create strong admin credentials

    After installation:

    • Go to Settings → General
    • Confirm both URLs use HTTPS
    • Go to Settings → Permalinks → Save

    Upload a test image and confirm it loads over HTTPS.


    Backups

    Back up both:

    Database

    docker exec wp-db sh -c 'exec mysqldump -u wp -p"$MARIADB_PASSWORD" wordpress' > wp-$(date +%F).sql
    

    WordPress files

    Archive the wp_data volume regularly.


    Hardening Recommendations

    • Use strong, unique DB passwords
    • Keep Docker images updated
    • Limit exposed ports (only 80/443 publicly)
    • Enable 2FA in WordPress
    • Remove unused plugins/themes
    • Consider disabling XML-RPC if unused

    Troubleshooting

    Caddy cannot issue certificates

    • DNS incorrect
    • Ports 80/443 blocked
    • Another service using 80/443

    502 / 504 Errors

    • WordPress container not running
    • Network misconfiguration
    • DB credentials incorrect

    Mixed Content Warnings

    • Hardcoded http links in theme or database
    • Missing proxy HTTPS snippet

    Final Result

    You now have:

    • Automatic HTTPS via Caddy
    • Isolated database
    • Persistent volumes
    • Easy stack management via Portainer
    • Clean, production-ready WordPress deployment

    This setup is simple, secure, and scalable. You can extend it with:

    • Redis caching
    • Staging stacks
    • Automated backups
    • CI/CD deployments

Secret Link