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.
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.
# 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
# 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.
# 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.
# 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
# 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
# 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.
# 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
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 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.
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.
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
# 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 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
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).
# 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 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
# .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.
# 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
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
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; } }
# 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.
# 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
# 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