Volver al blog
Agosto 19, 2026Guías

Cómo desplegar aplicaciones con Docker Compose en un VPS

Guía práctica de producción de Docker Compose v2 en Ubuntu: instalar el motor, escribir un fichero Compose con redes y volúmenes, gestionar secretos, poner un proxy inverso delante, actualizar sin pánico y hacer copias de los volúmenes con nombre.

Cómo desplegar aplicaciones con Docker Compose en un VPS

Docker Compose es la forma habitual de ejecutar una pila pequeña en un VPS: una aplicación web, una base de datos, una caché Redis y un proxy inverso, descritos en un fichero YAML. No es Kubernetes. No tendrás conmutación multi-nodo. Tendrás un montaje repetible y revisable que puedes recrear en minutos en un VPS Hiddence nuevo: exactamente lo que necesitan la mayoría de proyectos paralelos y productos pequeños.

Este artículo asume que quieres algo más cercano a producción que un docker run un viernes por la noche. Instalaremos Docker Engine y el plugin Compose, crearemos un usuario de despliegue sin root, escribiremos un fichero Compose con healthchecks y políticas de reinicio, mantendremos los secretos fuera de Git, publicaremos en internet solo los puertos del proxy inverso y definiremos una rutina de actualización y copia de seguridad. La pila de ejemplo es una aplicación web típica más PostgreSQL más Caddy, pero el mismo patrón sirve para Node, PHP, Python o un conjunto de workers.

Por qué Compose en un solo VPS sigue teniendo sentido

La gente salta a Kubernetes porque está de moda y luego se pasa el fin de semana con YAML para un sitio web. Compose se sigue leyendo. Versionas el fichero, documentas las variables de entorno y puedes hacer un diff del cambio antes de aplicarlo. El aislamiento basta: un contenedor de aplicación comprometido no debería tener el socket UNIX de la base si usaste redes y usuarios con el mínimo privilegio. Los límites de recursos impiden que una fuga de memoria congele todo el VPS.

  • Un fichero describe toda la pila
  • Los volúmenes con nombre sobreviven a recrear contenedores
  • Las redes internas de Docker mantienen Postgres fuera de internet pública
  • restart: unless-stopped cubre la mayoría de los reinicios
  • Fácil de copiar a un segundo VPS cuando te quedas pequeño en el primero

Requisitos

Usa un VPS con RAM suficiente para la aplicación más Postgres. Un plan de 2 GB es un mínimo realista para aplicación + base + proxy. Se asume Ubuntu 24.04. No instales Docker desde un Snap cualquiera sin leer qué hace con los cgroups; los pasos siguientes usan el repositorio apt oficial de Docker.

  • Ubuntu 22.04/24.04, root o sudo
  • Se recomiendan 2 GB de RAM (1 GB solo para pilas minúsculas sin Postgres)
  • Un dominio que apunte al VPS si quieres HTTPS automático
  • Git u otra forma de copiar el proyecto al servidor

Paso 1: Instalar Docker Engine y Compose v2

Compose v2 es un plugin que se invoca como docker compose (con un espacio), no el antiguo binario Python docker-compose. Instala ambos desde el repositorio de Docker para recibir parches de seguridad actuales.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

Paso 2: Usuario de despliegue y estructura de directorios

Ejecutar Compose como root funciona, pero un usuario dedicado en el grupo docker es más limpio. Pon el proyecto en /opt o /srv, no en /root, para que copias y permisos queden claros. Nunca dejes con escritura mundial el directorio que contiene .env.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Paso 3: Escribir un fichero Compose pensado para producción

El fichero de abajo es una plantilla. Sustituye la imagen de la aplicación por la tuya. Postgres no se publica en 0.0.0.0:5432: solo Caddy se publica en 80 y 443. Los healthchecks evitan que un proxy inverso envíe tráfico a una aplicación que aún está migrando la base. Fija las etiquetas de imagen; latest es cómo ocurren las roturas por sorpresa.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

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

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Paso 4: Secretos, .env y Caddyfile

