Saltar al contenido
Infrastructure 2026-09-05 22 min de lectura

Docker Compose en un VPS: instalación y despliegue

Docker Compose describe una aplicación de varios contenedores en un archivo YAML. En un VPS permite versionar la arquitectura y recrearla sin una secuencia manual de comandos.

Docker Compose en un VPS: instalación y despliegue
#Docker#Docker Compose#VPS#DevOps
T
Equipo Terranode
Editorial

Instalar Compose v2

Si instalaste Docker desde su repositorio oficial, añade el plugin y verifica:

bash
sudo apt update
sudo apt install -y docker-compose-plugin
docker compose version

Evita tutoriales antiguos que descargan el binario docker-compose v1.

Crear un proyecto

bash
sudo mkdir -p /opt/miapp
sudo chown "$USER":"$USER" /opt/miapp
cd /opt/miapp

Crea compose.yaml:

yaml
services:
  web:
    image: nginx:1.28-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./public:/usr/share/nginx/html:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost"]
      interval: 30s
      timeout: 5s
      retries: 3

Valida y despliega:

bash
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100

Variables y secretos

Compose lee .env para sustitución. Protege el archivo con chmod 600 .env y añádelo a .gitignore. Para secretos críticos, usa archivos montados o un gestor; evita imprimirlos en logs.

Volúmenes y backups

Usa volúmenes nombrados para datos persistentes. No hagas una copia ciega de una base activa: ejecuta pg_dump, mysqldump u otra herramienta consistente. Prueba la restauración.

Actualizar sin improvisar

bash
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m

Fija versiones en lugar de depender siempre de latest. Conserva un procedimiento de rollback y comprueba healthchecks antes de retirar la versión previa.

Comandos útiles

AcciónComando
Ver configuración finaldocker compose config
Ver serviciosdocker compose ps
Seguir logsdocker compose logs -f
Reiniciar unodocker compose restart web
Detener conservando datosdocker compose down
Ver consumodocker stats

Después de aprender Compose puedes instalar Coolify para una experiencia PaaS o Portainer para gestión visual. Empieza con Docker en un VPS si aún no dominas imágenes, redes y volúmenes.

Cómo piensa Docker Compose un proyecto

Compose agrupa servicios, redes y volúmenes bajo un mismo nombre de proyecto. Esa agrupación es lo que permite ejecutar up, logs o down sin administrar cada contenedor por separado. El archivo no contiene los contenedores: declara el estado deseado, y Docker crea o reemplaza lo necesario para aproximarse a él.

Conviene distinguir cuatro elementos antes de escribir un despliegue real:

  • Un servicio describe cómo ejecutar uno o varios contenedores de la misma aplicación.
  • Una imagen contiene el sistema de archivos y el proceso que arrancará el contenedor.
  • Una red permite que los servicios se encuentren por nombre sin publicar todos sus puertos.
  • Un volumen conserva datos fuera de la vida del contenedor.

El nombre del directorio suele convertirse en el nombre del proyecto. Si copias el mismo compose.yaml a /opt/tienda y /opt/tienda-pruebas, Compose crea conjuntos independientes. También puedes fijarlo al ejecutar un comando:

bash
docker compose --project-name tienda-prod up -d
docker compose --project-name tienda-prod ps

Usar un nombre explícito evita sorpresas cuando una automatización cambia de directorio. No uses el mismo nombre para producción y pruebas porque compartirían nombres de red y sería fácil operar el entorno equivocado.

Un compose.yaml preparado para una aplicación real

Un archivo de producción debe limitar exposición, persistir los datos y expresar cómo se comprueba la salud. El siguiente ejemplo une una aplicación web y PostgreSQL. La aplicación solo escucha en loopback para que Nginx del host sea la entrada pública, mientras que la base de datos permanece únicamente en la red interna.

yaml
services:
  app:
    image: ghcr.io/empresa/tienda:1.7.2
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "127.0.0.1:8080:3000"
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  frontend:
  backend:
    internal: true

