Despliegue con Docker Compose en VPS
TUTORIAL 8 min read fordnox

Despliegue con Docker Compose en VPS

Guía completa para desplegar aplicaciones con Docker Compose en un VPS. Desde la instalación hasta configuraciones multi-contenedor con redes y volúmenes.


Despliegue con Docker Compose en VPS

Docker Compose es la forma más sencilla de desplegar y gestionar aplicaciones en tu VPS. Si buscas el mejor VPS para Docker, consulta primero nuestra comparativa. Luego define toda tu stack en un solo archivo, despliega con un comando y actualiza sin tiempo de inactividad.

Por qué Importa Esto

Los despliegues tradicionales son un dolor de cabeza:

Docker Compose resuelve esto:

Requisitos Previos

Paso 1: Instalar Docker

# Eliminar versiones antiguas
sudo apt remove docker docker-engine docker.io containerd runc

# Instalar dependencias
sudo apt update
sudo apt install ca-certificates curl gnupg -y

# Añadir la clave GPG de Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

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

# Instalar Docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

# Añadir tu usuario al grupo docker (requiere cerrar y volver a iniciar sesión)
sudo usermod -aG docker $USER

# Verificar la instalación
docker --version
docker compose version

Cierra sesión y vuelve a iniciar para que los cambios de grupo surtan efecto.

Paso 2: Crear la Estructura de tu Proyecto

mkdir -p ~/apps/myapp
cd ~/apps/myapp

Estructura recomendada:

myapp/
├── docker-compose.yml      # Archivo compose principal
├── docker-compose.prod.yml # Overrides de producción
├── .env                    # Variables de entorno (¡nunca confirmes en git!)
├── .env.example            # Plantilla para variables de entorno
├── nginx/
│   └── nginx.conf          # Configuración personalizada de Nginx
├── data/                   # Datos persistentes (en .gitignore)
└── logs/                   # Logs de la aplicación (en .gitignore)

Paso 3: Escribir tu Primer docker-compose.yml

Vamos a desplegar una stack web completa:

# docker-compose.yml
services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./src:/app
      - /app/node_modules
    command: npm start
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - internal

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

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - internal

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: unless-stopped
    networks:
      - internal

volumes:
  postgres_data:
  redis_data:

networks:
  internal:
    driver: bridge

Paso 4: Usar Variables de Entorno Correctamente

Crea tu archivo .env:

# .env
DB_PASSWORD=your-super-secret-password-here
REDIS_PASSWORD=another-secret
API_KEY=your-api-key

Crea .env.example como documentación:

# .env.example
DB_PASSWORD=
REDIS_PASSWORD=
API_KEY=

¡Nunca confirmes .env en git! Añádelo al .gitignore:

echo ".env" >> .gitignore
echo "data/" >> .gitignore
echo "logs/" >> .gitignore

Paso 5: Overrides de Producción

Crea un archivo específico para producción:

# docker-compose.prod.yml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G

Despliega con:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Paso 6: Comandos Comunes de Docker Compose

# Iniciar todos los servicios
docker compose up -d

# Ver logs
docker compose logs -f

# Ver logs de un servicio específico
docker compose logs -f app

# Detener todos los servicios
docker compose down

# Detener y eliminar volúmenes (¡CUIDADO - borra los datos!)
docker compose down -v

# Reconstruir y reiniciar
docker compose up -d --build

# Reiniciar un servicio específico
docker compose restart app

# Ver contenedores en ejecución
docker compose ps

# Ejecutar un comando en el contenedor
docker compose exec app sh

# Ver uso de recursos
docker stats

Paso 7: Despliegues sin Tiempo de Inactividad

Para actualizar sin interrupciones:

# Descargar nuevas imágenes
docker compose pull

# Recrear solo los contenedores modificados
docker compose up -d --no-deps app

O usa actualizaciones graduales con múltiples réplicas:

services:
  app:
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s

Paso 8: Verificaciones de Salud

Añade siempre health checks:

services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Comprueba el estado de salud:

docker compose ps
docker inspect --format='{{json .State.Health}}' container_name

Paso 9: Gestionar Secretos

Para datos sensibles, usa secretos de Docker o gestores de secretos externos:

services:
  app:
    secrets:
      - db_password
      - api_key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Paso 10: Estrategia de Copias de Seguridad

Crea un script de backup:

#!/bin/bash
# backup.sh

BACKUP_DIR="/backups/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Hacer backup de PostgreSQL
docker compose exec -T db pg_dump -U user myapp > "$BACKUP_DIR/db.sql"

# Hacer backup de volúmenes
docker run --rm \
  -v myapp_postgres_data:/data:ro \
  -v "$BACKUP_DIR":/backup \
  alpine tar czf /backup/postgres_data.tar.gz /data

# Hacer backup de Redis
docker compose exec -T redis redis-cli BGSAVE
docker cp "$(docker compose ps -q redis)":/data/dump.rdb "$BACKUP_DIR/"

