Volver al índice
root@vps-donweb:~/docs/docker$

Docker

herramienta Docker + Compose
SO Ubuntu 22.04 LTS
categoría Infraestructura
CONCEPTOS Objetos principales de Docker

Docker tiene cuatro conceptos centrales que es importante distinguir antes de empezar:

IMAGEN (Image)
  Plantilla de solo lectura con el sistema de archivos
  y configuración de la app. Se construye desde un Dockerfile.
  Ej: nginx:latest, mysql:8.0, node:20-alpine

CONTENEDOR (Container)
  Instancia en ejecución de una imagen. Liviano, aislado.
  Se puede iniciar, detener, pausar y eliminar.
  Los datos dentro son efímeros (se pierden al eliminarlo).

VOLUMEN (Volume)
  Almacenamiento persistente fuera del contenedor.
  Los datos sobreviven aunque el contenedor se elimine.
  Se usa para bases de datos, uploads, configuraciones.

RED (Network)
  Canal de comunicación entre contenedores.
  Por defecto los contenedores están aislados.
  Con una red en común pueden hablarse por nombre.
FLUJO Ciclo de vida típico
Dockerfile → docker build → Imagen → docker run → Contenedor
                                          ↓
                                     docker push
                                          ↓
                                    Docker Hub / Registry
                                          ↓
                                     docker pull (en el VPS)
                                          ↓
                                     docker run (en el VPS)

En la práctica con Docker Compose el flujo se simplifica: docker compose up hace build + run de todos los servicios de una vez.

INSTALAR Docker Engine en Ubuntu
# 1. Dependencias y repo oficial de Docker
apt update
apt install -y ca-certificates curl gnupg

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

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" \
  | tee /etc/apt/sources.list.d/docker.list > /dev/null

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

# 3. Verificar instalación
docker --version
docker compose version

# 4. Arrancar y habilitar al inicio
systemctl enable docker
systemctl start  docker
POST-INSTALL Usar Docker sin sudo
# Agregar tu usuario al grupo docker
usermod -aG docker $USER

# Aplicar el cambio sin cerrar sesión
newgrp docker

# Verificar que funciona sin sudo
docker run hello-world

# Ver info general del daemon
docker info
docker version

Agregar un usuario al grupo docker es equivalente a darle acceso root. Solo hacerlo con usuarios de confianza.

RUN Crear y ejecutar contenedores
# Ejecutar imagen (la descarga si no existe)
docker run nginx

# En background (detached)
docker run -d nginx

# Con nombre, puerto mapeado y reinicio automático
docker run -d \
  --name mi-nginx \
  -p 8080:80 \
  --restart unless-stopped \
  nginx

# Con variables de entorno
docker run -d \
  --name mi-db \
  -e MYSQL_ROOT_PASSWORD=secreto \
  -e MYSQL_DATABASE=mi_app \
  -p 127.0.0.1:3306:3306 \
  --restart unless-stopped \
  mariadb:10.11

# Interactivo (abre terminal dentro)
docker run -it ubuntu bash

# Ejecutar y eliminar al terminar
docker run --rm alpine echo "hola"

Mapear a 127.0.0.1:3306:3306 en vez de 3306:3306 evita que el puerto quede expuesto públicamente. Solo Nginx u otros procesos locales pueden accederlo.

GESTIÓN Administrar contenedores
# Ver contenedores en ejecución
docker ps

# Ver todos (incluidos los detenidos)
docker ps -a

# Detener / iniciar / reiniciar
docker stop  mi-nginx
docker start mi-nginx
docker restart mi-nginx

# Eliminar contenedor (debe estar detenido)
docker rm mi-nginx
docker rm -f mi-nginx   # forzar aunque esté corriendo

# Ver logs del contenedor
docker logs mi-nginx
docker logs -f mi-nginx          # seguir en tiempo real
docker logs --tail 50 mi-nginx   # últimas 50 líneas

# Ver uso de recursos
docker stats
docker stats mi-nginx

# Ver detalles de configuración
docker inspect mi-nginx
docker inspect mi-nginx | grep IPAddress
EXEC Ejecutar comandos en contenedores
# Abrir shell interactivo en contenedor corriendo
docker exec -it mi-nginx bash
docker exec -it mi-nginx sh    # si no tiene bash

# Ejecutar un comando puntual
docker exec mi-nginx nginx -t
docker exec mi-db mysql -u root -p -e "SHOW DATABASES;"

# Copiar archivos entre host y contenedor
docker cp archivo.conf mi-nginx:/etc/nginx/conf.d/
docker cp mi-nginx:/etc/nginx/nginx.conf ./backup/