volumes:
  postgres_data:

La red backend marcada como internal no tiene salida externa directa. El contenedor app participa también en frontend, por lo que puede hablar con el proxy o con otros servicios si se amplía el proyecto. PostgreSQL no incluye ports: app se conecta a db:5432 usando el DNS interno de Docker.

El healthcheck de la aplicación debe probar algo significativo y barato. Una ruta /health puede comprobar que el proceso atiende solicitudes; si además consulta todas las dependencias en cada llamada, una avería de la base puede provocar reinicios en cascada. Decide si necesitas una comprobación de vida, una de disponibilidad o ambas, en vez de copiar el mismo endpoint para todo.

Antes de levantar el ejemplo, crea .env sin subirlo a Git:

bash
umask 077
cat > .env <<'EOF'
POSTGRES_DB=tienda
POSTGRES_USER=tienda_app
POSTGRES_PASSWORD=CAMBIA_ESTA_CLAVE
EOF
printf '.env\n' >> .gitignore
docker compose config --quiet

config --quiet valida la sintaxis sin imprimir al terminal la configuración interpolada. Si ejecutas docker compose config completo, ten presente que puede mostrar valores sensibles y quedar guardado en el historial de una herramienta de soporte.

Redes y puertos: qué queda expuesto de verdad

expose documenta un puerto para otros contenedores; ports lo publica en el host. No hace falta declarar ninguno para que dos servicios de la misma red se comuniquen. Este detalle permite mantener Redis, PostgreSQL o una API interna fuera de internet.

Hay una diferencia importante entre estas dos publicaciones:

yaml
ports:
  - "8080:3000"
yaml
ports:
  - "127.0.0.1:8080:3000"

La primera escucha en todas las interfaces del VPS. La segunda solo acepta conexiones originadas en el propio host y es adecuada cuando Nginx hace de proxy reverso. No asumas que UFW compensa una publicación demasiado amplia: Docker administra reglas de red propias. Comprueba el resultado desde otra máquina y revisa las direcciones del host:

bash
sudo ss -ltnp
curl -I http://127.0.0.1:8080/health

Dentro de un contenedor, localhost siempre apunta a ese contenedor. Si app necesita PostgreSQL, la dirección correcta es db, no 127.0.0.1. Cuando aparece connection refused tras migrar a Compose, este error conceptual es más frecuente que una avería del motor.

Volúmenes, bind mounts y permisos

Usa volúmenes nombrados para datos administrados por un servicio y bind mounts cuando necesitas controlar un archivo concreto desde el host. Un volumen como postgres_data queda en el área de Docker y sobrevive a recreaciones. Un montaje ./nginx.conf:/etc/nginx/nginx.conf:ro hace visible un archivo del proyecto dentro del contenedor.

No montes el directorio completo del código en producción si la imagen ya lo contiene. Un bind mount como .:/app tapa los archivos incorporados durante la construcción y convierte el despliegue en dependiente del contenido accidental del VPS. Ese patrón es útil en desarrollo con recarga en caliente, no como sustituto de una imagen versionada.

Para revisar qué almacenamiento usa cada servicio:

bash
docker compose config
docker volume ls
docker volume inspect tienda-prod_postgres_data
docker compose exec db sh -c 'du -sh /var/lib/postgresql/data'

Los problemas de permisos aparecen cuando el proceso del contenedor usa un UID que no coincide con el propietario del directorio del host. No respondas con chmod -R 777: averigua el UID ejecutado, asigna el propietario correcto y concede solo el acceso necesario.

bash
docker compose exec app id
stat -c '%u:%g %a %n' ./datos

Backups consistentes de una base en Compose

Copiar el directorio de un volumen mientras la base escribe puede producir una copia incoherente. Usa la herramienta del motor para generar un volcado lógico y sácalo del VPS. Para PostgreSQL:

bash
mkdir -p backups
docker compose exec -T db pg_dump \
  -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc \
  > "backups/tienda-$(date +%F-%H%M).dump"
