Qué vas a conseguir
La web corporativa en WordPress corriendo en contenedores en un servidor Linux: WordPress y su base de datos definidos en un fichero, los datos en volúmenes, las contraseñas fuera del repositorio, HTTPS delante, una copia diaria que sabes restaurar porque lo has probado y la rutina para cuando una actualización deja la web en blanco.
Antes de empezar
- Un servidor con Docker y el plugin de Compose (guía de Docker), con espacio para los volúmenes y las copias.
- El nombre de la web en DNS apuntando al servidor y un certificado (Let's Encrypt o el de la CA corporativa).
- Un segundo servidor o máquina de pruebas donde restaurar.
1. El compose: WordPress, MariaDB y volúmenes
services: db: image: mariadb:11.4 restart: unless-stopped environment: MARIADB_DATABASE: wordpress MARIADB_USER: wordpress MARIADB_PASSWORD: ${DB_PASSWORD} MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} volumes: - db_data:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 30s retries: 5 wordpress: image: wordpress:6.9-php8.3-apache restart: unless-stopped depends_on: db: condition: service_healthy environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} WORDPRESS_CONFIG_EXTRA: | define('DISALLOW_FILE_EDIT', true); volumes: - wp_html:/var/www/html ports: - "127.0.0.1:8080:80" wpcli: image: wordpress:cli-php8.3 profiles: ["tools"] user: "33:33" environment: HOME: /tmp WORDPRESS_DB_HOST: db WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} volumes: - wp_html:/var/www/html volumes: db_data: wp_html:- Etiquetas fijas, nunca
latest: así sabes qué versión corre y una actualización es un cambio que decides tú. La de WordPress que toque cuando lo leas, con su versión de PHP. - El puerto se publica solo en
127.0.0.1: a WordPress se llega por el proxy, no directamente. wpcliestá en un profile: no arranca condocker compose up, solo cuando lo llamas. Usa el usuario 33 porque es elwww-datade la imagen Debian de WordPress; la imagen de WP-CLI usa otro por defecto y daría problemas de permisos.
2. Las contraseñas, fuera de git
Compose lee las variables ${DB_PASSWORD} de un fichero .env junto al compose.yaml. Ese fichero no se sube nunca:
cat .gitignore.envbackups/chmod 600 .envgit check-ignore -v .env.gitignore:1:.env .env- En el repositorio va un
.env.examplecon los nombres de las variables y sin valores. docker compose configmuestra el fichero con las variables resueltas: incluidas las contraseñas. No lo pegues en un ticket.- Las variables
MARIADB_*solo se aplican cuando el volumen de datos está vacío, la primera vez. Cambiar la contraseña en.envdespués no la cambia en la base de datos: hay que hacerlo conALTER USERy luego en.env.
Si una contraseña llegó a un commit, cámbiala. Borrarla del historial no deshace que alguien la haya clonado.
3. Proxy inverso con TLS
Un Nginx en el propio servidor termina HTTPS y reenvía a WordPress. Traefik o Caddy sirven igual; lo importante son las cabeceras:
server { listen 80; server_name www.contoso.example; return 301 https://$host$request_uri;}server { listen 443 ssl; server_name www.contoso.example; ssl_certificate /etc/letsencrypt/live/www.contoso.example/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.contoso.example/privkey.pem; client_max_body_size 64m; location = /xmlrpc.php { deny all; } location ~* ^/wp-content/uploads/.*\.php$ { deny all; } location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}- La imagen oficial ya añade a
wp-config.phpel código que leeX-Forwarded-Proto. Sin esa cabecera WordPress cree que está en HTTP y aparecen bucles de redirección o contenido mixto. - Detrás del proxy, Apache ve la IP del proxy. Si un plugin de seguridad bloquea por IP, necesita saber leer
X-Forwarded-For, o bloqueará al proxy entero.
4. Actualizaciones: núcleo, plugins, temas e imagen
Con el volumen montado en /var/www/html, la imagen solo copia WordPress la primera vez, cuando el volumen está vacío. A partir de ahí WordPress se gestiona a sí mismo dentro del volumen. Por eso:
- Cambiar la etiqueta de la imagen actualiza PHP, Apache y las extensiones, no el núcleo de WordPress.
- El núcleo, los plugins y los temas se actualizan desde Escritorio › Actualizaciones o con WP-CLI.
docker compose run --rm wpcli wp core check-updatedocker compose run --rm wpcli wp plugin list --update=availablename status update versiongaleria-pro active available 3.4.1formularios-xy active available 2.8.0docker compose run --rm wpcli wp plugin update formularios-xyLa rutina que evita sustos:
- Copia (apartado 5) justo antes.
- Actualizar primero en pruebas y abrir las páginas que importan.
- En producción, un plugin cada vez, comprobando la web entre uno y otro.
- Plugins y temas que no se usan, fuera: desactivados siguen siendo código que se puede atacar. Deja un tema por defecto como reserva.
- La imagen: cambiar la etiqueta en
compose.yaml,docker compose pullydocker compose up -d. Para un salto de versión mayor de MariaDB, la variableMARIADB_AUTO_UPGRADE=1hace que la imagen ejecute la actualización de tablas al arrancar.
5. Copias de la base de datos y de wp-content, y la prueba de restauración
Lo que hay que guardar: la base de datos (entradas, páginas, usuarios, ajustes) y wp-content (subidas, plugins, temas). Más compose.yaml y .env, en un sitio seguro.
docker compose exec -T db sh -c 'exec mariadb-dump --single-transaction -uroot -p"$MARIADB_ROOT_PASSWORD" wordpress' | gzip > backups/wp-db-$(date +%F).sql.gzdocker compose exec -T wordpress tar -C /var/www/html -czf - wp-content > backups/wp-content-$(date +%F).tar.gzls -lh backups/-rw-r--r-- 1 root root 18M Oct 30 02:00 wp-db-2026-10-30.sql.gz-rw-r--r-- 1 root root 2.1G Oct 30 02:01 wp-content-2026-10-30.tar.gz--single-transactionhace una copia coherente sin bloquear la web. Las imágenes recientes de MariaDB traenmariadb-dump, nomysqldump.- Las copias, fuera del servidor: si se pierde la máquina, se pierden con ella.
La prueba de restauración, una vez al mes en el servidor de pruebas con el mismo compose.yaml:
docker compose up -d db wordpressgunzip -c wp-db-2026-10-30.sql.gz | docker compose exec -T db sh -c 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" wordpress'docker compose exec -T wordpress tar -C /var/www/html -xzf - < wp-content-2026-10-30.tar.gzApunta en tu fichero hosts el nombre de la web a la IP de pruebas y comprueba: la portada carga, puedes iniciar sesión, la última entrada publicada está, las imágenes se ven y el formulario de contacto envía. Anota cuánto ha tardado: ese es tu tiempo real de recuperación.
6. Bastionado básico
- xmlrpc.php bloqueado en el proxy (apartado 3), salvo que una aplicación lo use de verdad. Es la puerta favorita de los ataques de fuerza bruta.
- Ningún usuario llamado admin, pocos administradores y con segundo factor. Quien solo publica, con el rol Editor. Revisa la lista con
wp user list --role=administrator. - Edición de ficheros desactivada con
DISALLOW_FILE_EDIT: nadie edita PHP desde el navegador. Si todas las actualizaciones las haces tú por WP-CLI,DISALLOW_FILE_MODStambién impide instalar o actualizar plugins desde el escritorio. - wp-admin restringido a la VPN o a las IPs de la oficina en el proxy, dejando público
admin-ajax.php, que usan muchos temas. - PHP en uploads bloqueado: un fichero subido nunca debe ejecutarse.
- WAF: ModSecurity con las reglas OWASP CRS en el proxy, el WAF de la CDN si la hay, o un plugin de seguridad. Empieza en modo detección, revisa los falsos positivos y luego bloquea.
7. El ticket: "la web está caída después de actualizar un plugin"
Marketing actualizó un plugin desde el escritorio. La web muestra Ha habido un error crítico en esta web.
- Qué ha fallado: el error está en el registro del contenedor.
docker compose logs --since 30m wordpress | grep -i "fatal"PHP Fatal error: Uncaught Error: Call to undefined function galeria_pro_init() in /var/www/html/wp-content/plugins/galeria-pro/galeria-pro.php:48docker compose run --rm wpcli wp plugin deactivate galeria-pro --skip-pluginsPlugin 'galeria-pro' deactivated.- Desactívalo.
--skip-pluginshace que WP-CLI no cargue los plugins, porque el roto también lo tumbaría. Sin WP-CLI vale renombrar la carpeta:docker compose exec wordpress mv wp-content/plugins/galeria-pro wp-content/plugins/galeria-pro.off. - Vuelve a la versión anterior si hace falta la funcionalidad:
wp plugin install galeria-pro --version=3.4.1 --force --skip-pluginsy actívalo. - Si la actualización cambió la base de datos, la versión anterior puede no entenderla: entonces se restaura la copia de antes de la actualización, la del apartado 5.
- Después: aviso al proveedor del plugin, el plugin fuera de las actualizaciones automáticas hasta que lo corrijan, y la regla de que las actualizaciones pasan por pruebas.
Siguientes pasos
- La guía de Docker cubre la base: volúmenes, registros y qué hacer cuando un contenedor se reinicia solo.
- La guía de PostgreSQL/MySQL/SQL Server profundiza en copias y restauraciones de bases de datos.
- Para llevar el registro de cada prueba de restauración (qué se restauró, cuándo, cuánto tardó y si funcionó), el repositorio 32-backup-restore-test-tracker de mi GitHub sigue esa misma idea.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.
Apúntate y llévate las 33 ideas →