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

Comments

Leave a Reply

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

Secret Link