ls -lh backups/

La restauración debe ensayarse en una base distinta, no sobre producción:

bash
docker compose exec -T db createdb -U "$POSTGRES_USER" tienda_restore
docker compose exec -T db pg_restore \
  -U "$POSTGRES_USER" -d tienda_restore --clean --if-exists \
  < backups/tienda-FECHA.dump

Un respaldo que solo existe en /opt/miapp/backups se pierde junto con el disco del VPS. Cópialo cifrado a otra ubicación y registra cuánto tarda la restauración. Ese tiempo, no la mera existencia del archivo, determina si tu recuperación cumple lo que el negocio espera.

Desplegar una versión y poder volver atrás

El rollback más simple consiste en conservar una imagen anterior identificable y no ejecutar migraciones destructivas sin un plan compatible. Etiquetas como 1.7.2 son preferibles a latest, porque permiten saber exactamente qué binario corre.

Antes del cambio, registra el estado:

bash
docker compose ps
docker compose images
docker compose logs --since=15m > /tmp/tienda-antes.log

Cambia la etiqueta en compose.yaml, valida y despliega:

bash
docker compose config --quiet
docker compose pull app
docker compose up -d --no-deps app
docker compose ps
docker compose logs --since=5m app
curl -fsS http://127.0.0.1:8080/health

--no-deps evita recrear la base cuando solo cambió la aplicación. Si la prueba falla, restaura la etiqueta anterior y repite docker compose up -d --no-deps app. Esto no deshace una migración de esquema incompatible: las migraciones requieren su propia estrategia, por ejemplo cambios aditivos primero y eliminación de columnas en una entrega posterior.

No ejecutes docker image prune -a inmediatamente después de actualizar. Si eliminas la imagen anterior, conviertes un rollback rápido en una descarga que puede fallar justo durante el incidente.

Leer estado y logs sin reiniciar por reflejo

El estado Up solo indica que el proceso principal sigue vivo; no demuestra que la aplicación responda correctamente. Combina la vista de Compose con una petición al servicio y con los logs del periodo afectado.

bash
docker compose ps
docker compose top
docker compose logs --tail=200 --timestamps app
docker compose exec app sh -c 'wget -qO- http://127.0.0.1:3000/health'
docker stats --no-stream

Si un contenedor reinicia repetidamente, observa el código de salida antes de recrearlo:

bash
docker inspect -f '{{.State.ExitCode}} {{.State.Error}} {{.RestartCount}}' tienda-prod-app-1
docker compose logs --tail=300 app

Un código 137 suele acompañar una terminación forzada, a menudo por falta de memoria, pero debe confirmarse en los registros del kernel. Un error de configuración puede producir un ciclo igual de visible. Comprueba el motivo en lugar de aumentar RAM de inmediato.

Fallos habituales de Docker Compose en un VPS

SíntomaComprobaciónCorrección probable
variable is not setdocker compose configCrear .env o corregir el nombre
La app no encuentra localhost:5432Revisar DATABASE_URLUsar el servicio db:5432
Datos desaparecen tras recreardocker inspect y docker volume lsMontar un volumen en la ruta real del motor
Puerto ocupadosudo ss -ltnpLiberar el puerto o cambiar la publicación
Contenedor unhealthyInspeccionar salida del healthcheckCorregir comando, ruta o tiempo de arranque
Disco crece sin tráficodocker system df y tamaño de logsRotar logs y limpiar imágenes tras verificar rollback
Cambios del YAML no aparecendocker compose up -d --force-recreateRecrear solo los servicios afectados

Cuando Compose no sabe cuál archivo usar, indica la ruta para evitar operar otro proyecto:

bash
docker compose -f /opt/tienda/compose.yaml config --quiet
docker compose -f /opt/tienda/compose.yaml ps

Seguridad y límites en un único servidor

Compose mejora la reproducibilidad, no crea una frontera de seguridad equivalente a una máquina virtual. Los contenedores comparten el kernel del VPS y el acceso al socket Docker concede control del host. Evita privileged: true, montajes de / y capacidades que la aplicación no necesita.

