Docker: contenedores de cero a producción
Docker empaqueta tu aplicación junto con su entorno — dependencias, configuración, runtime — en una unidad reproducible llamada contenedor. Aprende desde los conceptos hasta el despliegue con Docker Compose y registros de imágenes.
¿Qué es Docker?
Docker es una plataforma para desarrollar, enviar y ejecutar aplicaciones en contenedores. Un contenedor es un proceso aislado que comparte el kernel del sistema operativo anfitrión pero tiene su propio sistema de archivos, red y espacio de procesos. Eso lo hace mucho más ligero que una máquina virtual.
Contenedores vs máquinas virtuales
Una VM virtualiza el hardware y ejecuta un sistema operativo completo por encima de un hypervisor: pesa gigabytes y tarda minutos en arrancar. Un contenedor virtualiza solo el espacio de usuario y comparte el kernel del host: pesa megabytes, arranca en milisegundos y arranca decenas de ellos sin agotar la máquina.
namespaces (procesos, red, montajes) y cgroups (límites de CPU y memoria). En macOS y Windows, Docker Desktop ejecuta una VM ligera de Linux en segundo plano para darles el mismo comportamiento.Imágenes y contenedores
Una imagen es una plantilla inmutable y de solo lectura: código, runtime, librerías y configuración. Un contenedor es una instancia en ejecución de una imagen: puede arrancarse, detenerse, eliminarse y duplicarse. La relación es la misma que entre una clase y sus objetos en programación orientada a objetos.
El daemon y el CLI
Docker tiene dos piezas: el daemon (dockerd), un servicio en segundo plano que gestiona imágenes, contenedores, redes y volúmenes; y el cliente (docker), la CLI con la que le das órdenes. Al escribir docker run, el cliente envía la petición al daemon vía una API REST local.
En un flujo de CI/CD verás imágenes empaquetadas en el mismo formato que corre en producción: ese es el valor central. Si funciona en tu máquina, funcionará en el servidor, porque el contenedor lleva consigo el entorno completo.
Instalación
1. Descarga Docker Desktop desde docker.com y arrastra la app a Aplicaciones. Incluye el daemon, el CLI, Docker Compose y una interfaz gráfica.
2. Ábrela por primera vez y acepta el uso del Hypervisor (puede pedir permisos de administrador).
3. Verifica la instalación — el daemon debe estar en ejecución:
docker version
# Client: ... Engine: ... -> todo OK
docker info
# Muestra versión del kernel, contenedores e imágenes actualesEn Linux puedes instalar el engine directamente (apt install docker.io) y ejecutar contenedores sin GUI, pero Docker Desktop es la vía más sencilla en macOS y Windows.
docker version falla con "Cannot connect to the Docker daemon", el problema casi siempre es que Docker Desktop no está abierto. Ábrelo y espera a que la ballena deje de animarse.Primeros comandos
Descarga una imagen sin ejecutarla con pull y ejecútala de forma interactiva con run -it:
# Descargar la imagen de Ubuntu
docker pull ubuntu:24.04
# Ejecutar un contenedor interactivo con una shell dentro
docker run -it ubuntu:24.04 bash
root@a1b2c3:/# ls /etc/os-release
root@a1b2c3:/# exit
# Ejecutar y eliminar al salir
docker run --rm -it ubuntu:24.04 echo "hola desde un contenedor"Gestiona los contenedores en ejecución y detenidos:
docker ps # contenedores en ejecución
docker ps -a # también los detenidos
docker stop <id> # detener uno
docker rm <id> # eliminar uno (solo detenidos)
docker rm -f <id> # eliminar incluso en ejecuciónEjecuta un proceso adicional dentro de un contenedor vivo con exec — imprescindible para depurar:
docker run -d --name web nginx
docker exec -it web bash
# abre una shell dentro del contenedor nginx
docker logs web # ver la salida estándar
docker inspect web# JSON con toda la configuración y estado-d (detached) ejecuta el contenedor en segundo plano y te devuelve el prompt. Con --name le pones un identificador legible para no depender de IDs largos.El Dockerfile
Un Dockerfile es un script que describe cómo construir una imagen, instrucción a instrucción. Cada instrucción crea una capa inmutable que Docker cachea: si una capa no cambia, las siguientes se reutilizan en la siguiente construcción.
# Dockerfile de una API Node.js
FROM node:22-alpine
# Directorio de trabajo: todas las instrucciones se ejecutan ahí
WORKDIR /app
# Copiar dependencias primero para aprovechar el cache de capas
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Después el código fuente (cambia más a menudo)
COPY . .
# Puerto que expondrá el contenedor (documental)
EXPOSE 3000
CMD ["node", "src/index.js"]ENTRYPOINT vs CMD
CMD define el comando por defecto y se puede sobrescribir al ejecutar: docker run mi-api node otra.js. ENTRYPOINT define el ejecutable fijo de la imagen y no se reemplaza, aunque admite argumentos adicionales.
# ENTRYPOINT fijo: la imagen SIEMPRE ejecuta python app.py
ENTRYPOINT ["python", "app.py"]
# Forma exec (recomendada) vs forma shell
CMD ["python", "app.py"] # exec: el PID 1 es el proceso real
CMD python app.py # shell: envuelto en /bin/sh -c/bin/sh -c, así las señales (CTRL+C, SIGTERM) no llegan al proceso y Docker debe esperar un timeout antes de forzar el cierre.La selección de la imagen base importa: las variantes -alpine o -slim reducen decenas de megabytes y la superficie de ataque, a cambio de alguna librería que quizá debas instalar tú mismo.
.dockerignore
Funciona como .gitignore: excluye archivos del contexto de construcción que se envía al daemon. Evita mandar node_modules, builds y archivos de entorno con secretos:
node_modules
dist
.env
.git
*.log
.DS_Store.env dentro de la imagen: las capas se quedan guardadas y cualquiera con acceso al registro puede extraer los secretos. Los valores de entorno se inyectan en tiempo de ejecución con --env-file o con secretos de tu proveedor.Construir y etiquetar
Construye la imagen a partir del Dockerfile con build y nómbrala con -t. El tag va seguido del registro, el repositorio y la versión:
# Construir con tag nombre:version
docker build -t mi-api:v1 .
# Ver las imágenes locales
docker images
# Etiquetar una imagen existente con otro nombre (para subirla a un registro)
docker tag mi-api:v1 ghcr.io/miusuario/mi-api:latest
# Ejecutar mapeando puertos: HOST:CONTENEDOR
docker run -d -p 8080:3000 --name api mi-api:v1
curl http://localhost:8080En -p 8080:3000, 8080 es el puerto de tu máquina y 3000 el puerto interno del contenedor. Sin este mapeo, la app queda inaccesible desde el host aunque esté escuchando.
Volúmenes
El sistema de archivos de un contenedor es efímero: todo lo que escribas desaparece al eliminarlo. Los volúmenes montan datos del host (o gestionados por Docker) dentro del contenedor para persistir y compartir información:
# Bind mount: carpeta del host dentro del contenedor
docker run -d -p 80:80 -v "$PWD/sitio:/usr/share/nginx/html" nginx
# Named volume: Docker gestiona el almacenamiento por ti
docker volume create datos-postgres
docker run -d -v datos-postgres:/var/lib/postgresql/data postgres:16
docker volume ls
docker volume inspect datos-postgres-v ruta:ruta) es ideal en desarrollo: editas el código en el host y el contenedor lo ve al instante. Los named volumes son la opción correcta para datos de bases de datos: Docker controla la ubicación y el backup.Docker Compose
Compose define en un único archivo YAML los servicios, redes y volúmenes de tu aplicación y los arranca con un comando. Es la forma estándar de orquestar varios contenedores en desarrollo: una API con su base de datos, por ejemplo.
# docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- datos-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
volumes:
datos-postgres:# Arrancar todo en segundo plano
docker compose up -d
# Ver logs de todos los servicios
docker compose logs -f
# Detener y eliminar contenedores (los volúmenes persisten)
docker compose down
# Eliminar también los volúmenes
docker compose down -vDentro de la red de Compose, los servicios se resuelven por su nombre: api conecta a db:5432 sin necesidad de IPs ni configuración de host. depends_on con condition: service_healthy espera a que la base de datos pase el healthcheck antes de arrancar la API.
Registros y push
Un registro es un repositorio remoto de imágenes. Docker Hub es el público por defecto; GitHub Container Registry (ghcr.io), GitLab y ECR de AWS son alternativas habituales ligadas a tu plataforma de CI/CD.
# Login en Docker Hub
docker login
# Etiquetar con tu usuario de Hub y subir
docker tag mi-api:v1 miusuario/mi-api:v1
docker push miusuario/mi-api:v1
# Cualquiera (o tu CI/CD) puede bajarla
docker pull miusuario/mi-api:v1
docker run -d -p 3000:3000 miusuario/mi-api:v1Usa tags inmutables por versión (v1.2.3) en producción y reserva latest para desarrollo. Si un despliegue falla, así puedes volver a la versión exacta anterior.
Ejercicios
Con Docker ya tienes el ciclo completo: contenedores aislados, imágenes reproducibles, volúmenes persistentes y multi-servicio con Compose. El siguiente paso natural es la orquestación: cuando necesitas decenas de contenedores con balanceo, escalado y auto-reparación, herramientas como Kubernetes toman el relevo.