Docker para Desarrolladores — Lo Esencial
Docker no elimina los bugs, pero elimina una clase entera de diferencias entre entornos. Esta guía cubre lo que uso a diario: imágenes y contenedores, Dockerfile, Compose, datos, redes, seguridad y operación, con los errores que más cuestan.
Qué es Docker y qué problema resuelve
Docker empaqueta una aplicación con sus dependencias en una imagen que se ejecuta igual en local, en CI y en el servidor. Resuelve el clásico "en mi máquina funciona" sin obligarte a mantener una máquina virtual por servicio.
| Aspecto | Contenedor | Máquina virtual |
|---|---|---|
| Arranque | Segundos | Minutos |
| Aislamiento | Proceso | Sistema completo |
| Tamaño | Megabytes | Gigabytes |
| Kernel | Compartido con el host | Propio |
| Uso típico | Servicios, CI, desarrollo | Aislamiento fuerte u otro sistema operativo |
Imagen, contenedor, volumen y red
Una imagen es el paquete inmutable: sistema base, dependencias y código. Un contenedor es una instancia en ejecución. Si la imagen es la receta, el contenedor es el plato. Un volumen es almacenamiento que sobrevive al contenedor, y una red permite que los servicios se encuentren por nombre sin exponer puertos innecesarios.
Esa distinción explica por qué reconstruir una imagen cambia el contenedor y por qué los datos escritos dentro de un contenedor desaparecen al recrearlo si no están en un volumen.
Primeros comandos
# Construir una imagen
docker build -t mi-app .
# Ejecutar un contenedor
docker run -d -p 3000:3000 --name app mi-app
# Ver logs
docker logs -f app
# Ejecutar shell dentro del contenedor
docker exec -it app sh
# Listar contenedores e imágenes
docker ps
docker images
# Detener y eliminar un contenedor
docker stop app
docker rm app
Dockerfile: capas, multi-stage y .dockerignore
Un Dockerfile se construye por capas. El orden importa: copiar primero package.json y el lockfile permite reutilizar la capa de dependencias cuando solo cambia el código. Copiar todo antes de npm ci invalida la caché en cada commit.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]
El multi-stage separa compilación y ejecución, deja una imagen final más pequeña y evita llevar las dependencias de build a producción. Un .dockerignore con node_modules, .git y archivos locales acelera el build y reduce el contexto.
Docker Compose: varios servicios sin dolor
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
api:
build: ./api
ports:
- "3000:3000"
depends_on:
- db
volumes:
pgdata:
Con un solo docker compose up -d despliego PostgreSQL, Redis, n8n, una API NestJS y un dashboard React. Un docker compose down detiene los servicios, pero el volumen pgdata conserva los datos.
Datos y secretos
Los datos persistentes van en volúmenes; los secretos, en variables de entorno o en un gestor de secretos. Nunca en la imagen ni en el repositorio. Un archivo .env sirve para desarrollo, pero debe estar en .gitignore. Si el contenedor de PostgreSQL no tiene volumen, un docker compose down puede borrar la base de datos.
Redes y reverse proxy
En Compose, los servicios se encuentran por su nombre: la API se conecta a db:5432, no a localhost. Esa red interna evita exponer la base de datos al exterior; solo el servicio que necesita un puerto público lo publica. En producción, un reverse proxy o la plataforma termina TLS y reenvía al contenedor; el contenedor no debería saber si está detrás de Nginx, Caddy o un túnel.
Build cache y CI/CD
En CI construyo la imagen una vez, la etiqueto con el SHA del commit y la promuevo entre entornos. Así lo que se prueba es exactamente lo que se despliega. La caché de capas acelera los builds, pero solo si el orden del Dockerfile la respeta. Para dependencias pesadas, un caché de BuildKit o un registro intermedio evita reconstruir todo en cada pipeline.
Seguridad básica
No ejecutes contenedores como root, no publiques la base de datos y mantén las imágenes base actualizadas. Un escaneo con Trivy en CI detecta vulnerabilidades conocidas antes de que lleguen a producción. Los secretos no van en el Dockerfile: si necesitas pasarlos en build, usa BuildKit secrets en lugar de ARG, porque los argumentos quedan en el historial de la imagen.
Operación y diagnóstico
En producción reviso logs, uso healthchecks y configuro una política de reinicio. Compose permite declarar restart: unless-stopped y un healthcheck que consulta un endpoint. Si el contenedor falla, se reinicia; si no responde, se marca como unhealthy. docker stats muestra CPU y memoria, docker system df revela el espacio usado y docker system prune limpia lo que ya no se usa.
Errores comunes y cómo diagnosticarlos
| Síntoma | Causa probable | Acción |
|---|---|---|
| Puerto ocupado | Otro proceso usa el puerto | Cambia el mapeo o detén el proceso |
| El contenedor reinicia | Falla el proceso o el healthcheck | Revisa docker logs y el comando |
| No persisten datos | Falta un volumen | Declara el volumen en Compose |
| Permiso denegado | UID distinto al del host | Ajusta usuario o permisos |
| Imagen enorme | Dependencias de build y capas | Multi-stage, Alpine y .dockerignore |
| Arquitectura incorrecta | Imagen ARM en un servidor x86 | Construye con --platform |
Docker en desarrollo
En local uso bind mounts para montar el código dentro del contenedor y aprovechar el hot reload. Así el contenedor mantiene las dependencias y el editor sigue trabajando en el host. Para tareas puntuales, docker compose run --rm ejecuta un comando sin dejar contenedores colgando.
No reconstruyo la imagen en cada cambio de código; solo cuando cambian dependencias o el Dockerfile. Esa separación ahorra tiempo y evita builds innecesarios.
Compose, Swarm o Kubernetes
Compose es suficiente para un VPS con pocos servicios y un solo host. Si necesitas alta disponibilidad, escalado automático o varios nodos, el siguiente paso es un orquestador. Kubernetes resuelve eso, pero añade complejidad operativa; no lo adoptaría solo porque suena más profesional.
Para muchos proyectos, un VPS con Compose, healthchecks y backups bien probados es más fiable que un clúster mal administrado.
Registros e imágenes
Etiqueto cada imagen con el SHA del commit y la publico en un registro. El servidor hace pull de esa etiqueta exacta, no de latest. Así puedo volver a una versión anterior sin adivinar qué contenía la imagen.
Límites de recursos y logs
En Compose declaro límites de memoria y CPU para que un servicio no consuma el host entero. También configuro el driver de logs con rotación; sin límite, un contenedor ruidoso puede llenar el disco. Son ajustes pequeños que evitan incidentes grandes.
Docker, Podman y otras alternativas
Podman es una alternativa sin daemon y con soporte rootless; para la mayoría de flujos, los comandos son compatibles con Docker. Docker sigue siendo el estándar y tiene el ecosistema más amplio. La elección depende del entorno y de las políticas de seguridad, no de una preferencia personal.
También existen runtimes más ligeros para casos embebidos, pero no sustituyen a Docker cuando necesitas imágenes portables, Compose y un registro. Antes de cambiar de herramienta, mide qué problema concreto estás resolviendo.
Despliegue sin downtime
Para actualizar un servicio sin cortar la conexión, levanto el nuevo contenedor, espero su healthcheck y solo entonces retiro el anterior. Con un reverse proxy delante, el tráfico se mueve cuando el nuevo está sano. Si el healthcheck falla, el proxy sigue apuntando a la versión anterior.
No es magia de orquestación: es un procedimiento pequeño y comprobable. En un VPS con Compose, un script de despliegue con healthcheck cubre la mayoría de los casos.
Cuándo no usar Docker
Para un script de una sola ejecución o una función serverless pequeña, un contenedor puede ser más infraestructura de la necesaria. Docker brilla cuando hay varios servicios, dependencias del sistema o una diferencia real entre local y producción.
Checklist de producción
- Imagen con versión fija, no
latest. - Usuario no root y
.dockerignore. - Datos en volúmenes y secretos fuera de la imagen.
- Healthcheck y política de reinicio.
- Puertos publicados solo los necesarios.
- Límites de memoria y CPU definidos.
- Logs accesibles y rotación configurada.
- Escaneo de vulnerabilidades en CI.
- Backup de la base de datos probado.
- Proceso de rollback documentado.
Preguntas frecuentes
¿Docker es una máquina virtual?
No. Un contenedor comparte el kernel del host y aísla el proceso; una máquina virtual emula un sistema completo. Por eso los contenedores arrancan más rápido y pesan menos, aunque el aislamiento es distinto.
¿Puedo usar Docker en producción?
Sí, con healthchecks, reinicios, límites de recursos y una estrategia de despliegue. Para proyectos pequeños, una plataforma gestionada puede ser más simple que administrar el orquestador.
¿Cómo persisto los datos?
Con volúmenes. Los datos escritos en la capa del contenedor se pierden al recrearlo. Declara el volumen en Compose y prueba la restauración de un backup.
¿Cómo reduzco el tamaño de la imagen?
Usa Alpine, multi-stage builds, .dockerignore y solo dependencias de producción en la etapa final. Revisa el tamaño por capas antes de optimizar a ciegas.
Para seguir
La referencia oficial está en la documentación de Docker y Docker Compose. El caso de Convenios, con PostgreSQL, n8n y una API NestJS en contenedores, está en este caso de estudio. Si quieres una segunda opinión sobre tu infraestructura, hablemos. También puedes leer cómo llevo un modelo de IA al servicio en De las series temporales al producto.