Cuando la imagen lo permite, endurece el servicio:

yaml
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

Estas opciones no son universales: una aplicación que escribe en rutas internas necesitará volúmenes o tmpfs específicos. Aplícalas en pruebas y verifica funciones como carga de archivos, generación de caché y envío de trabajos.

En un solo VPS, Compose sigue teniendo un punto único de fallo. Puedes reducir el tiempo de recuperación con un archivo versionado, imágenes disponibles y copias externas, pero no obtienes alta disponibilidad. Si el proyecto supera esa tolerancia, la decisión ya no es añadir otra sección al YAML: necesitas distribuir la carga o contratar una plataforma que opere los nodos.

Separar producción, pruebas y tareas puntuales

El mismo archivo base puede servir a varios entornos si las diferencias quedan declaradas y revisables. No copies compose.yaml completo para cambiar dos puertos: las copias divergen y un arreglo de seguridad termina aplicado solo a una. Usa un archivo adicional cuya intención sea explícita.

yaml
# compose.staging.yaml
services:
  app:
    image: ghcr.io/empresa/tienda:1.7.3-rc1
    ports:
      - "127.0.0.1:18080:3000"
    environment:
      APP_ENV: staging

  db:
    volumes:
      - postgres_staging:/var/lib/postgresql/data

volumes:
  postgres_staging:

Valida la combinación antes de iniciarla:

bash
docker compose -f compose.yaml -f compose.staging.yaml config --quiet
docker compose -f compose.yaml -f compose.staging.yaml \
  --project-name tienda-staging up -d

El nombre distinto del proyecto y el volumen distinto evitan que una prueba use la base de producción. Revisa la configuración resuelta, porque mapas y listas no siempre se combinan como alguien imagina: el último archivo puede reemplazar valores completos.

Los perfiles sirven para herramientas que no deben arrancar siempre. Por ejemplo, una consola administrativa:

yaml
services:
  adminer:
    image: adminer:5
    profiles: ["debug"]
    ports:
      - "127.0.0.1:18081:8080"
    networks:
      - backend
bash
docker compose --profile debug up -d adminer
docker compose --profile debug stop adminer

No dejes el perfil de diagnóstico activo después del incidente. Que escuche en loopback reduce exposición, pero sigue teniendo acceso a la red de base.

Ejecutar migraciones y trabajos únicos sin crear servicios permanentes

Una migración debe ejecutarse con la misma imagen y variables de la aplicación, pero no como un contenedor que reinicia indefinidamente. run --rm crea una ejecución puntual:

bash
docker compose run --rm app node dist/migrate.js
docker compose run --rm app node dist/check-data.js

Antes confirma el backup y compatibilidad con la versión anterior. Si la migración elimina columnas que el código viejo necesita, cambiar otra vez la etiqueta no constituye rollback.

Para tareas programadas, decide si el cron vive en el host o en un servicio dedicado. No pongas el mismo cron dentro de cada réplica de la aplicación: si escalas a tres contenedores, la tarea se ejecutará tres veces. Un servicio de job con bloqueo distribuido o una cola hace explícita esa responsabilidad.

yaml
services:
  mantenimiento:
    image: ghcr.io/empresa/tienda:1.7.2
    profiles: ["jobs"]
    env_file: [.env]
    networks: [backend]
    command: ["node", "dist/maintenance.js"]
bash
docker compose --profile jobs run --rm mantenimiento

Registra salida y código de retorno desde el programador. Un contenedor que termina con cero puede eliminarse; uno que falla necesita conservar logs suficientes para saber qué operación quedó incompleta.

Comprobar qué cambió antes de recrear servicios

Compose no ofrece una revisión humana de intención: ejecutará la definición resuelta. Guarda la configuración normalizada sin secretos en un entorno seguro y compara los cambios de Git. Presta atención a volúmenes, comandos, puertos y nombres de imagen.

