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 statedb_data→ MariaDB database fileswp_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:
Arecord → your server’s public IPv4AAAArecord (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.comyou@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
Leave a Reply