# Ver procesos dentro del contenedor
docker top mi-nginx
LIMPIEZA Liberar espacio en disco
# Eliminar contenedores detenidos
docker container prune

# Eliminar imágenes sin usar
docker image prune
docker image prune -a    # incluir todas las no usadas

# Eliminar volúmenes huérfanos
docker volume prune

# Eliminar redes sin usar
docker network prune

# Limpieza total (todo lo no usado)
docker system prune
docker system prune -a --volumes   # incluye imágenes y volúmenes

# Ver cuánto espacio usa Docker
docker system df

docker system prune -a --volumes elimina imágenes cacheadas y volúmenes sin usar. En producción verificá bien qué vas a borrar.

IMÁGENES Gestionar imágenes
# Buscar imagen en Docker Hub
docker search nginx

# Descargar imagen
docker pull nginx
docker pull nginx:1.25          # versión específica
docker pull nginx:alpine        # variante Alpine (más liviana)

# Listar imágenes locales
docker images
docker image ls

# Eliminar imagen
docker rmi nginx
docker rmi nginx:alpine

# Ver historial de capas de una imagen
docker history nginx

# Etiquetar imagen
docker tag mi-app:latest mi-app:v1.2.0

# Subir imagen a Docker Hub
docker login
docker push usuario/mi-app:latest
DOCKERFILE Construir imágenes propias

El Dockerfile define las instrucciones para construir la imagen. Archivo en la raíz del proyecto.

# Dockerfile para app Node.js
FROM    node:20-alpine          # imagen base

WORKDIR /app                    # directorio de trabajo

# Copiar dependencias primero (caché de capas)
COPY    package*.json ./
RUN     npm ci --only=production

# Copiar el resto del código
COPY    . .

# Usuario no root por seguridad
RUN     addgroup -S appgroup && adduser -S appuser -G appgroup
USER    appuser

EXPOSE  3000                    # documentar el puerto

CMD     ["node", "server.js"]   # comando al iniciar
# Dockerfile para app Python / FastAPI
FROM    python:3.12-slim

WORKDIR /app

COPY    requirements.txt .
RUN     pip install --no-cache-dir -r requirements.txt

COPY    . .

EXPOSE  8000
CMD     ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Dockerfile para PHP con Nginx integrado
FROM    php:8.2-fpm-alpine

RUN     docker-php-ext-install pdo pdo_mysql

WORKDIR /var/www/html
COPY    . .

RUN     chown -R www-data:www-data /var/www/html
BUILD Construir y optimizar imágenes
# Build básico (desde la carpeta con el Dockerfile)
docker build -t mi-app .

# Con tag de versión
docker build -t mi-app:1.0.0 .

# Sin caché (reconstruir todo)
docker build --no-cache -t mi-app .

# Especificar Dockerfile alternativo
docker build -f Dockerfile.prod -t mi-app:prod .

# Build con argumentos
docker build --build-arg NODE_ENV=production -t mi-app .

# .dockerignore — excluir archivos del contexto de build
# .dockerignore
node_modules
.git
.env
*.log
.DS_Store
dist
coverage

Siempre crear un .dockerignore. Sin él, node_modules o .git se incluyen en el contexto de build, haciéndolo lento e innecesariamente grande.

MULTI-STAGE Build multi-etapa para imágenes pequeñas

Separa el entorno de compilación del de ejecución. La imagen final solo contiene lo necesario para correr la app — sin compiladores, headers ni dependencias de desarrollo.

# Dockerfile multi-stage para Node.js
## Etapa 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN  npm ci
COPY . .
RUN  npm run build

## Etapa 2: producción (solo lo necesario)
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN  npm ci --only=production
COPY --from=builder /app/dist ./dist   # solo los archivos compilados
USER node
CMD  ["node", "dist/server.js"]

Una imagen Node.js multi-stage puede pasar de 1GB a menos de 200MB. Menos superficie de ataque, menos tiempo de deploy.

VOLÚMENES Tipos de almacenamiento persistente
TIPO 1: Named volumes (recomendado para datos)
  Docker gestiona la ubicación en /var/lib/docker/volumes/
  Portables, fáciles de hacer backup
docker run -v mi-volumen:/var/lib/mysql mariadb

TIPO 2: Bind mounts (recomendado para código y configs)
  Monta una carpeta del host directamente
docker run -v /var/www/mi-app:/app mi-app
docker run -v $(pwd):/app mi-app   # carpeta actual