bash
git diff -- compose.yaml compose.staging.yaml
docker compose config --images
docker compose config --services
docker compose pull --policy missing

Un cambio que convierte un volumen nombrado en una ruta distinta puede levantar una base vacía sin borrar el volumen antiguo. Antes de declarar pérdida, inspecciona el inventario y los montajes de ambos contenedores. Del mismo modo, cambiar container_name puede provocar conflictos entre entornos y elimina parte de la flexibilidad de los nombres administrados por Compose; úsalo solo si existe una dependencia externa justificada.

Documentar dependencias externas que Compose no crea

El YAML solo reconstruye los recursos declarados; DNS, almacenamiento externo, reglas del proveedor y credenciales pueden quedar fuera. Añade al repositorio un inventario sin secretos que indique dominio, nombre de bucket, política de firewall, ubicación de backups y responsable. Si el VPS se pierde, esa lista evita descubrir dependencias durante la restauración.

Prueba el procedimiento en un directorio limpio con otro nombre de proyecto. Descarga las imágenes, crea .env desde el gestor seguro, restaura una base de prueba y ejecuta una acción de usuario. Registra qué pasos no estaban en Compose y decide si deben automatizarse o documentarse.

También verifica arquitectura de la imagen. Un respaldo perfecto no ayudará si la etiqueta fue eliminada del registro. Conserva artefactos necesarios o una forma probada de reconstruirlos desde el commit y dependencias fijadas.

Asignar recursos sin confundir límites con capacidad

Los límites protegen al host de un servicio descontrolado, pero no crean CPU ni RAM. Antes de fijarlos observa el consumo durante carga y despliegue. En un archivo Compose para un solo motor puedes declarar:

yaml
services:
  app:
    mem_limit: 768m
    cpus: 1.5
    pids_limit: 200

Comprueba que la versión de Compose interpreta las claves en tu modo de ejecución mediante docker compose config y luego inspecciona el contenedor. No copies únicamente deploy.resources de ejemplos de Swarm esperando que todos sus límites se apliquen igual en un motor local.

bash
docker compose up -d app
docker inspect tienda-prod-app-1 \
  --format 'memory={{.HostConfig.Memory}} cpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}'

Si la aplicación alcanza memoria máxima, puede terminar y reiniciar; revisa OOMKilled y el kernel. Si alcanza CPU, se ralentiza y puede superar timeouts sin caer. Deja margen para el proxy, el sistema, Docker y la base, y no asignes a todos los servicios el 100 % de la máquina simultáneamente.

Después de ajustar, repite una operación real y observa docker stats. El objetivo es contener un fallo sin degradar permanentemente la carga legítima. Documenta por qué existe cada cifra y en qué medición se basó.

Nombrar y etiquetar recursos para saber a quién pertenecen

En un VPS con varios proyectos, nombres y etiquetas evitan limpiar el recurso equivocado. Compose ya aplica etiquetas de proyecto y servicio. Consúltalas antes de borrar:

bash
docker ps --filter label=com.docker.compose.project=tienda-prod
docker volume ls --filter label=com.docker.compose.project=tienda-prod
docker network ls --filter label=com.docker.compose.project=tienda-prod

No fijes container_name solo para obtener nombres bonitos: impide escalar un servicio a varias réplicas y puede chocar con otro proyecto. Los nombres generados expresan proyecto, servicio e instancia y son suficientes para la mayoría de operaciones.

Añade etiquetas propias cuando una automatización necesita responsable, entorno o política de respaldo. No almacenes secretos en etiquetas; son visibles mediante inspección. Una convención breve permite construir inventarios y alertas sin inferir propiedad por directorios o nombres históricos.

Compose convierte el despliegue en una definición revisable. Valida el YAML, limita los puertos, separa secretos, persiste datos y diseña actualizaciones y backups antes de llamar “producción” al proyecto.

¿Listo para llevar tu sitio al siguiente nivel?

Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.

Ver planes de hosting

Preguntas frecuentes