---
title: "Cómo instalar WordPress con Docker Compose: guía del stack completo"
description: "Instala WordPress con Docker Compose. Stack completo con MySQL 8.4, phpMyAdmin, SSL de Cloudflare, copias de seguridad automáticas y caché con Redis. Guía de configuración lista para producción."
date: 2024-02-21
categories: ["wordpress"]
tags: ["self-hosted","docker","docker-compose"]
---

import { Picture } from "astro:assets";
import imag1 from "@assets/images/24/01/cloudflare-tunel-setup.png";
import imag2 from "@assets/images/24/02/docker-wp-access.png";
import YouTubeEmbed from "@components/widgets/YouTubeEmbed.astro";
import Button from "@components/widgets/Button.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";

Esta guía te lleva paso a paso por la instalación de WordPress con Docker Compose, cubriendo un stack completo listo para producción: MySQL 8.4 LTS, phpMyAdmin, copias de seguridad automáticas de la base de datos, caché de objetos Redis opcional y Cloudflare Tunnels para SSL. Todo funciona en cinco contenedores gestionados por un único archivo `compose.yaml` en cualquier VPS Linux o [mini PC como servidor doméstico](/es/mejor-mini-pc-servidor-casa/).

Necesitarás un VPS Linux. Recomiendo [Hetzner](https://go.bitdoze.com/hetzner) por su relación precio-rendimiento, la mejor en Europa, o [Hostinger](https://go.bitdoze.com/hostinger-vps) si prefieres almacenamiento NVMe a precio económico. Si usas WooCommerce, también puedes desplegar el [dashboard de administración de productos Woo](/es/dashboard-admin-woocommerce/) junto a WordPress en Docker para una gestión de productos más rápida.

_Los precios de los VPS subieron en todos los proveedores en 2026 — si estás reconsiderando un servidor alquilado, mira [qué cambió y cuándo gana un mini PC](/es/subidas-precios-vps/)._

Esto es lo que obtienes al seguir esta guía:

<ListCheck>
<ul>
<li>Contenedor de WordPress (versión de PHP fijada, volumen persistente)</li>
<li>Base de datos MySQL 8.4 LTS (con soporte hasta 2032)</li>
<li>phpMyAdmin para la gestión de la base de datos</li>
<li>Copias de seguridad automáticas de la base de datos con rotación</li>
<li>Caché de objetos Redis para consultas más rápidas a la base de datos</li>
<li>SSL mediante Cloudflare Tunnels (plan gratuito)</li>
</ul>
</ListCheck>

> Usaré `latest` en algunas etiquetas de imagen más abajo, pero puedes fijar versiones exactas si prefieres despliegues predecibles. La única excepción es MySQL. Más sobre esto en el paso 3.

## Stack de WordPress con Docker Compose: configuración paso a paso

<YouTubeEmbed
  url="https://www.youtube.com/embed/m3FNd_7MSGQ"
  label="Instalar WordPress en un contenedor Docker con Docker Compose"
/>

### 1. Requisitos previos para tu stack de WordPress en Docker

<ListCheck>
<ul>
<li>Un VPS Linux con 2GB+ de RAM (4GB recomendados para WordPress + Redis + phpMyAdmin)</li>
<li>Docker Engine 24+ y Docker Compose V2 (el plugin <code>docker compose</code>, no el <code>docker-compose</code> independiente)</li>
<li>Un nombre de dominio apuntando a tu servidor (o usa acceso directo por IP)</li>
<li>Una cuenta de Cloudflare (plan gratuito) para Tunnels y SSL</li>
<li>Puertos 5010 y 5011 disponibles (o elige los tuyos)</li>
</ul>
</ListCheck>

<Notice type="info" title="Sintaxis de Docker Compose V2">
Esta guía usa la sintaxis de Docker Compose V2. El `docker-compose` independiente (V1) está obsoleto y ya no recibe actualizaciones. Todos los ejemplos usan el nombre `compose.yaml`. Si todavía estás en V1, actualiza con `apt install docker-compose-plugin` o consulta la documentación de tu distribución.
</Notice>

Dockge es opcional pero recomendado. Te da una interfaz web para gestionar tus stacks de compose sin tener que entrar por SSH al servidor. Consulta [Instalar Dockge - Gestor de Docker Compose para Self-Hosting](/es/instalar-dockge/) para la configuración completa. Si prefieres un servidor doméstico en lugar de un VPS, echa un vistazo a los [mejores mini PC para servidor doméstico](/es/mejor-mini-pc-servidor-casa/). Un [GMKtec M5 Ultra](/go/gmktec-m5-ultra/) funciona bien para este tipo de stack.

Familiarízate con estos [comandos esenciales de Docker](/es/comandos-docker/) antes de continuar. Te ayudarán a resolver problemas.

### 2. Crea el directorio del proyecto y los archivos de configuración

Los bind-mounts de Docker se comportan mal cuando el archivo de origen no existe. Docker crea un directorio en lugar de un archivo, y tu configuración de PHP no se cargará. Crea primero los archivos de configuración:

```sh
# Navega hasta donde vivirá el stack
cd /opt/stacks/wordpress

# Crea el directorio de configuración y los archivos vacíos
mkdir -p config
touch ./config/wp_php.ini
touch ./config/pma_php.ini
touch ./config/pma_config.php

# Ajusta el propietario para que los contenedores puedan escribir en los volúmenes
# WordPress se ejecuta como UID 1000 dentro del contenedor
chown -R 1000:1000 ./config
```

<Notice type="warning" title="Los permisos de archivo importan">
Si te saltas la creación de estos archivos primero, Docker los creará como directorios y tu configuración de PHP no se cargará. Después del primer `docker compose up`, revisa también el propietario de los directorios de datos: `chown -R 1000:1000 ./wp-app ./db_data ./backups` para evitar errores de permiso denegado dentro de WordPress.
</Notice>

Verifica que los archivos se crearon correctamente:

```sh
ls -la ./config/
```

Deberías ver archivos normales (`-rw-r--r--`), no directorios (`drwxr-xr-x`).

### 3. Archivo Docker Compose: WordPress, MySQL 8.4 LTS y phpMyAdmin

Aquí está el `compose.yaml` completo con los cinco servicios. Explico las decisiones clave después del código.

<Tabs>
<Tab name="Stack base (4 servicios)">

```yaml
services:
  wp:
    image: wordpress:php8.3
    restart: unless-stopped
    ports:
      - 5010:80
    volumes:
      - ./config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
      - ./wp-app:/var/www/html
    environment:
      WORDPRESS_DB_HOST: wp-db:3306
      WORDPRESS_DB_NAME: "${DB_NAME}"
      WORDPRESS_DB_USER: "${DB_USER}"
      WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
      WORDPRESS_TABLE_PREFIX: "wp_"
      WORDPRESS_CONFIG_EXTRA: |
        define('FS_METHOD', 'direct');
    depends_on:
      wp-db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

  wp-db:
    image: mysql:8.4
    volumes:
      - ./db_data:/var/lib/mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
      MYSQL_DATABASE: "${DB_NAME}"
      MYSQL_USER: "${DB_USER}"
      MYSQL_PASSWORD: "${DB_PASSWORD}"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M

  pma:
    image: phpmyadmin:latest
    ports:
      - 5011:80
    volumes:
      - ./config/pma_php.ini:/usr/local/etc/php/conf.d/conf.ini
      - ./config/pma_config.php:/etc/phpmyadmin/config.user.inc.php
    restart: unless-stopped
    environment:
      PMA_HOST: wp-db
      PMA_PORT: 3306
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
      UPLOAD_LIMIT: 100M
    depends_on:
      - wp-db

  wp-db-backup:
    image: tiredofit/db-backup:4.1
    volumes:
      - ./backups:/backup
    restart: unless-stopped
    environment:
      DB_TYPE: mysql
      DB_HOST: wp-db
      DB_NAME: "${DB_NAME}"
      DB_USER: "${DB_USER}"
      DB_PASS: "${DB_PASSWORD}"
      DB_BACKUP_INTERVAL: 720
      DB_CLEANUP_TIME: 72000
      CHECKSUM: SHA1
      COMPRESSION: ZSTD
      CONTAINER_ENABLE_MONITORING: "false"
    depends_on:
      - wp-db
```

</Tab>
<Tab name="Stack completo con Redis (5 servicios)">

```yaml
services:
  wp:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - 5010:80
    volumes:
      - ./config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
      - ./wp-app:/var/www/html
    environment:
      WORDPRESS_DB_HOST: wp-db:3306
      WORDPRESS_DB_NAME: "${DB_NAME}"
      WORDPRESS_DB_USER: "${DB_USER}"
      WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
      WORDPRESS_TABLE_PREFIX: "wp_"
      WORDPRESS_CONFIG_EXTRA: |
        define('FS_METHOD', 'direct');
        define('WP_REDIS_HOST', 'redis-wp');
        define('WP_REDIS_PORT', 6379);
        define('WP_REDIS_DATABASE', 0);
    depends_on:
      wp-db:
        condition: service_healthy
      redis-wp:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

  wp-db:
    image: mysql:8.4
    volumes:
      - ./db_data:/var/lib/mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
      MYSQL_DATABASE: "${DB_NAME}"
      MYSQL_USER: "${DB_USER}"
      MYSQL_PASSWORD: "${DB_PASSWORD}"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M

  pma:
    image: phpmyadmin:latest
    ports:
      - 5011:80
    volumes:
      - ./config/pma_php.ini:/usr/local/etc/php/conf.d/conf.ini
      - ./config/pma_config.php:/etc/phpmyadmin/config.user.inc.php
    restart: unless-stopped
    environment:
      PMA_HOST: wp-db
      PMA_PORT: 3306
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
      UPLOAD_LIMIT: 100M
    depends_on:
      - wp-db

  wp-db-backup:
    image: tiredofit/db-backup:4.1
    volumes:
      - ./backups:/backup
    restart: unless-stopped
    environment:
      DB_TYPE: mysql
      DB_HOST: wp-db
      DB_NAME: "${DB_NAME}"
      DB_USER: "${DB_USER}"
      DB_PASS: "${DB_PASSWORD}"
      DB_BACKUP_INTERVAL: 720
      DB_CLEANUP_TIME: 72000
      CHECKSUM: SHA1
      COMPRESSION: ZSTD
      CONTAINER_ENABLE_MONITORING: "false"
    depends_on:
      - wp-db

  redis-wp:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - ./redis_data:/data
    deploy:
      resources:
        limits:
          memory: 256M
        reservations:
          memory: 128M
```

</Tab>
</Tabs>

<Notice type="error" title="NO uses mysql:latest">
`mysql:latest` ahora sigue las versiones Innovation de MySQL 9.x, versiones de vida corta con solo ~3 meses de soporte por versión menor. Para WordPress en producción, usa `mysql:8.4` (LTS, con soporte hasta abril de 2032). La etiqueta `mysql:lts` también funciona como puntero móvil a la LTS.
</Notice>

<Notice type="info" title="MySQL Innovation vs LTS">
Desde julio de 2024, MySQL usa una doble vía de publicación. Las versiones **Innovation** (9.x) traen novedades cada trimestre, pero tienen una ventana de soporte corta. Bien para pruebas, mal para producción. Las versiones **LTS** (8.4, la futura 9.7) tienen 5 años de soporte premier más 3 años de soporte extendido. Usa siempre LTS para cualquier cosa que almacene datos que te importen.
</Notice>

Decisiones clave en este archivo compose:

- **`wordpress:php8.3`** en lugar de `wordpress:latest`: fija PHP en 8.3, que es la versión recomendada por WordPress. La etiqueta `latest` también incluye PHP 8.3 a mediados de 2025, pero fijar la etiqueta evita sorpresas cuando cambie el valor por defecto.
- **`mysql:8.4`**: la versión LTS actual, con soporte hasta 2032.
- **`redis:7-alpine`**: versión fijada, basada en Alpine para una imagen más pequeña (~30MB frente a ~130MB).
- **`tiredofit/db-backup:4.1`**: versión mayor fijada. Esta imagen está migrando a `nfrastack/container-db-backup`. La antigua sigue funcionando bien, pero estate atento a la nueva versión.
- **`COMPRESSION: ZSTD`**: el nuevo valor por defecto en db-backup, con compresión y descompresión más rápidas que GZ.
- **`FS_METHOD: direct`**: indica a WordPress que escriba archivos directamente en lugar de usar FTP, que no funciona en Docker.
- **Health checks**: MySQL tiene una comprobación `mysqladmin ping` y WordPress una comprobación con `curl`. El `depends_on: condition: service_healthy` hace que WordPress espere a que MySQL esté realmente listo, no solo iniciado.
- **Límites de recursos**: evitan que cada contenedor se coma toda tu RAM. Ajústalos según el tamaño de tu VPS.

Para producción, considera los [secretos de Docker Compose](/es/secretos-docker-compose/) en lugar de archivos `.env`. Son más seguros y no dejan credenciales en el historial del shell ni en los listados de procesos.

### 4. Configura el archivo .env y las claves de seguridad

Crea un archivo `.env` en el mismo directorio que tu `compose.yaml`:

```sh
DB_NAME='wordpress'
DB_USER='wp'
DB_PASSWORD='use-a-strong-random-password-here'
DB_ROOT_PASSWORD=another-strong-random-password
```

Ahora añade las sales de seguridad de WordPress. Son 8 claves criptográficas que WordPress usa para cifrar cookies y tokens de autenticación. La imagen oficial de Docker genera hashes SHA1 aleatorios únicos a partir de los valores que proporciones. Es una mejora de seguridad gratuita.

<Notice type="success" title="Mejora de seguridad gratuita">
Añadir las sales de seguridad de WordPress no cuesta nada y hace que el secuestro de sesiones sea significativamente más difícil. La imagen de Docker lee estas variables de entorno y escribe las constantes `define()` correspondientes en `wp-config.php` en el primer arranque.
</Notice>

Añade esto a tu archivo `.env`:

```sh
WORDPRESS_AUTH_KEY='put-unique-phrase-here'
WORDPRESS_SECURE_AUTH_KEY='put-unique-phrase-here'
WORDPRESS_LOGGED_IN_KEY='put-unique-phrase-here'
WORDPRESS_NONCE_KEY='put-unique-phrase-here'
WORDPRESS_AUTH_SALT='put-unique-phrase-here'
WORDPRESS_SECURE_AUTH_SALT='put-unique-phrase-here'
WORDPRESS_LOGGED_IN_SALT='put-unique-phrase-here'
WORDPRESS_NONCE_SALT='put-unique-phrase-here'
```

Genera valores aleatorios reales con el generador oficial de sales de WordPress:

<Button text="Generar sales de WP →" link="https://api.wordpress.org/secret-key/1.1/salt/" variant="outline" color="blue" size="md" icon="arrow-right" />

Sustituye cada `put-unique-phrase-here` por los valores generados. No los reutilices entre instalaciones.

Si usas Dockge, puedes añadirlas como variables de entorno en la configuración del stack en lugar de un archivo `.env`. Para configuraciones de producción, mira los [secretos de Docker Compose](/es/secretos-docker-compose/) o las variantes de variables de entorno `_FILE` que admite la imagen de WordPress (p. ej., `WORDPRESS_DB_PASSWORD_FILE=/run/secrets/wp-db-password`).

### 5. Arranca WordPress en Docker

Si usas Dockge, guarda el archivo compose y haz clic en **Start**. Si no:

```sh
docker compose up -d
```

Vigila los logs para detectar errores de arranque:

```sh
docker compose logs -f
```

Espera unos 30 segundos y comprueba que todos los contenedores están sanos:

```sh
docker compose ps
```

Deberías ver todos los servicios con estado `Up`. Si incluiste health checks, el servicio `wp-db` debería mostrar `(healthy)` tras unos segundos, y el servicio `wp` alrededor de los 30 segundos.

<Notice type="info" title="El orden de arranque importa">
El `depends_on: condition: service_healthy` del servicio `wp` hace que WordPress no arranque hasta que MySQL supere su health check. Esto evita la típica condición de carrera del "Error establishing a database connection" que se produce con un `depends_on` simple.
</Notice>

**Si algo falla:**

- Puerto 5010 o 5011 ya en uso: `lsof -i :5010` para ver qué lo ocupa, luego cambia el mapeo de puertos
- El health check de MySQL sigue fallando: `docker compose logs wp-db` (busca errores de autenticación o de configuración)
- WordPress no puede conectar a la base de datos: verifica que los valores de `.env` coinciden entre los servicios `wp` y `wp-db`

### 6. Configura Cloudflare Tunnels para SSL

Cloudflare Tunnels te da SSL y protección DDoS sin abrir puertos en tu firewall ni gestionar certificados.

<Notice type="info" title="Ruta actualizada del panel">
La ruta del panel de Cloudflare ha cambiado. Ve a **Zero Trust → Networks → Tunnels**, no a la antigua ruta "Access → Tunnels" que referencian las guías más viejas.
</Notice>

En el panel de Cloudflare Zero Trust:

1. Ve a **Zero Trust → Networks → Tunnels**
2. Selecciona tu túnel (o crea uno con `cloudflared`)
3. Añade un hostname que mapee tu dominio a `http://localhost:5010`
4. Guarda — Cloudflare gestiona el SSL automáticamente

<Picture formats={["webp"]} fallbackFormat="webp" src={imag1} alt="Configuración de Cloudflare Tunnel" />

Puedes añadir un segundo hostname para phpMyAdmin en un subdominio (p. ej., `pma.yourdomain.com`) apuntando a `http://localhost:5011`. Recomiendo esto antes que exponer el puerto 5011 directamente.

Verifica que el túnel funciona:

```sh
curl -I https://yourdomain.com
```

Deberías obtener una respuesta `200` o `301` con las cabeceras `cf-ray` y `server: cloudflare`.

**¿502 Bad Gateway?** El contenedor de WordPress no está corriendo o el mapeo de puertos es incorrecto. Ejecuta `docker compose ps` y comprueba que el servicio `wp` está `Up`.

> También puedes usar [Traefik v3 como proxy inverso](/es/traefik-proxy-docker/) si prefieres gestionar el SSL tú mismo, o [CloudPanel con Dockge](/es/configurar-dockge-cloudpanel/) para un enfoque de proxy inverso diferente.

### 7. Completa la instalación de WordPress

Abre tu dominio en el navegador (o `http://your-server-ip:5010` si aún no has configurado Cloudflare). Verás el asistente de instalación de WordPress:

<Picture formats={["webp"]} fallbackFormat="webp" src={imag2} alt="Instalación de WordPress en Docker" />

Elige tu idioma, crea una cuenta de administrador y ya estás dentro. Después, configura los enlaces permanentes en **Ajustes → Enlaces permanentes** (yo uso «Nombre de la entrada» en la mayoría de los sitios) y empieza a añadir temas y plugins.

### 8. Personaliza la configuración de PHP para WordPress en Docker

Edita `./config/wp_php.ini` para ajustar PHP para WordPress:

```ini
file_uploads = On
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
max_input_time = 1000
```

Sube `memory_limit` a `512M` y `upload_max_filesize` a `128M` si usas WooCommerce o subes archivos multimedia grandes.

Tras editarlo, reinicia el contenedor de WordPress:

```sh
docker compose restart wp
```

<Notice type="info" title="Versión de PHP en la imagen de WordPress">
La imagen `wordpress:latest` incluye PHP 8.2 a mediados de 2025. Si has seguido esta guía, estás usando `wordpress:php8.3`, que es el mínimo recomendado por WordPress. La etiqueta `wordpress:php8.4` también está disponible y es totalmente compatible con WordPress 6.7+. Comprueba tu versión con: `docker exec wp php -v`
</Notice>

## Gestión y copias de seguridad de la base de datos

### 9. Accede a phpMyAdmin para la gestión de la base de datos

Accede a phpMyAdmin en `http://your-server-ip:5011` (o mediante un subdominio del túnel de Cloudflare). Inicia sesión con las credenciales de la base de datos de tu archivo `.env` — los valores `DB_USER` y `DB_PASSWORD`.

El `UPLOAD_LIMIT: 100M` del archivo compose te permite importar volcados de base de datos más grandes desde la interfaz de phpMyAdmin.

<Notice type="warning" title="No expongas phpMyAdmin públicamente">
phpMyAdmin da acceso total a tu base de datos. En producción, cierra el puerto 5011 con el firewall y accede solo a través de un Cloudflare Tunnel con una política de Access, o usa un túnel SSH: `ssh -L 5011:localhost:5011 your-server-ip`.
</Notice>

### 10. Verifica las copias de seguridad automáticas de la base de datos

El contenedor `wp-db-backup` se ejecuta según la programación definida por `DB_BACKUP_INTERVAL: 720` (cada 12 horas) y limpia las copias de seguridad con más de `DB_CLEANUP_TIME: 72000` minutos de antigüedad (~50 días).

Revisa el directorio de copias de seguridad:

```sh
ls -ltr ./backups/
```

Deberías ver archivos como:

```
-rw------- 1 10000 10000  495 Jul 17 09:26 mysql_wordpress_wp-db_20250717-092619.sql.zst
-rw------- 1 10000 10000   87 Jul 17 09:26 mysql_wordpress_wp-db_20250717-092619.sql.zst.sha1
lrwxrwxrwx 1 10000 10000   44 Jul 17 09:26 latest-mysql_wordpress_wp-db -> mysql_wordpress_wp-db_20250717-092619.sql.zst
```

<Notice type="info" title="Migración de la imagen de backups">
La imagen `tiredofit/db-backup` está migrando a `nfrastack/container-db-backup`. La imagen actual (fijada en 4.1) sigue funcionando bien. Estate atento a la nueva versión si configuras esto después de mediados de 2026. La extensión de compresión cambió de `.sql.gz` a `.sql.zst` (ZSTD es más rápido que GZ).
</Notice>

Para copias de seguridad completas del sitio (archivos + base de datos), combina esto con un plugin de backups de WordPress — consulta los [mejores plugins gratuitos de copias de seguridad para WordPress](/es/mejores-plugins-gratuitos-backup-wordpress/) para ver opciones que también cubren temas, plugins y archivos subidos.

### 11. Cómo restaurar una copia de seguridad de la base de datos

<Notice type="warning" title="Prueba tus restauraciones">
Una copia de seguridad que no puedes restaurar no es una copia de seguridad. Ejecuta este procedimiento al menos una vez después de la configuración inicial para asegurarte de que funciona.
</Notice>

Para restaurar desde una copia comprimida:

```sh
# Para backups comprimidos con ZSTD (nuevo valor por defecto)
zstd -d ./backups/latest-mysql_wordpress_wp-db -c | docker exec -i wp-db mysql -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}"

# Para backups comprimidos con GZ (si no has actualizado la compresión)
zcat ./backups/latest-mysql_wordpress_wp-db | docker exec -i wp-db mysql -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}"
```

Para verificar la integridad de la copia antes de restaurar, comprueba el archivo SHA1 adjunto:

```sh
cd ./backups
sha1sum -c mysql_wordpress_wp-db_20250717-092619.sql.zst.sha1
```

Tras restaurar, abre la administración de WordPress y confirma que tus entradas y páginas están presentes.

**Errores comunes:**

- "Access denied" — credenciales incorrectas o faltan comillas alrededor de la contraseña
- "Unknown database" — el `DB_NAME` del comando de restauración no coincide con la copia
- "ERROR 2006 (HY000)" — MySQL server has gone away, el volcado es demasiado grande; aumenta `max_allowed_packet` en la configuración de MySQL

## Mejoras de rendimiento opcionales

### 12. Añade Redis Object Cache a WordPress en Docker

<Notice type="error" title="Redis requiere una extensión de PHP">
La imagen oficial de WordPress para Docker NO incluye la extensión Redis de PHP. Añadir el servicio `redis-wp` a tu archivo compose no es suficiente — también debes instalar la extensión `phpredis` o usar la librería `Predis` de PHP puro. Sin esto, el plugin Redis Object Cache mostrará "Not connected".
</Notice>

Hay tres formas de añadir soporte para Redis. Recomiendo el enfoque del Dockerfile personalizado — es el más limpio.

<Tabs>
<Tab name="Dockerfile personalizado (recomendado)">

Crea un `Dockerfile` en el mismo directorio que tu `compose.yaml`:

```dockerfile
FROM wordpress:php8.3
RUN pecl install redis && docker-php-ext-enable redis
```

Luego cambia el servicio `wp` de tu archivo compose de `image: wordpress:php8.3` a:

```yaml
wp:
  build:
    context: .
    dockerfile: Dockerfile
```

El `WORDPRESS_CONFIG_EXTRA` del compose del stack completo (pestaña 2 del paso 3) ya incluye los defines de host y puerto de Redis. Reconstruye con:

```sh
docker compose up -d --build
```

</Tab>
<Tab name="Imagen preconstruida">

Usa `fazalfarhan01/wordpress-redis` — una imagen de la comunidad que incluye la extensión de Redis:

```yaml
wp:
  image: fazalfarhan01/wordpress-redis:php8.3
```

No hace falta Dockerfile, pero estás confiando en una imagen de terceros. Revisa la página de Docker Hub para las últimas etiquetas.

</Tab>
<Tab name="Predis (sin build)">

El plugin [Redis Object Cache](https://wordpress.org/plugins/redis-cache/) admite la librería `Predis` de PHP puro como alternativa a la extensión `phpredis`. Instálala vía Composer dentro del contenedor:

```sh
docker exec wp bash -c "curl -sS https://getcomposer.org/installer | php && php composer.phar require predis/predis"
```

Luego, en los ajustes del plugin, cambia el cliente a "Predis". Es más lento que la extensión nativa, pero no requiere personalizar la imagen.

</Tab>
</Tabs>

Tras activar el plugin Redis Object Cache (de Till Krüss), ve a **Ajustes → Redis** en la administración de WordPress y haz clic en **Enable Object Cache**. El estado debería mostrar "Connected".

**¿No conecta?** Comprueba que:
1. El contenedor de Redis está corriendo: `docker compose ps redis-wp`
2. El host/puerto en `WP_REDIS_HOST` / `WP_REDIS_PORT` coinciden con el nombre del servicio y el puerto
3. La extensión PHP de Redis está realmente instalada: `docker exec wp php -m | grep redis`

> Para máximo rendimiento, combina el caché de Redis con Varnish y Cloudflare — consulta [Cómo acelerar WordPress con Cloudflare, Varnish y Redis](/es/acelerar-wordpress-cloudflare-varnish-redis/).

## Endurecimiento para producción

### 13. Health checks de Docker para producción

Los health checks ya están configurados en el archivo compose del paso 3. Esto es lo que hacen:

- **MySQL (`wp-db`)**: ejecuta `mysqladmin ping` cada 10 segundos. Tras 5 comprobaciones fallidas, el contenedor se marca como unhealthy. Esto evita que WordPress conecte antes de que MySQL esté listo.
- **WordPress (`wp`)**: hace curl a la página de instalación cada 30 segundos. Confirma que el servidor web y PHP responden.

Monitoriza el estado de salud:

```sh
docker compose ps
```

Todos los servicios deberían mostrar `(healthy)` en la columna STATUS. Si un servicio está `(unhealthy)`, revisa sus logs: `docker compose logs <nombre-del-servicio>`.

La política `restart: unless-stopped` hace que los contenedores se reinicien automáticamente ante un fallo o un reinicio del servidor, pero permanezcan detenidos si los paras manualmente.

### 14. Contenedor WP-CLI para el mantenimiento de WordPress

La imagen oficial `wordpress:cli` te da acceso por línea de comandos a WordPress sin instalar nada extra. Ejecuta comandos contra tu contenedor de WordPress existente:

```sh
docker run -it --rm \
  --volumes-from wp \
  --network container:wp \
  wordpress:cli \
  wp plugin list
```

<Accordion label="Comandos comunes de WP-CLI" group="wpcli" expanded="false">

**Gestión de plugins:**
```sh
# Lista los plugins instalados
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin list

# Actualiza todos los plugins
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin update --all

# Desactiva un plugin
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin deactivate plugin-name
```

**Gestión de usuarios:**
```sh
# Lista los usuarios
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp user list

# Restablece la contraseña de un usuario
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp user update admin --user_pass=newpassword
```

**Operaciones con la base de datos:**
```sh
# Exporta la base de datos
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp db export /var/www/html/backup.sql

# Buscar y reemplazar URLs (útil tras cambios de dominio)
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp search-replace 'http://old-domain.com' 'https://new-domain.com' --skip-columns=guid
```

**Actualizaciones del núcleo:**
```sh
# Comprueba la versión actual
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp core version

# Actualiza el núcleo de WordPress
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp core update
```

</Accordion>

El define `FS_METHOD: direct` en `WORDPRESS_CONFIG_EXTRA` es necesario para que WP-CLI (y el propio WordPress) pueda escribir archivos en Docker sin FTP.

**¿"Error: This does not seem to be a WordPress install"?** Te falta `--volumes-from wp` o el nombre del contenedor es incorrecto. Compruébalo con `docker compose ps`.

### 15. Lista de verificación de endurecimiento de seguridad

<ListCheck>
<ul>
<li>MySQL 8.4 LTS fijado (no <code>mysql:latest</code>)</li>
<li>Sales de seguridad de WordPress definidas en <code>.env</code></li>
<li><code>FS_METHOD: direct</code> en <code>WORDPRESS_CONFIG_EXTRA</code></li>
<li>Puerto 5011 (phpMyAdmin) cerrado con firewall — acceso solo vía Cloudflare Tunnel o SSH</li>
<li>Política de Cloudflare Access delante del subdominio de phpMyAdmin</li>
<li>Las contraseñas de la base de datos son cadenas aleatorias fuertes, no palabras de diccionario</li>
<li>Prefijo de tablas de WordPress cambiado de <code>wp_</code> si eres paranoico (se define en <code>WORDPRESS_TABLE_PREFIX</code>)</li>
<li>XML-RPC desactivado si no usas Jetpack (añádelo a <code>.htaccess</code> o usa un plugin)</li>
<li>Límites de recursos definidos en el archivo compose para evitar contenedores desbocados</li>
<li>Restauraciones de copias de seguridad probadas regularmente (no solo su creación)</li>
</ul>
</ListCheck>

<Notice type="success" title="Ya cubierto">
La mayoría de estos puntos ya quedan cubiertos siguiendo los pasos 3-6 de esta guía. Esta lista está aquí como referencia y para cuando audites tu configuración más adelante.
</Notice>

Monitoriza tu servidor para detectar anomalías pronto — consulta [Cómo monitorizar recursos del servidor y Docker](/es/monitorizar-servidor/) para configurar la monitorización de recursos con herramientas como Beszel o Netdata.

### 16. MariaDB como alternativa a MySQL

MariaDB es totalmente compatible con WordPress, tiene una huella de memoria más ligera y está recomendado junto a MySQL en el WordPress Hosting Handbook. Muchos autoalojados lo prefieren.

Para cambiar, modifica una línea de tu archivo compose:

```yaml
# Sustituye esto:
  wp-db:
    image: mysql:8.4

# Por esto:
  wp-db:
    image: mariadb:11.4
```

<Notice type="info" title="Mismas variables de entorno, mismo todo">
MariaDB usa las mismas variables de entorno que MySQL (`MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE`, etc.). No hace falta ningún otro cambio — solo cambia la etiqueta de la imagen. MariaDB 11.4 LTS tiene soporte hasta mayo de 2029.
</Notice>

### 17. Actualizar WordPress en Docker

Dos estrategias según el control que quieras:

**Estrategia 1: Autogestionada (por defecto)**

WordPress se auto-actualiza dentro del volumen. Es el comportamiento por defecto — WordPress busca actualizaciones y las aplica sin que toques Docker. Simple, pero tu infraestructura no es inmutable.

**Estrategia 2: Versión fijada (recomendada para producción)**

Fija la versión de la imagen de WordPress en tu archivo compose, desactiva las auto-actualizaciones y controla cuándo actualizas:

```yaml
wp:
  image: wordpress:php8.3:6.8
```

Añade a `WORDPRESS_CONFIG_EXTRA`:

```php
define('WP_AUTO_UPDATE_CORE', false);
```

Cuando estés listo para actualizar, cambia la etiqueta de versión y redespliega:

```sh
docker compose pull
docker compose up -d
```

Tu volumen `wp-app` conserva todos los archivos de WordPress, temas, plugins y subidas. El contenedor es solo el runtime — tus datos viven en el host.

**¿"Another update is in progress"?** Es un transient atascado. Límpialo con WP-CLI:

```sh
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp option delete core_updater.lock
```

### 18. Próximos pasos

Tu stack de WordPress en Docker está en marcha. Algunas cosas para hacer desde aquí:

- **Cierra los puertos del firewall** — si usas Cloudflare Tunnels, bloquea los puertos 5010 y 5011 a nivel de firewall para que solo el túnel pueda alcanzarlos. El acceso es solo a través de tu dominio.
- **Configura monitorización** — [monitoriza los recursos de tu servidor y de Docker](/es/monitorizar-servidor/) para detectar picos de CPU, discos llenos y reinicios de contenedores antes de que se conviertan en problemas.
- **Explora paneles autoalojados** — si quieres una interfaz de gestión más amplia, mira los [mejores paneles de servidor autoalojados](/es/mejores-paneles-autoalojados/) para opciones más allá de Dockge.
- **Domina los comandos de Docker** — guarda estos [comandos esenciales de Docker](/es/comandos-docker/) para diagnosticar contenedores, liberar espacio en disco y gestionar imágenes.
- **Instala temas y plugins** — WordPress está listo para tu contenido. Empieza con un tema ligero y añade solo los plugins que necesites.

## Conclusión

Ahora tienes un stack de WordPress listo para producción corriendo en Docker con MySQL 8.4 LTS (con soporte hasta 2032), phpMyAdmin para la gestión de la base de datos, copias de seguridad automáticas con compresión ZSTD, caché de objetos Redis opcional y SSL a través de Cloudflare Tunnels — todo desde un único archivo `compose.yaml`.

El siguiente paso más importante es probar tus copias de seguridad. Una copia que nunca has restaurado es una apuesta, no una estrategia. Ejecuta el procedimiento de restauración del paso 11 al menos una vez, y luego pon un recordatorio en el calendario para probarlo cada trimestre.

Si algo se rompe, `docker compose logs` es tu mejor amigo. La mayoría de los problemas se reducen a conflictos de puertos, errores de permisos o que MySQL no esté listo cuando WordPress intenta conectar — todo lo cual los health checks de esta configuración están diseñados para detectar.