TIPO 3: tmpfs (solo en memoria, temporal)
  Para datos sensibles que no deben persistir en disco
docker run --tmpfs /tmp mi-app
GESTIÓN Comandos de volúmenes
# Crear volumen
docker volume create mi-datos

# Listar volúmenes
docker volume ls

# Inspeccionar volumen (ver su ruta en el host)
docker volume inspect mi-datos

# Eliminar volumen
docker volume rm mi-datos

# Eliminar volúmenes huérfanos
docker volume prune

# Ruta donde Docker guarda los named volumes
ls /var/lib/docker/volumes/
BACKUP Respaldar y restaurar volúmenes
# Backup de un volumen a archivo tar
docker run --rm \
  -v mi-datos:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/mi-datos-$(date +%Y%m%d).tar.gz -C /source .

# Restaurar volumen desde backup
docker run --rm \
  -v mi-datos:/target \
  -v $(pwd):/backup \
  alpine tar xzf /backup/mi-datos-20240101.tar.gz -C /target

# Backup de base de datos MariaDB en contenedor
docker exec mi-db \
  mysqldump -u root -psecreto mi_base \
  > backup-$(date +%Y%m%d).sql

# Restaurar base de datos en contenedor
docker exec -i mi-db \
  mysql -u root -psecreto mi_base \
  < backup-20240101.sql
REDES Tipos de red en Docker
bridge (por defecto)
  Red interna privada. Los contenedores se comunican
  entre sí. Con -p se exponen puertos al host.

host
  El contenedor comparte la red del host directamente.
  Sin aislamiento de red. Mejor performance.
  Solo en Linux.

none
  Sin red. Contenedor completamente aislado.

custom bridge (recomendado)
  Red bridge con nombre. Los contenedores se pueden
  encontrar por nombre (DNS automático entre containers).
GESTIÓN Crear y usar redes
# Crear red personalizada
docker network create mi-red
docker network create --driver bridge app-network

# Listar redes
docker network ls

# Inspeccionar red (ver contenedores conectados)
docker network inspect mi-red

# Conectar contenedor a red al crear
docker run -d --name mi-app \
  --network mi-red \
  mi-imagen

# Conectar contenedor existente a una red
docker network connect mi-red mi-app

# Desconectar
docker network disconnect mi-red mi-app

# Eliminar red
docker network rm mi-red

# Los contenedores en la misma red se encuentran por nombre
# Desde mi-app puedo hacer: curl http://mi-db:3306
# sin saber la IP interna

Crear siempre una red personalizada para tus stacks. En la red bridge por defecto el DNS por nombre no funciona entre contenedores.

COMPOSE Estructura de docker-compose.yml

Compose define y ejecuta stacks multi-contenedor. Un solo archivo describe todos los servicios, redes y volúmenes de la app.

# docker-compose.yml — Stack completo: Nginx + PHP + MariaDB

services:

  # Base de datos
  db:
    image: mariadb:10.11
    container_name: mi-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE:      ${DB_NAME}
      MYSQL_USER:          ${DB_USER}
      MYSQL_PASSWORD:      ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - app-net
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect"]
      interval: 10s
      timeout:  5s
      retries:  5

  # Aplicación PHP
  app:
    build: .
    container_name: mi-app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST:     db           # nombre del servicio como host
      DB_NAME:     ${DB_NAME}
      DB_USER:     ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./src:/var/www/html     # código en bind mount
    networks:
      - app-net

  # Proxy web
  nginx:
    image: nginx:alpine
    container_name: mi-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - app
    networks:
      - app-net

volumes:
  db-data:     # volumen persistente para la DB

networks:
  app-net:     # red interna del stack
VARIABLES Archivo .env para secretos
# .env — valores que Compose carga automáticamente
DB_ROOT_PASSWORD=password_raiz_segura
DB_NAME=mi_base
DB_USER=mi_usuario
DB_PASSWORD=password_usuario_segura
APP_ENV=production
APP_KEY=base64:clave_aleatoria_larga

Agregar .env al .gitignore y al .dockerignore. Nunca comitear credenciales al repositorio.

COMANDOS Gestionar stacks con Compose
# Iniciar todos los servicios (build si es necesario)
docker compose up -d

# Reconstruir imágenes antes de iniciar
docker compose up -d --build

# Ver estado de los servicios
docker compose ps

# Ver logs de todos los servicios
docker compose logs -f
docker compose logs -f nginx    # solo un servicio

# Detener sin eliminar
docker compose stop

# Detener y eliminar contenedores y redes
docker compose down