echo "Backup completado: $BACKUP_DIR"

Ejemplos del Mundo Real

WordPress con Base de Datos

services:
  wordpress:
    image: wordpress:latest
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress_data:/var/www/html
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: ${WP_DB_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped

volumes:
  wordpress_data:
  db_data:

Aplicación JavaScript Full-Stack

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://api:4000
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: pass
      POSTGRES_USER: user
      POSTGRES_DB: app

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Git Autoalojado con Gitea

services:
  gitea:
    image: gitea/gitea:latest
    ports:
      - "3000:3000"
      - "222:22"
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=${DB_PASSWORD}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: gitea
    restart: unless-stopped

volumes:
  gitea_data:
  postgres_data:

Buenas Prácticas

  1. Fija las versiones de las imágenes - Usa postgres:16-alpine, no postgres:latest
  2. Usa archivos .env - Mantén los secretos fuera de los archivos compose (consulta nuestra guía de seguridad para VPS)
  3. Volúmenes con nombre para los datos - No uses bind mounts para bases de datos
  4. Health checks en todos los servicios - Sabe cuándo los servicios están realmente listos
  5. Límites de recursos - Evita que contenedores desbocados maten tu servidor
  6. Límites de logs - Establece max-size para evitar que se llene el disco
  7. Usa redes - Aísla los servicios que no necesitan comunicarse entre sí
  8. depends_on con condiciones - Espera a que los servicios estén sanos, no solo iniciados

Errores Comunes que Debes Evitar

Usar el tag latest - Las builds se vuelven irreproducibles

Almacenar datos en contenedores - Los datos desaparecen cuando se elimina el contenedor

Confirmar archivos .env - Los secretos quedan en el historial de git para siempre

Sin health checks - depends_on no espera a que la app esté lista

Ignorar los logs - Llenarán tu disco sin límites configurados

Exponer puertos de base de datos - Solo expón lo que necesita acceso externo

Ejecutar como root - Usa la directiva USER en los Dockerfiles

Sin política de reinicio - Los contenedores no vuelven tras un fallo

Consejos para Depurar

# Ver por qué falla un contenedor
docker compose logs app --tail=100

# Abrir una shell en un contenedor en ejecución
docker compose exec app sh

# Abrir una shell en un contenedor detenido
docker compose run app sh

# Inspeccionar detalles del contenedor
docker inspect $(docker compose ps -q app)

# Comprobar conectividad de red
docker compose exec app ping db

# Ver variables de entorno
docker compose exec app env

Preguntas Frecuentes

¿Cuánta RAM necesito?

Para proyectos pequeños, 2 GB suele ser suficiente. Cada contenedor tiene su overhead, así que calcula ~100 MB por contenedor más las necesidades reales de la app. Los planes de Hostinger VPS comienzan en 4 GB, lo que maneja la mayoría de las stacks con comodidad.

¿Debo usar Docker Compose o Kubernetes?

Docker Compose para despliegues en un solo servidor (la mayoría de la gente). Kubernetes cuando necesitas clústeres multi-nodo, autoescalado o tienes un equipo de DevOps dedicado. No lo compliques más de lo necesario.

¿Cómo actualizo una aplicación en ejecución?

# Descargar las últimas imágenes
docker compose pull
# Recrear los contenedores modificados
docker compose up -d

Para builds personalizadas: docker compose up -d --build

¿Puedo usar Docker Compose con Nginx Proxy Manager?

¡Sí! No expongas los puertos directamente, simplemente pon los contenedores en la misma red que NPM. Consulta nuestra guía de proxy inverso.

¿Cómo persisto los datos?

Usa volúmenes con nombre (Docker gestiona la ubicación) o bind mounts (tú especificas la ruta). Los volúmenes con nombre son los recomendados para bases de datos.

¿Cuál es la diferencia entre up y start?

up crea e inicia los contenedores. start solo inicia contenedores existentes que estén detenidos. Usa siempre up -d.


Próximos pasos: Configura copias de seguridad automatizadas para proteger tus datos de Docker y añade monitorización para controlar la salud de los contenedores.

~/docker-compose-vps-guide/get-started

Ready to get started?

Get the best VPS hosting deal today. Hostinger offers 4GB RAM VPS starting at just $4.99/mo.

Get Hostinger VPS — $4.99/mo

// up to 75% off + free domain included

// related topics

Docker Compose VPS Docker despliegue de contenedores tutorial Docker despliegue en VPS

// related guides

Andrius Putna

Andrius Putna

I am Andrius Putna. Geek. Since early 2000 in love tinkering with web technologies. Now AI. Bridging business and technology to drive meaningful impact. Combining expertise in customer experience, technology, and business strategy to deliver valuable insights. Father, open-source contributor, investor, 2xIronman, MBA graduate.

// last updated: February 6, 2026. Disclosure: This article may contain affiliate links.