Docker Compose no sustituye ni a una estrategia de copias de seguridad, ni a una política de actualización, ni al fortalecimiento del VPS. Lo que sí aporta es un modelo declarativo que hace reproducible un stack: servicios, imágenes, redes, volúmenes, puertos y políticas de reinicio quedan descritos en un archivo que se puede releer, probar y versionar.
Esta guía no cubre la instalación de Docker. El motor y el plugin Compose deben estar ya operativos según la guía Instalar Docker en un VPS Linux. La trampa Docker/UFW también se trata allí. Aquí el objetivo es explotar de forma duradera una aplicación y su base de datos en un VPS.
El caso práctico utiliza las imágenes oficiales de WordPress y MariaDB. Los principios se aplican después a Bitwarden, Pi-hole, Jellyfin, Portainer y a la mayoría de stacks formados por una aplicación y un servicio de datos.
Las tres convenciones que debe aplicar hoy
1. No añadir la clave version:
Un archivo moderno empieza directamente por services:. La propiedad de primer nivel version se conserva únicamente por compatibilidad. Docker la califica de obsoleta, precisa que es solo informativa e indica que su uso produce un aviso. Compose valida el archivo con la especificación más reciente en cualquier caso. Añadir version: "3.8" no selecciona, por tanto, ningún motor de compatibilidad concreto.
Fuente: docs.docker.com, propiedad version
2. Usar docker compose, en dos palabras
Los comandos de esta guía utilizan el plugin integrado en la CLI de Docker:
docker compose up -d El comando docker-compose, con guion, designa la antigua herramienta v1 escrita en Python. Compose v2, anunciada en 2020, está escrita en Go y se invoca con docker compose. Compose v5, publicada en 2025, es funcionalmente idéntica a v2 en el lado de la CLI: su principal novedad es un SDK oficial en Go, y la numeración saltó directamente a 5 para evitar confusiones con los antiguos formatos de archivo etiquetados «v2» y «v3». En todos los casos, la forma que debe usarse sigue siendo docker compose.
Fuente: docs.docker.com, historia de Docker Compose
3. Llamar al archivo compose.yaml
El nombre canónico es compose.yaml. Las variantes compose.yml, docker-compose.yaml y docker-compose.yml siguen reconociéndose por compatibilidad, pero Docker recomienda el nombre canónico y le da prioridad cuando hay varias variantes en el mismo directorio.
Fuente: docs.docker.com, modelo de aplicación Compose
La arquitectura elegida
El stack respeta cuatro fronteras sencillas:
- el servicio
appes el único que publica un puerto en el host; - ese puerto escucha únicamente en
127.0.0.1, para que un proxy inverso instalado en el VPS sea el único punto de entrada público; - el servicio
dbno publica ningún puerto y solo es alcanzable por su nombre DNSdben la red privadabackend; - los datos sobreviven a la recreación de los contenedores gracias a dos volúmenes con nombre.
Compose crea un DNS interno para los servicios de una misma red. La aplicación se conecta por tanto a db:3306, no a una dirección IP de contenedor. Una IP de contenedor es efímera y nunca debe fijarse en la configuración.
Preparar un directorio de explotación
Un stack debe tener una ubicación conocida, permisos restrictivos y una estructura que se pueda respaldar.
sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/stacks/wordpress-prod
cd /opt/stacks/wordpress-prod
umask 077
mkdir -p secrets backups
openssl rand -base64 48 > secrets/db_password.txt
openssl rand -base64 48 > secrets/db_root_password.txt
chmod 600 secrets/*.txt
chmod 700 secrets backups La cuenta que lanza Compose debe poder leer ambos archivos. Permanecen en claro en el disco del VPS: los secretos locales de Compose se entregan al contenedor como archivos montados en /run/secrets, pero su origen sigue siendo un archivo local que hay que proteger. Este mecanismo limita su exposición a los servicios autorizados y evita colocarlos directamente en variables de entorno. No sustituye a un gestor de secretos centralizado.
Fuente: docs.docker.com, gestión de secretos con Compose
El archivo compose.yaml, comentado línea por línea
El archivo empieza deliberadamente por services:. No contiene ninguna clave version:.
services:
# Servicio HTTP de la aplicación.
app:
# La referencia de imagen viene de .env y debe validarse antes de producción.
image: "${WORDPRESS_IMAGE:?Defina WORDPRESS_IMAGE en .env}"
# Se reinicia al arrancar, salvo que un administrador haya parado el servicio a propósito.
restart: unless-stopped
# Evita procesos zombis si la imagen no los recoge por sí misma.
init: true
# La base debe declararse sana antes de crear la aplicación.
depends_on:
db:
condition: service_healthy
# El puerto 80 del contenedor escucha solo en el bucle local del VPS.
ports:
- "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8080}:80"
# Los valores no sensibles pueden interpolarse desde .env.
environment:
WORDPRESS_DB_HOST: "db:3306"
WORDPRESS_DB_NAME: "${DB_NAME:?Defina DB_NAME en .env}"
WORDPRESS_DB_USER: "${DB_USER:?Defina DB_USER en .env}"
# La imagen oficial sabe leer la contraseña desde un archivo.
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
# El contenido de la aplicación persiste más allá del ciclo de vida del contenedor.
volumes:
- wordpress_data:/var/www/html
# Solo se concede a la aplicación la contraseña que necesita.
secrets:
- db_password
# La aplicación recibe el tráfico frontal y dialoga con la base en privado.
networks:
- frontend
- backend
# Un límite local evita que los registros llenen el disco del VPS.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Da tiempo al servidor HTTP para cerrar sus conexiones correctamente.
stop_grace_period: 30s
# Servicio de base de datos, sin puerto publicado en el host.
db:
# La referencia de imagen también se gestiona en .env.
image: "${MARIADB_IMAGE:?Defina MARIADB_IMAGE en .env}"
# Una parada manual se respeta tras reiniciar el demonio o el VPS.
restart: unless-stopped
# Inicializa la base y el usuario solo en el primer arranque.
environment:
MARIADB_DATABASE: "${DB_NAME:?Defina DB_NAME en .env}"
MARIADB_USER: "${DB_USER:?Defina DB_USER en .env}"
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
# Los archivos de datos de MariaDB residen en un volumen con nombre.
volumes:
- mariadb_data:/var/lib/mysql
# A diferencia de la aplicación, la base recibe ambos secretos.
secrets:
- db_password
- db_root_password
# Este script lo proporciona la imagen oficial de MariaDB.
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# La base pertenece únicamente a la red privada interna.
networks:
- backend
# La misma política de rotación local que para la aplicación.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Una base puede necesitar tiempo para terminar sus escrituras.
stop_grace_period: 1m
# Declaración de los almacenamientos persistentes gestionados por Docker.
volumes:
wordpress_data:
mariadb_data:
# Declaración de las dos zonas de red del stack.
networks:
frontend:
driver: bridge
backend:
driver: bridge
# Impide que esta red proporcione conectividad externa directa.
internal: true
# Cada secreto local procede de un archivo distinto en el host.
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt La imagen oficial de WordPress admite la convención WORDPRESS_DB_PASSWORD_FILE. La imagen oficial de MariaDB acepta las variantes MARIADB_*_FILE e incluye healthcheck.sh. Estos comportamientos pertenecen a las imágenes, no a Compose. Conviene por tanto consultar la documentación de cada imagen antes de trasladar este modelo a otra aplicación.
Fuentes: imagen oficial de WordPress, imagen oficial de MariaDB, referencia de MariaDB sobre healthcheck.sh
depends_on con condition: service_healthy impide que Compose cree la aplicación antes de que la comprobación de salud de la base tenga éxito. Eso mejora el arranque inicial, pero no exime a la aplicación de saber reintentar una conexión perdida durante la explotación. Véase el orden de arranque en Compose.
El archivo .env: configuración, no caja fuerte
Compose lee automáticamente un archivo .env situado junto a compose.yaml y lo usa para interpolar las expresiones ${VARIABLE}.
# Nombre estable del proyecto. Influye en el nombre real de volúmenes y redes.
COMPOSE_PROJECT_NAME=wordpress-prod
# El servicio permanece local al VPS y lo publicará un proxy inverso.
APP_BIND_IP=127.0.0.1
APP_PORT=8080
# Valores no sensibles transmitidos a ambos servicios.
DB_NAME=wordpress
DB_USER=wordpress
# Selectores legibles para preparar el primer pull.
# Antes de producción, sustitúyalos por los digests validados como se explica abajo.
WORDPRESS_IMAGE=wordpress:apache
MARIADB_IMAGE=mariadb:lts Fuente: docs.docker.com, interpolación de variables en Compose
Las etiquetas de imagen son mutables: un editor puede hacer que la misma etiqueta apunte a un contenido distinto. Para un despliegue estrictamente reproducible, valide la imagen y fije después su referencia con un digest nombre@sha256:.... Actualizar el digest se convierte así en un cambio deliberado y revisable. Véanse las buenas prácticas de Docker sobre la fijación por digest.
Para obtener las referencias inmutables correspondientes a las imágenes que acaba de probar:
docker pull wordpress:apache
docker pull mariadb:lts
docker image inspect wordpress:apache --format '{{index .RepoDigests 0}}'
docker image inspect mariadb:lts --format '{{index .RepoDigests 0}}' Copie cada resultado completo en la variable correspondiente de .env. Tras esta fijación, docker compose pull no cambiará el contenido en silencio: la actualización pasará por una modificación explícita del digest.
El digest fija la imagen, no el estado del volumen. Esto importa especialmente en este ejemplo: la imagen oficial de WordPress indica que las actualizaciones automáticas de WordPress pueden modificar el contenido de /var/www/html después del despliegue. Una política de actualización reproducible debe cubrir por tanto tanto las imágenes como el mecanismo de actualización propio de la aplicación.
Aunque el ejemplo no coloca ninguna contraseña en .env, ese archivo no debe subirse al repositorio. En muchos proyectos acaba recibiendo un token, una URL privada o un valor propio de producción. Un secreto escrito directamente en compose.yaml o en .env y subido a Git permanece en el historial aunque se elimine en el último commit.
Otra trampa importante: las variables MARIADB_DATABASE, MARIADB_USER y MARIADB_*_PASSWORD_FILE sirven para inicializar un directorio de datos vacío. La imagen oficial precisa que no reconfiguran una base ya existente. Sustituir el contenido de un archivo de secreto no cambia por tanto automáticamente la contraseña registrada en MariaDB. Una rotación debe modificar la cuenta en la base, actualizar el secreto que consume la aplicación y después reiniciar o recargar los componentes según su documentación.
Cree un archivo .gitignore:
.env
secrets/
backups/ Puede versionar un .env.example que contenga únicamente nombres de variables y valores ficticios. Proteja el archivo real:
chmod 600 .env Cuidado también con docker compose config sin opciones: este comando muestra la configuración resuelta y puede revelar valores interpolados si ha colocado secretos en variables. docker compose config --quiet valida sin imprimir nada. Véase la referencia de docker compose config.
Volúmenes con nombre y bind mounts: no confundirlos
Un contenedor es reemplazable. Todo dato escrito únicamente en su capa interna desaparece con él. La persistencia debe declararse explícitamente.
| Criterio | Volumen con nombre | Bind mount |
|---|---|---|
| Origen | Objeto gestionado por Docker | Ruta explícita del VPS, por ejemplo /srv/app/config |
| Caso recomendado | Datos generados por la aplicación o la base | Archivo de configuración administrado desde el host, certificado, contenido que el host debe manipular directamente |
| Portabilidad | Poco acoplado al árbol de directorios del VPS | Depende de la ruta, de los permisos y a veces del contexto SELinux del host |
| Riesgo principal | Olvidar que existe fuera de la carpeta del stack | Montar la ruta equivocada, ocultar contenido ya presente en la imagen o dar un acceso de escritura demasiado amplio |
| Copia de seguridad | Debe exportarse explícitamente | Debe incluirse explícitamente en la copia de la ruta del host |
Docker recomienda los volúmenes para los datos persistentes producidos por los contenedores. Un volumen sobrevive a la eliminación del contenedor que lo usaba. Un bind mount es preferible cuando un administrador o una herramienta del host debe modificar directamente un archivo.
En este stack:
wordpress_dataconserva/var/www/html;mariadb_dataconserva/var/lib/mysql;./secrets/*.txtson archivos del host montados por separado en los servicios autorizados.
¿Dónde reside realmente un volumen con nombre?
El nombre mariadb_data es el nombre lógico del modelo Compose. Con COMPOSE_PROJECT_NAME=wordpress-prod, Docker crea normalmente un nombre como wordpress-prod_mariadb_data. No construya sus scripts sobre esa suposición. Compose aplica etiquetas a los volúmenes, y docker volume inspect devuelve su punto de montaje real:
docker volume ls \
--filter label=com.docker.compose.project=wordpress-prod
DB_VOLUME="$(docker volume ls \
--filter label=com.docker.compose.project=wordpress-prod \
--filter label=com.docker.compose.volume=mariadb_data \
--format '{{.Name}}')"
docker volume inspect "$DB_VOLUME" --format '{{.Mountpoint}}' Con el controlador local y un demonio Docker root clásico, la ruta suele estar bajo /var/lib/docker/volumes/. No es una garantía: el modo rootless, un data-root personalizado o un controlador remoto cambian esa ubicación. La respuesta de docker volume inspect es la que manda. No edite directamente los archivos internos de una base en ese directorio.
Los comandos que destruyen datos
docker compose down retira los contenedores y las redes del stack, pero conserva los volúmenes con nombre por defecto. La opción -v pide también su eliminación.
No use nunca
docker compose down -vcomo comando de actualización o de resolución de problemas. Compruebe que dispone de una copia restaurable antes de eliminar deliberadamente cualquier volumen.
Del mismo modo, docker volume prune elimina los volúmenes considerados sin uso. Un volumen de producción pasa a estar «sin uso» en cuanto se elimina su contenedor, aunque sus datos sigan siendo imprescindibles. Véase el comportamiento de docker compose down.
Publicación de puertos: la diferencia entre local y público
Estas dos líneas no son equivalentes:
ports:
- "8080:80" Sin dirección de host, Docker publica el puerto en todas las direcciones del VPS, en la práctica 0.0.0.0 y, según la configuración, IPv6. El servicio puede entonces volverse accesible desde internet si el enrutamiento y las reglas de red lo permiten.
ports:
- "127.0.0.1:8080:80" Aquí el puerto solo escucha en el bucle local IPv4. Es la opción correcta cuando Nginx o Apache se ejecuta directamente en el VPS y reenvía las peticiones a http://127.0.0.1:8080. Docker documenta explícitamente que la ausencia de dirección publica en todas las direcciones y que enlazar a 127.0.0.1 limita el acceso al host.
Fuente: docs.docker.com, publicación de puertos
Compruebe el resultado, no se limite a releer el YAML:
docker compose ps
ss -lntp | grep ':8080'
curl --fail --head http://127.0.0.1:8080 La base no tiene sección ports. La directiva expose no es necesaria para que app alcance a db en la misma red Compose.
Docker gestiona sus propias reglas de cortafuegos y un puerto publicado puede eludir la ruta de filtrado esperada con UFW. Este punto se detalla en Asegurar un VPS Linux: la checklist completa y en la guía de instalación de Docker citada como requisito. Las tres reglas coherentes son: no publicar lo que no debe publicarse, enlazar a 127.0.0.1 detrás de un proxy local y después comprobar qué escucha realmente.
Si el proxy inverso es a su vez un contenedor, 127.0.0.1 designa a ese contenedor, no al host ni a la aplicación. Conecte entonces el proxy y la aplicación a una red Docker compartida, sin publicar el puerto de la aplicación en internet. Para un proxy instalado en el host, consulte Alojar varios sitios en un VPS con Nginx y después Certbot: instalar un SSL Let's Encrypt en VPS.
¿always o unless-stopped al reiniciar el VPS?
Ambas políticas relanzan un contenedor tras un fallo y cuando vuelve el demonio Docker. La diferencia aparece tras una parada administrativa:
alwaysrelanza un contenedor parado manualmente cuando se reinicia el demonio Docker;unless-stoppedrespeta esa parada manual, incluso tras reiniciar el demonio o el VPS.
Para un stack administrado manualmente, unless-stopped evita volver a poner en línea, en el siguiente arranque, un servicio que se había parado deliberadamente para mantenimiento. always conviene cuando un servicio debe volver siempre y ese comportamiento se busca explícitamente. La política no sustituye a una comprobación de salud: reacciona a la parada del proceso principal, no a una aplicación viva pero bloqueada.
Fuente: docs.docker.com, políticas de reinicio
Validar y arrancar el stack
Empiece por comprobar la CLI:
docker compose version
docker info Después valide y despliegue desde el directorio del stack:
cd /opt/stacks/wordpress-prod
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 --timestamps docker compose pull descarga las imágenes pero no sustituye los contenedores en ejecución. docker compose up -d compara después el estado deseado con el existente, crea lo que falta y recrea los servicios cuya imagen o configuración ha cambiado. Los volúmenes montados se conservan durante esa recreación. Véase el comportamiento de docker compose up.
Tras el primer arranque, compruebe también la exposición desde otra máquina. Una prueba local satisfactoria no demuestra que el puerto 8080 sea inaccesible públicamente.
Actualizar sin perder datos
Una actualización en producción es un pequeño procedimiento de cambio, no un comando lanzado a ciegas.
Antes de la actualización
- Lea las notas de versión de la aplicación y de la base, en particular las migraciones y las rutas de actualización admitidas.
- Compruebe el espacio en disco y los inodos con
df -hydf -i. - Realice una copia de seguridad y pruebe su restauración con regularidad.
- Anote las referencias de imagen desplegadas actualmente con
docker compose images. - Modifique en
.envlas etiquetas precisas o los digests solo después de validarlos. - Ejecute
docker compose config --quiet.
Aplicar la actualización
cd /opt/stacks/wordpress-prod
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps Lo que puede recrearse:
- el contenedor
appsi cambia su referencia de imagen o su configuración; - el contenedor
dbsi cambia su referencia de imagen o su configuración; - las redes si cambia su definición.
Lo que se conserva:
- el contenido de los volúmenes con nombre
wordpress_dataymariadb_data; - los archivos del VPS usados como bind mounts u orígenes de secretos;
- las imágenes anteriores mientras no se hayan eliminado.
docker compose restart no basta tras modificar compose.yaml o las variables: ese comando reinicia los contenedores existentes sin aplicar la nueva configuración. Use docker compose up -d. Véase la referencia de docker compose restart.
Una recreación puede provocar una breve interrupción. Compose en un VPS único no promete una actualización sin corte. Para una aplicación crítica, prevea una estrategia adaptada a la aplicación, una segunda instancia o una plataforma de orquestación.
Volver atrás
Para una vuelta atrás de la aplicación, restituya en .env la referencia de imagen anterior y relance pull y up -d. Este método no anula una migración de esquema. Si la nueva versión ha modificado la base de forma incompatible, solo un plan de retorno documentado por el editor o una restauración validada permiten volver limpiamente. Esa es la razón por la que la copia de seguridad precede a la actualización.
Respaldar el stack, no solo su YAML
Copiar compose.yaml no respalda ningún dato contenido en los volúmenes. A la inversa, copiar en caliente los archivos internos de MariaDB no garantiza una base coherente. Para este caso se combinan:
- un volcado lógico de MariaDB, producido por la herramienta de la base;
- un archivo comprimido del volumen de la aplicación mientras esta está parada;
- una copia de
compose.yaml,.envy la lista de referencias de imagen; - una copia separada y cifrada de los archivos de secretos;
- una copia fuera del VPS, con retención y pruebas de restauración.
MariaDB documenta mariadb-dump en su guía oficial de copia de seguridad de contenedores.
Ejemplo de copia coherente
Ejecute este bloque con Bash desde el directorio del stack. La aplicación se para para impedir escrituras durante el volcado y el archivado. La base permanece activa mientras dura el volcado lógico. El trap reinicia la aplicación incluso si un comando falla.
set -Eeuo pipefail
cd /opt/stacks/wordpress-prod
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DEST="backups/$STAMP"
install -d -m 0700 "$DEST"
docker compose config --quiet
docker compose stop app
trap 'docker compose start app' EXIT
docker compose exec -T db sh -c '
exec mariadb-dump \
--user=root \
--password="$(cat /run/secrets/db_root_password)" \
--single-transaction \
--routines \
--triggers \
--events \
"$MARIADB_DATABASE"
' | gzip -9 > "$DEST/database.sql.gz"
docker compose run --rm --no-deps -T \
--entrypoint tar app \
-C /var/www/html -czf - . \
> "$DEST/wordpress-data.tar.gz"
cp compose.yaml .env "$DEST/"
docker compose images > "$DEST/images.txt"
gzip -t "$DEST/database.sql.gz"
tar -tzf "$DEST/wordpress-data.tar.gz" > /dev/null
(cd "$DEST" && sha256sum database.sql.gz wordpress-data.tar.gz compose.yaml .env images.txt > SHA256SUMS)
docker compose start app
trap - EXIT Este script no copia deliberadamente secrets/ en el mismo archivo. Exporte esos archivos a una caja fuerte o a una copia cifrada con un control de acceso distinto. Una copia presente únicamente en el mismo VPS desaparece con el disco, la cuenta o el incidente que destruye la producción.
El volcado con --single-transaction es adecuado para tablas transaccionales. Una aplicación que usa otros motores o varios sistemas de almacenamiento exige un procedimiento de coherencia propio del editor. Para grandes volúmenes y objetivos de recuperación estrictos, estudie también las herramientas de copia física, la replicación y las instantáneas coordinadas.
Si prefiere no programar ni supervisar usted mismo esta cadena, la opción de copias de seguridad automáticas disponible con nuestros VPS Linux realiza una copia diaria en un centro de datos distinto, con un historial rotativo y restauración en un clic. No exime de probar una restauración a nivel de aplicación, pero cubre el caso en que se pierde el propio VPS.
Probar una restauración
Un archivo cuya restauración nunca se ha probado no es más que una hipótesis de copia de seguridad. La prueba debe realizarse en un proyecto aislado, con volúmenes vacíos y las mismas referencias de imagen que la copia:
- copie
compose.yaml,.envy los secretos de prueba a otro VPS o a un directorio aislado; - cambie
COMPOSE_PROJECT_NAMEy el puerto del host para no tocar la producción; - cree el volumen de la aplicación lanzando un comando puntual y extraiga después el archivo;
- arranque únicamente
db, espere su estadohealthye importe después el volcado en la base vacía; - arranque
app, verifique las funciones de negocio y anote el tiempo real de recuperación.
Ejemplo de importación en una base de pruebas ya inicializada y vacía:
gzip -dc backups/DATE/database.sql.gz | \
docker compose exec -T db sh -c '
exec mariadb \
--user=root \
--password="$(cat /run/secrets/db_root_password)" \
"$MARIADB_DATABASE"
' No ejecute esta importación sobre la base de producción existente. Un procedimiento de restauración completo debe precisar cómo obtener una base vacía, qué tiempo de parada se acepta y cómo volver al estado anterior si la validación falla.
Leer los registros y diagnosticar un bucle de reinicio
El primer error suele ser lanzar down de inmediato. Eso elimina los contenedores y hace perder parte de la información de estado útil. Empiece por observar.
docker compose ps --all
docker compose logs --tail=200 --timestamps app
docker compose logs --tail=200 --timestamps db
docker compose logs --follow --since=10m app El comando logs permite filtrar por servicio, seguir el flujo y limitar el historial con --tail o --since. Véase la referencia de docker compose logs.
Inspeccione después el estado exacto del contenedor:
CID="$(docker compose ps -q app)"
docker inspect "$CID" --format \
'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} error={{.State.Error}}'
docker compose top
docker stats --no-stream
df -h
df -i
free -h
journalctl -u docker --since '30 minutes ago' Las causas frecuentes son:
- secreto ausente, ilegible o montado con un nombre incorrecto;
- base todavía no disponible, credenciales incoherentes o migración fallida;
- puerto del host ya ocupado;
- volumen montado en el lugar equivocado o permisos incompatibles con el usuario del contenedor;
- proceso terminado por falta de memoria, visible con
OOMKilled=true; - disco o inodos saturados;
- imagen incompatible con la arquitectura del VPS;
- configuración interpolada distinta de la esperada.
Valide el modelo sin mostrar los valores y consulte después las variables esperadas:
docker compose config --quiet
docker compose config --variables Si el servicio se reinicia demasiado deprisa para permitir exec, lance un contenedor puntual sin sus dependencias y con un intérprete de comandos, siempre que la imagen incluya uno:
docker compose run --rm --no-deps --entrypoint sh app Ese contenedor puntual monta los mismos volúmenes y secretos declarados para el servicio. Evite cualquier modificación mientras no se comprenda la causa.
La rotación json-file declarada en el archivo limita el espacio que consumen los registros locales. Docker precisa que max-size vale -1 por defecto, es decir, sin límite. Para una producción importante, envíe también los registros a un sistema externo con una retención adecuada. Véase el controlador de registros json-file.
Controles de explotación que conviene conservar
En cada modificación
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps Antes de cada actualización
- notas de versión y ruta de migración leídas;
- espacio en disco comprobado;
- copia creada, exportada fuera del VPS y restauración ya probada;
- imágenes actuales anotadas;
- ventana de interrupción anunciada si es necesario.
Tras cada reinicio del VPS
docker compose ps
docker compose logs --since=30m --timestamps
ss -lntp Verifique que la aplicación está sana, que la base no está publicada, que el puerto de la aplicación sigue enlazado a 127.0.0.1 y que el proxy inverso sirve el dominio por HTTPS.
Errores frecuentes que conviene evitar
- empezar el archivo por
version: "3.8"; - usar el binario histórico
docker-compose; - llamar al archivo
docker-compose.ymlen un proyecto nuevo; - escribir
8080:80pensando que el servicio permanece local; - publicar
3306:3306cuando solo la aplicación debe alcanzar la base; - almacenar las contraseñas en
compose.yamlo en el repositorio Git; - usar un bind mount para una base sin dominar propietario, permisos, copia de seguridad y contexto de seguridad;
- creer que eliminar un contenedor elimina o respalda su volumen;
- lanzar
docker compose down -vpara «empezar de cero»; - ejecutar
docker compose restartesperando aplicar una nueva configuración; - actualizar una base sin leer su ruta de migración ni disponer de una vuelta atrás;
- respaldar únicamente en el disco del VPS;
- confundir un contenedor
runningcon una aplicación realmente sana.
Preguntas frecuentes
¿Hay que seguir escribiendo version en un archivo Compose?
No. La propiedad de primer nivel version está documentada como obsoleta: es puramente informativa y produce un aviso. Compose interpreta el archivo con la especificación actual, sea cual sea el valor escrito.
¿Qué diferencia hay entre docker compose y docker-compose?
docker-compose con guion es la herramienta v1 en Python. docker compose en dos palabras es el plugin en Go, en v2 desde 2020 y en v5 desde 2025. Hoy solo debe usarse esta segunda forma.
¿docker compose down elimina mis datos?
No por defecto: los volúmenes con nombre se conservan. Es la opción -v la que los elimina, así como docker volume prune sobre un volumen del que ya no existe ningún contenedor.
¿Cómo actualizar un stack sin perder la base?
Haga una copia de seguridad, cambie la referencia de imagen en .env y lance después docker compose pull seguido de docker compose up -d. Los contenedores se recrean, los volúmenes con nombre se conservan. docker compose restart no basta: no vuelve a leer la configuración.
¿Qué configuración de VPS conviene a un stack Compose?
Una aplicación web y su base caben en 2 vCPU y 4 GB de RAM. Prevea más memoria en cuanto añada una caché, un motor de búsqueda o varios stacks en la misma máquina, y vigile sobre todo el espacio en disco: las imágenes, los volúmenes y los registros se acumulan más deprisa de lo previsto.
Conclusión
Un stack Compose fiable descansa menos en la cantidad de YAML que en unos pocos invariantes: un archivo compose.yaml conforme a la especificación actual, imágenes elegidas y actualizadas de forma deliberada, volúmenes identificados, puertos publicados solo donde es estrictamente necesario, secretos separados del repositorio, una copia de seguridad restaurable y un diagnóstico basado en el estado real de los contenedores.
El ciclo normal se vuelve entonces previsible: validar, respaldar, descargar las imágenes, aplicar con docker compose up -d, comprobar la salud y conservar la posibilidad de volver atrás. Es esa disciplina la que convierte un ejemplo de Compose en una base de explotación para los stacks siguientes.