# Detener y eliminar TODO incluyendo volúmenes
docker compose down -v

# Reiniciar un servicio específico
docker compose restart nginx

# Ejecutar comando en servicio
docker compose exec app bash
docker compose exec db mysql -u root -p

# Escalar un servicio
docker compose up -d --scale app=3

# Ver configuración procesada (con variables expandidas)
docker compose config
OVERRIDE Compose para distintos entornos

Compose carga automáticamente docker-compose.yml y luego docker-compose.override.yml, mergeando ambos. Útil para diferencias entre dev y prod.

# docker-compose.yml — configuración base (compartida)
services:
  app:
    image: mi-app:latest
    environment:
      APP_ENV: production

---
# docker-compose.override.yml — overrides para desarrollo
services:
  app:
    build: .               # build local en dev, no usar image
    volumes:
      - .:/app             # hot reload del código
    environment:
      APP_ENV: development
    ports:
      - "9229:9229"        # puerto de debug de Node
# Especificar archivo explícitamente para producción
docker compose -f docker-compose.yml up -d

# Combinar múltiples archivos
docker compose -f docker-compose.yml \
               -f docker-compose.prod.yml up -d
RECETA Stack Node.js + MariaDB + Nginx en VPS

Setup completo para deploy en producción. Nginx del host maneja SSL y hace proxy al contenedor de la app.

# Estructura del proyecto en el VPS
/var/www/mi-app/
├── docker-compose.yml
├── .env
├── Dockerfile
└── src/
# docker-compose.yml para producción
services:
  db:
    image: mariadb:10.11
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE:      ${DB_NAME}
      MYSQL_USER:          ${DB_USER}
      MYSQL_PASSWORD:      ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - internal
    # Sin "ports:" → la DB no se expone al host

  app:
    build: .
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"   # solo localhost
    depends_on: [db]
    env_file: .env
    networks:
      - internal

volumes:
  db-data:
networks:
  internal:
# Nginx del host hace proxy al contenedor
# /etc/nginx/sites-available/mi-app.conf
server {
    listen 443 ssl http2;
    server_name mi-app.com;

    ssl_certificate     /etc/letsencrypt/live/mi-app.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mi-app.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass         http://127.0.0.1:3000;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
DEPLOY Flujo de actualización sin downtime
# En el VPS — pull del código y rebuild
cd /var/www/mi-app

git pull origin main

docker compose build app

docker compose up -d --no-deps app
# --no-deps: no reiniciar db ni otros servicios
# solo reemplaza el contenedor de app

# Verificar que arrancó bien
docker compose ps
docker compose logs --tail 30 app

# Si algo salió mal, rollback rápido
docker compose down app
git checkout HEAD~1
docker compose up -d --no-deps app

Con --no-deps el contenedor de la base de datos y otros servicios no se tocan durante el deploy de la app.

SEGURIDAD Buenas prácticas en producción
# 1. Nunca correr como root dentro del contenedor
USER node           # en Dockerfile

# 2. Imagen de solo lectura
docker run --read-only mi-app
# en compose:
read_only: true

# 3. Limitar recursos del contenedor
deploy:
  resources:
    limits:
      cpus: '0.5'
      memory: 512M

# 4. No exponer puertos innecesarios al host
# MAL: expone la DB a internet si el firewall falla
ports: ["3306:3306"]
# BIEN: solo accesible desde el host
ports: ["127.0.0.1:3306:3306"]
# MEJOR: sin ports, solo accesible dentro de la red Docker
# (omitir la sección ports para la DB)

# 5. Escanear imagen por vulnerabilidades
docker scout cves mi-app:latest
docker scan mi-app:latest    # alternativa con Snyk

# 6. Política de reinicio apropiada
restart: unless-stopped   # reinicia siempre salvo stop manual
restart: on-failure       # solo si termina con error
MONITOREO Supervisar contenedores en producción
# Ver estado de todos los contenedores
docker compose ps

# Uso de CPU, RAM, red y disco en tiempo real
docker stats
docker stats --no-stream    # snapshot único

# Ver eventos del daemon Docker
docker events
docker events --filter type=container

# Logs con timestamps
docker compose logs -f --timestamps app

# Ver cuánto espacio usa cada imagen/contenedor
docker system df -v

# Healthcheck manual
docker inspect --format='{{.State.Health.Status}}' mi-app

# Notificación si un contenedor se cae (con cron)
docker inspect --format='{{.State.Running}}' mi-app \
  | grep -q false \
  && echo "mi-app caído: $(date)" \
  | mail -s "⚠ Docker alert" tu@email.com