Pon las contraseñas en .env en el servidor, chmod 600, y nunca hagas commit de ese fichero. Genera contraseñas con openssl rand -base64 32. El Caddyfile solo necesita hacer proxy inverso hacia el nombre de servicio de la aplicación en la red Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Paso 5: Cortafuegos, registros y límites de recursos

UFW solo debe permitir 22, 80 y 443. Docker a veces elude UFW en los puertos publicados; si necesitas un cortafuegos de host estricto, consulta la interacción actual Docker + UFW de tu versión de Ubuntu o publica puertos solo en 127.0.0.1 y pon un Caddy de host delante. Define límites de memoria en Compose para que Postgres no se coma el VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Paso 6: Actualizaciones y reversiones

La actualización aburrida que funciona: descargar imágenes nuevas, up -d, vigilar healthchecks, conservar la etiqueta anterior en git para poder volver a fijarla. No hagas docker compose down en una base de producción salvo que pretendas dejar de aceptar tráfico. down -v borra volúmenes: eso es un borrado, no un reinicio.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Copias de los volúmenes con nombre

Una instantánea del VPS está bien. Un volcado lógico de Postgres es mejor si necesitas restaurar en otra máquina. Prográmalo con cron. Prueba una restauración una vez; si no, no tienes copias de seguridad, tienes ficheros que esperas que lo sean.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Resolución de problemas

Lee docker compose ps y docker compose logs service. Los fallos al descargar imágenes suelen ser límites de tasa de ghcr/Docker Hub o una imagen privada sin inicio de sesión. Un 502 de Caddy significa que la aplicación no está sana o que el nombre de host del proxy es incorrecto (usa el nombre de servicio de Compose, no localhost, desde el contenedor Caddy). Los errores de permisos en volúmenes suelen significar que la imagen corre como uid 1000 mientras el directorio del host es de root.

  • compose: command not found — instalaste el nombre antiguo del binario; usa docker compose
  • port is already allocated — otra cosa posee 80/443 (Apache, Nginx, otro Caddy)
  • database connection refused — la aplicación arrancó antes de que Postgres estuviera listo; usa healthcheck + condición depends_on
  • disk full — docker system df, luego prune de imágenes no usadas con cuidado
  • permission denied on docker.sock — el usuario no está en el grupo docker, o necesitas una sesión de acceso nueva

Seguridad

No publiques puertos de base de datos. No uses privileged: true. No montes /var/run/docker.sock en un contenedor de aplicación salvo que estés escribiendo a propósito un gestor de contenedores. Mantén el motor actualizado. Analiza tus propias imágenes si las construyes. .env en disco sigue siendo un fichero secreto: restringe quién puede entrar por SSH a la máquina.

  • Sin 5432 / 3306 / 6379 públicos
  • Fija digests de imagen o al menos etiquetas inmutables
  • chmod 600 .env
  • Unattended-upgrades del sistema anfitrión sigue importando
  • Un proyecto Compose por aplicación reduce el radio de impacto

Consejos

  • Pon compose.yaml en git; guarda .env.example sin secretos reales
  • Usa perfiles para workers opcionales (docker compose --profile workers up -d)
  • Observa docker stats cuando dimensionas el VPS
  • Los bind mounts del código fuente no son el patrón de producción: hornea una imagen
  • Si la pila crece a varios VPS, entonces mira orquestación, no antes

Un montaje Compose casi de producción en un VPS es: paquetes oficiales de Docker, un .env protegido, un YAML que no publica la base, healthchecks, un proxy inverso en 80/443, UFW, etiquetas de imagen a las que puedes volver y volcados de Postgres que has restaurado de verdad una vez. Parte de la plantilla de este artículo, sustituye la imagen de la aplicación y trata docker compose down -v como un comando destructivo, con el mismo respeto que rm -rf.