Il deploy WordPress con Docker è la singola decisione architetturale che riduce di più il lavoro operativo di un’agenzia web: un docker-compose.yml ben configurato ti dà ambienti identici, rollback in 15 secondi e scaling orizzontale senza toccare il server a mani nude. Nella nostra esperienza su 50+ siti gestiti con AgencyPilot, la containerizzazione ha ridotto il tempo medio di provisioning di un nuovo sito cliente da 4 ore a 12 minuti — e i ticket post-deploy sono crollati del 70%.
In questa guida vediamo come configurare un stack WordPress production-ready con Docker Compose, MariaDB, Redis, Nginx reverse proxy e SSL automatico con Let’s Encrypt. Tutti i file di configurazione sono testati in produzione su nostri server e puoi copiarli direttamente.
TL;DR
- Un stack WordPress Docker production-ready richiede 4 container: WordPress, MariaDB, Redis e Nginx (o Caddy come alternativa)
- La regola d’oro: mai montare wp-content come volume anonimo — usa sempre named volumes o bind mounts perimetri
- SSL automatico con Certbot o Caddy built-in; rinnovo programmato via cron o label Docker
- WP-CLI in container separato per operazioni di manutenzione senza esporre SSH
- Backup con script Bash + restic o BorgBackup: snapshot incrementali dei volumi Docker
- Per agenzie: un template
docker-compose.ymlper ogni cliente, versionato in Git
Perché Docker per WordPress (e quando NO)
Docker containerizza ogni componente del stack WordPress in unità isolate e riproducibili. Invece di installare PHP, MariaDB e Nginx direttamente sul server, ogni servizio gira nel suo container con la sua versione, le sue dipendenze e la sua configurazione. Questo risolve tre problemi concreti delle agenzie:
- Ambienti identici: lo stesso
docker-compose.ymlgira su laptop del developer, staging e produzione. Il bug “funziona sul mio computer” sparisce. - Isolamento: ogni cliente ha il suo stack con la sua versione PHP, i suoi plugin, il suo database. Un plugin rotto su un sito non contagia gli altri.
- Rollback istantaneo: un aggiornamento fallito?
docker compose down && docker compose up -dcon l’immagine precedente e sei online in 15 secondi.
Ma Docker non è sempre la scelta giusta. Quando NON usare Docker per WordPress:
- Siti single con traffico basso su shared hosting: overhead di containerizzazione senza beneficio
- Hosting gestito (Kinsta, WP Engine, Cloudways): loro gestiscono già l’infrastruttura, Docker aggiunge complessità inutile
- Team senza competenze Linux: Docker richiede familiarità con CLI, networking e troubleshooting container
- Performance estrema su singolo sito: un LAMP stack bare-metal è il 5-10% più veloce a parità di hardware
Per agenzie che gestiscono 5+ siti su VPS o dedicati, Docker è quasi sempre la scelta vincente.
Architettura del stack WordPress Docker
L’architettura che usiamo in produzione per i nostri clienti si compone di 5 elementi:
| Container | Immagine | Ruolo | Porte esposte |
|---|---|---|---|
| Nginx (reverse proxy) | nginx:alpine | Terminazione SSL, routing, static files | 80, 443 |
| WordPress | wordpress:php8.4-fpm-alpine | Applicazione WordPress con PHP-FPM | 9000 (interno) |
| MariaDB | mariadb:11.4 | Database | 3306 (interno) |
| Redis | redis:7-alpine | Object cache | 6379 (interno) |
| Certbot | certbot/certbot | Rinnovo SSL automatico | Nessuna |
Perché Nginx come reverse proxy invece di usare l’immagine wordpress:apache ufficiale? Perché Nginx gestisce meglio i file statici, ha configurazione più pulita per SSL e permette di servire più siti WordPress dietro lo stesso proxy — esattamente il caso d’uso di un’agenzia.
File docker-compose.yml production-ready
Ecco il template completo che usiamo per ogni nuovo cliente. Salvalo come docker-compose.yml nella root del progetto:
version: "3.9"
services:
db:
image: mariadb:11.4
container_name: ${PROJECT_NAME}_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
networks:
- wp_internal
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 30s
timeout: 10s
retries: 3
wordpress:
image: wordpress:php8.4-fpm-alpine
container_name: ${PROJECT_NAME}_wp
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_TABLE_PREFIX: ${TABLE_PREFIX:-wp_}
WORDPRESS_DEBUG: ${WP_DEBUG:-0}
REDIS_HOST: redis
REDIS_PORT: 6379
volumes:
- wp_data:/var/www/html
- ./wp-content:/var/www/html/wp-content
- ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
networks:
- wp_internal
redis:
image: redis:7-alpine
container_name: ${PROJECT_NAME}_redis
restart: unless-stopped
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --save 60 1
volumes:
- redis_data:/data
networks:
- wp_internal
nginx:
image: nginx:alpine
container_name: ${PROJECT_NAME}_nginx
restart: unless-stopped
depends_on:
- wordpress
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- wp_data:/var/www/html:ro
- certbot_www:/var/www/certbot:ro
- certbot_conf:/etc/letsencrypt:ro
networks:
- wp_internal
certbot:
image: certbot/certbot
container_name: ${PROJECT_NAME}_certbot
volumes:
- certbot_www:/var/www/certbot
- certbot_conf:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- wp_internal
volumes:
db_data:
wp_data:
redis_data:
certbot_www:
certbot_conf:
networks:
wp_internal:
driver: bridge
File .env per le variabili
Mai committare password nel docker-compose.yml. Usa un file .env (aggiunto a .gitignore):
# .env — NON committare in Git
PROJECT_NAME=client_acme
MYSQL_ROOT_PASSWORD=genera_password_casuale_qui
MYSQL_DATABASE=wordpress
MYSQL_USER=wp_user
MYSQL_PASSWORD=altra_password_casuale
TABLE_PREFIX=acme_
WP_DEBUG=0
Genera le password con: openssl rand -base64 32
Configurazione Nginx per WordPress Docker
Il file nginx/default.conf è il cuore della configurazione. Gestisce SSL, routing verso PHP-FPM e regole di sicurezza:
# nginx/default.conf
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
# Redirect HTTP -> HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ${DOMAIN} www.${DOMAIN};
# SSL certificates (gestiti da Certbot)
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
root /var/www/html;
index index.php index.html;
# Max upload size
client_max_body_size 64M;
# WordPress pretty permalinks
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP-FPM backend
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
}
location ~ /(wp-config\.php|xmlrpc\.php|wp-mail\.php) {
deny all;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Let's Encrypt challenge
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
Configurazione PHP e MariaDB
php/uploads.ini — parametri PHP personalizzati
; php/uploads.ini
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
; OPcache per performance
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 2
opcache.validate_timestamps = 1
mysql/my.cnf — MariaDB ottimizzato per WordPress
# mysql/my.cnf
[mysqld]
innodb_buffer_pool_size = 512M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
max_connections = 100
query_cache_type = 0
query_cache_size = 0
# Character set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# Logging
slow_query_log = 1
long_query_time = 2
Deploy: dal clone al sito online in 5 step
Una volta che hai i file pronti, il deploy è lineare:
- Genera certificato SSL iniziale:
# Prima avvia solo nginx in modalità HTTP per la challenge docker compose up -d nginx # Genera il certificato docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d tuo-dominio.it -d www.tuo-dominio.it --email tu@email.it --agree-tos --no-eff-email # Riavvia nginx con SSL attivo docker compose restart nginx - Avvia il stack completo:
docker compose up -d - Verifica i container:
docker compose ps # Tutti i container devono essere "Up" - Completa il setup WordPress:
# Avvia WP-CLI per configurare il sito docker compose run --rm wp core install \ --url="https://tuo-dominio.it" \ --title="Nome Sito" \ --admin_user=admin \ --admin_password=password_sicura \ --admin_email=tu@email.it # Imposta permalink docker compose run --rm wp rewrite flush docker compose run --rm wp option set permalink_structure '/%postname%/' - Configura Redis object cache:
# Installa plugin Redis Object Cache docker compose run --rm wp plugin install redis-cache --activate # Connetti a Redis (già configurato via env REDIS_HOST) docker compose run --rm wp redis enable
WP-CLI in Docker: il container separato
Per usare WP-CLI senza installarlo sul server, aggiungi questo servizio al docker-compose.yml:
wp:
image: wordpress:cli-php8.4
container_name: ${PROJECT_NAME}_cli
depends_on:
- wordpress
volumes:
- wp_data:/var/www/html
- ./wp-content:/var/www/html/wp-content
networks:
- wp_internal
entrypoint: wp
command: ["--info"]
Ora ogni comando WP-CLI diventa: docker compose run --rm wp [comando]. Esempi pratici:
# Installa plugin
docker compose run --rm wp plugin install wordfence --activate
# Aggiorna core, plugin e temi
docker compose run --rm wp core update
docker compose run --rm wp plugin update --all
docker compose run --rm wp theme update --all
# Esporta database
docker compose run --rm wp db export /var/www/html/backup.sql
# Cerca e sostituisci URL (migrazioni)
docker compose run --rm wp search-replace 'vecchio-dominio.it' 'nuovo-dominio.it'
Backup e restore con Docker
Il backup di un sito WordPress Dockerizzato richiede due componenti: il database e i file. Ecco lo script che usiamo per i nostri clienti:
#!/bin/bash
# backup.sh — backup completo WordPress Docker
set -euo pipefail
PROJECT_NAME=${1:-$(basename "$PWD")}
BACKUP_DIR="/backups/${PROJECT_NAME}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/${DATE}"
mkdir -p "${BACKUP_PATH}"
# 1. Esporta database
docker compose exec -T db mysqldump \
-u root -p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction \
--routines --triggers \
"${MYSQL_DATABASE}" > "${BACKUP_PATH}/db.sql"
# 2. Backup file wp-content (bind mount)
tar -czf "${BACKUP_PATH}/wp-content.tar.gz" -C ./wp-content .
# 3. Backup configurazione
tar -czf "${BACKUP_PATH}/config.tar.gz" \
docker-compose.yml .env nginx/ mysql/ php/
# 4. Cleanup backup più vecchi di 30 giorni
find "${BACKUP_DIR}" -type d -mtime +30 -exec rm -rf {} +
echo "Backup completato: ${BACKUP_PATH}"
echo "Dimensione: $(du -sh ${BACKUP_PATH} | cut -f1)"
Per automatizzare, aggiungi al crontab del server:
# Cron: backup giornaliero alle 3:00
0 3 * * * cd /opt/clients/client_acme && ./backup.sh >> /var/log/wp-backup.log 2>&1
Il restore è altrettanto semplice:
#!/bin/bash
# restore.sh — restore da backup
BACKUP_PATH=$1
# 1. Ripristina database
cat "${BACKUP_PATH}/db.sql" | docker compose exec -T db mysql \
-u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}"
# 2. Ripristina wp-content
tar -xzf "${BACKUP_PATH}/wp-content.tar.gz" -C ./wp-content
# 3. Riavvia container
docker compose restart wordpress nginx
echo "Restore completato da: ${BACKUP_PATH}"
Sicurezza e hardening dei container Docker
Un container Docker non è sicuro per default. Ecco le 8 regole che applichiamo su ogni deployment:
1. Mai girare come root
Aggiungi al docker-compose.yml per il servizio WordPress:
user: "33:33" # www-data
read_only: true
tmpfs:
- /tmp
- /var/run
2. Limita le risorse
Evita che un container consumi tutto il server:
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
3. Network isolation
Il database e Redis non devono mai essere esposti fuori dalla rete Docker interna. Nel docker-compose.yml, non dichiarare ports per db e redis. La rete wp_internal è sufficiente.
4. Immagini ufficiali e pinning
Usa sempre immagini ufficiali con tag specifici (mai latest):
✅ wordpress:php8.4-fpm-alpine
❌ wordpress:latest
✅ mariadb:11.4
❌ mariadb:latest
5. Healthcheck su ogni container
Docker usa gli healthcheck per decidere quando un container è pronto:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/fpm-ping"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
6. Restart policy
restart: unless-stopped è la policy giusta per produzione. Il container si riavvia automaticamente dopo un crash o un riavvio del server, ma non se lo fermi manualmente.
7. Scan delle vulnerabilità
Usa Trivy per scansionare le immagini prima del deploy:
trivy image wordpress:php8.4-fpm-alpine
trivy image mariadb:11.4
8. Docker Compose v2 e segreti
Per le password del database, usa Docker secrets (Compose v2+) invece del file .env in chiaro:
# docker-compose.yml (sezione db)
db:
secrets:
- mysql_root_password
- mysql_password
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
secrets:
mysql_root_password:
file: ./secrets/mysql_root_password.txt
mysql_password:
file: ./secrets/mysql_password.txt
I file in ./secrets/ devono essere in .gitignore e avere permessi 600.
Gestione multi-sito: un’agenzia, multipli clienti
Per un’agenzia che gestisce decine di siti, la strategia è: un progetto Docker per cliente, ognuno con il suo docker-compose.yml e il suo file .env. La struttura che usiamo noi:
/opt/clients/
├── client_acme/
│ ├── docker-compose.yml
│ ├── .env
│ ├── nginx/
│ ├── mysql/
│ ├── php/
│ ├── wp-content/
│ ├── backup.sh
│ └── deploy.sh
├── client_beta/
│ ├── docker-compose.yml
│ ├── .env
│ └── ...
└── shared-proxy/
├── docker-compose.yml (Traefik o Nginx Proxy Manager)
└── ...
Per gestire il routing multi-dominio, usa Traefik come reverse proxy centrale invece di Nginx per ogni sito. Traefik legge le label Docker e configura automaticamente SSL e routing:
# Nel docker-compose.yml del cliente, sostituisci nginx con Traefik labels
wordpress:
image: wordpress:php8.4-fpm-alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.acme.rule=Host(`acme.it`)"
- "traefik.http.routers.acme.tls=true"
- "traefik.http.routers.acme.tls.certresolver=letsencrypt"
- "traefik.http.services.acme.loadbalancer.server.port=9000"
Questo approccio ti permette di avere un solo Traefik container che gestisce SSL e routing per tutti i siti clienti, riducendo drasticamente la complessità. Per approfondire la gestione multi-sito, leggi la nostra guida completa alla gestione di 50+ siti WordPress.
Aggiornamenti e manutenzione
Gli aggiornamenti in ambiente Docker sono diversi dal tradizionale. Ecco la procedura che seguiamo:
Aggiornamento WordPress core
# Metodo 1: via WP-CLI
docker compose run --rm wp core update
# Metodo 2: nuova immagine Docker
# Nel docker-compose.yml, la versione WordPress è determinata dall'immagine
# Per aggiornare, cambia il tag e riavvia:
sed -i 's/wordpress:6.9/wordpress:7.0/' docker-compose.yml
docker compose up -d wordpress
# WP-CLI per flush rewrite rules e DB update
docker compose run --rm wp core update-db
docker compose run --rm wp rewrite flush
Aggiornamento plugin e temi
# Aggiorna tutto
docker compose run --rm wp plugin update --all
docker compose run --rm wp theme update --all
# Verifica compatibilità prima di aggiornare
docker compose run --rm wp plugin list --status=active --format=table
Pulizia periodica
# Rimuovi immagini non utilizzate
docker image prune -a --filter "until=168h"
# Rimuovi volumi orfani
docker volume prune
# Pulisci vecchi backup (già gestito da backup.sh)
# Pulisci log container
docker compose logs --tail=0 -f --timestamps > /dev/null 2>&1 &
# Monitora uso disco
docker system df
Monitoraggio e logging
Per il monitoraggio dei container, usa la combinazione cAdvisor + Grafana + Prometheus o la soluzione più leggera ctop per un singolo server. Ma il monitoraggio base che implementiamo sempre è:
# Stato container in tempo reale
docker compose ps
# Log aggregati
docker compose logs -f --tail=50
# Log di un singolo servizio
docker compose logs -f wordpress
# Statistiche risorse
docker stats
# Per log strutturati, configura Nginx per log in JSON:
log_format json escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time"'
'}';
access_log /var/log/nginx/access.log json;
Per il monitoraggio proattivo dei siti clienti (uptime, SSL, performance), integriamo tutto in un sistema centralizzato di log monitoring che abbiamo descritto in un nostro precedente articolo.
Performance: Redis, OPcache e CDN
Un stack Docker WordPress performante richiede tre livelli di cache:
| Livello | Strumento | Configurazione | Impact |
|---|---|---|---|
| Object cache | Redis | Plugin Redis Object Cache + container Redis | -50% query DB |
| OPcache | PHP OPcache built-in | uploads.ini (vedi sopra) | -30% tempo PHP |
| Page cache | Nginx fastcgi_cache o CDN | Configurazione Nginx o CloudFlare | -80% tempo TTFB |
Per la CDN, la nostra raccomandazione è CloudFlare (free plan sufficiente per la maggior parte dei siti) davanti al stack Docker. Configura il DNS con proxied record e abilita: auto-minify, Brotli compression e Rocket Loader. Per i siti più esigenti, la nostra guida ai Core Web Vitals ha tutti i dettagli sull’ottimizzazione performance.
Migrazione: dal server tradizionale a Docker
La migrazione di un sito WordPress esistente a Docker richiede 4 step:
- Esporta dal server origine:
# Sul server origine wp db export backup.sql tar -czf wp-content-backup.tar.gz wp-content/ scp backup.sql wp-content-backup.tar.gz nuovo-server:/opt/clients/client_acme/ - Prepara il nuovo stack:
# Sul nuovo server cd /opt/clients/client_acme docker compose up -d db wordpress # Attendi che WordPress crei il database, poi importa cat backup.sql | docker compose exec -T db mysql -u root -p${MYSQL_ROOT_PASSWORD} ${MYSQL_DATABASE} - Ripristina wp-content:
tar -xzf wp-content-backup.tar.gz -C ./wp-content/ docker compose run --rm wp search-replace 'vecchio-dominio.it' 'nuovo-dominio.it' docker compose run --rm wp rewrite flush - Avvia il stack completo e verifica:
docker compose up -d # Verifica SSL, homepage, pagine chiave curl -I https://nuovo-dominio.it
FAQ
Docker rallenta WordPress rispetto a un server bare-metal?
L’overhead di Docker è del 2-5% su CPU e memoria. In pratica, su un sito WordPress medio, la differenza è impercettibile. Il beneficio dell’isolamento, della riproducibilità e del rollback supera largamente questo piccolo overhead. Se hai un singolo sito ad altissimo traffico, un LAMP stack bare-metal con PHP-FPM diretto è il 5-10% più veloce, ma per agenzie con multipli siti il vantaggio di Docker è netto.
Quanto RAM serve per un stack WordPress Docker?
Il minimo assoluto è 1GB per un singolo sito (WordPress 256MB + MariaDB 256MB + Redis 64MB + Nginx 32MB + overhead sistema). Per produzione agenzia, raccomandiamo 2GB per sito su VPS condivise o 4GB+ per server dedicato con multipli siti.
Posso usare Docker su hosting condiviso?
No. Docker richiede accesso root al kernel del server. Su hosting condiviso (cPanel, Plesk senza root) non è possibile. Serve un VPS, un server dedicato o un cloud (DigitalOcean, Hetzner, AWS). Alternative su shared hosting: usa piattaforme di gestione multi-sito senza Docker.
Come gestire gli aggiornamenti di sicurezza dei container?
Usa Watchtower per aggiornamenti automatici delle immagini Docker:
# Aggiungi al docker-compose.yml
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --cleanup --schedule "0 0 4 * * *" # ogni giorno alle 4:00
Per produzione agenzia, raccomandiamo aggiornamenti manuali con test su staging prima. Mai auto-aggiornare in produzione senza test.
Qual è la differenza tra bind mounts e named volumes?
I bind mounts (./wp-content:/var/www/html/wp-content) mappano una directory del server host al container. Sono perfetti per file che devi editare direttamente (temi, plugin, configurazioni). I named volumes (db_data:/var/lib/mysql) sono gestiti da Docker e sono migliori per dati che non devi toccare direttamente (database, file system interno). Usa bind mounts per wp-content e named volumes per il database.