Lección 1 de 314 min
Docker y Docker Compose
Contenedores frente a máquinas virtuales, imágenes y capas, Dockerfile, volúmenes, redes y Compose.
«En mi máquina funciona.» Es la frase más repetida del desarrollo: en tu equipo tienes una versión de Node y en el servidor otra; tú trabajas en macOS y producción es Linux; a tu compañero le falta una librería del sistema. Cada diferencia es un error esperando a ocurrir.
Docker empaqueta tu aplicación con todo lo que necesita (el runtime, las librerías, la configuración) en una imagen, y la ejecuta como un contenedor idéntico en cualquier máquina. En esta lección aprenderás a escribir un Dockerfile, a sacar partido de la caché de capas y a levantar varios servicios a la vez con Docker Compose.
Contenedores e imágenes
Contenedor frente a máquina virtual
Aislar procesos compartiendo el núcleo del sistema, en vez de emular una máquina entera.
- Aísla con:
- namespaces y cgroups
- Comparte:
- El núcleo del anfitrión
Una máquina virtual emula un equipo completo: un hipervisor reparte el hardware y cada VM arranca su propio sistema operativo, con su propio núcleo (kernel). El aislamiento es muy fuerte, pero cada VM ocupa varios gigas y tarda en arrancar.
Un contenedor es un proceso normal al que el núcleo de Linux aísla con dos mecanismos: los namespaces, que le dan su propia vista de archivos, red y procesos, y los cgroups, que limitan cuánta CPU y memoria puede usar. Todos los contenedores comparten el núcleo del anfitrión, así que arrancan en segundos o menos y ocupan poco más que tu aplicación.
En macOS y Windows, Docker Desktop ejecuta por debajo una pequeña máquina virtual Linux, porque los contenedores de Linux necesitan un núcleo Linux.
Úsalo cuando
Contenedores para empaquetar y desplegar aplicaciones de forma reproducible; máquinas virtuales cuando necesitas otro sistema operativo o un aislamiento más fuerte, por ejemplo entre clientes que no se fían entre sí.
Ten en cuenta
Un contenedor no es una VM ligera: su aislamiento es menor, así que evita ejecutar tu aplicación como root dentro de él. Y no metas varios servicios en un mismo contenedor: uno por proceso.
# Abre una shell dentro de un contenedor de Node
docker run --rm -it node:24-alpine sh
/ # head -1 /etc/os-release # Alpine Linux, no tu sistema
/ # ps # solo ve sus propios procesos
/ # uname -r # pero el núcleo es el del anfitriónImágenes, capas y registros
Una plantilla de solo lectura hecha de capas que se reutilizan y se comparten.
- Registros:
- Docker Hub, GHCR, Amazon ECR
- Etiqueta por defecto:
- latest
Una imagen es la plantilla de la que nace un contenedor: un sistema de archivos con tu aplicación y sus dependencias, más metadatos como el comando de arranque. Se construye por capas apiladas: cada instrucción del Dockerfile que cambia archivos añade una. Al arrancar, Docker pone encima una capa escribible que desaparece con el contenedor.
Las capas se identifican por un hash de su contenido, así que se reutilizan: diez imágenes basadas en node:24-alpine comparten esas capas en disco y, al construir, Docker reaprovecha las que no han cambiado. Es la caché de capas, que probarás en el simulador.
Las imágenes se guardan en registros como Docker Hub, GitHub Container Registry o Amazon ECR. Un nombre como node:24-alpine tiene repositorio y etiqueta; la etiqueta es solo un alias que puede moverse, mientras que el digest (@sha256:…) identifica un contenido exacto.
Úsalo cuando
Para publicar tu aplicación como un artefacto versionado: la construyes una vez, subes la imagen al registro y esa misma imagen corre en pruebas y en producción.
Ten en cuenta
latest no significa «la más reciente»: es solo la etiqueta por defecto. En producción usa etiquetas concretas (la versión o el commit) y, si quieres reproducibilidad total, el digest. Las imágenes pequeñas, como las -alpine o -slim, se descargan antes y tienen menos que parchear.
# Construye y etiqueta con la versión y con el commit
docker build -t tienda-api:1.4.0 -t tienda-api:$(git rev-parse --short HEAD) .
# Mira las capas y cuánto pesa cada una
docker history tienda-api:1.4.0
# Sube la imagen a un registro
docker tag tienda-api:1.4.0 ghcr.io/mi-equipo/tienda-api:1.4.0
docker push ghcr.io/mi-equipo/tienda-api:1.4.0Escribir un Dockerfile
El Dockerfile es la receta de tu imagen. El orden de sus instrucciones no es un detalle de estilo: decide cuánto tarda cada build. Compruébalo en el simulador del final de esta sección.
El Dockerfile
La receta, instrucción por instrucción, para construir una imagen.
- Archivo:
- Dockerfile
- Exclusiones:
- .dockerignore
FROM elige la imagen base y WORKDIR fija la carpeta de trabajo. COPY copia archivos de tu proyecto a la imagen y RUN ejecuta un comando durante el build, como instalar dependencias. CMD indica el comando que arranca el contenedor; ENTRYPOINT fija el ejecutable y deja que CMD aporte los argumentos por defecto.
La clave de un build rápido es el orden. Docker reutiliza cada capa mientras ni la instrucción ni los archivos que copia hayan cambiado, pero en cuanto una capa se reconstruye, todas las siguientes también. Por eso se copia primero package.json con su lockfile, se instalan las dependencias y solo después se copia el resto del código, que cambia mucho más a menudo.
El archivo .dockerignore excluye lo que no debe entrar en la imagen: node_modules, .git o tus archivos .env. Sin él, el build es más lento y puedes acabar publicando secretos.
Úsalo cuando
En cualquier aplicación que vayas a desplegar en contenedores: es la fuente de verdad de cómo se construye.
Ten en cuenta
Escribe CMD en forma de lista (["node", "server.js"]): en forma de texto se ejecuta a través de una shell y tu proceso no recibe bien la señal para apagarse. Y un RUN apt-get update en su propia capa queda en caché para siempre: combínalo con la instalación en el mismo RUN.
FROM node:24-alpine
WORKDIR /app
# 1. Solo lo necesario para instalar: esta capa casi nunca cambia
COPY package*.json ./
RUN npm ci --omit=dev
# 2. El código, que cambia en cada commit
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]Builds multietapa
Compila en una imagen con todas las herramientas y copia solo el resultado a otra mínima.
- Clave:
- COPY --from
Un Dockerfile puede tener varios FROM, y cada uno empieza una etapa nueva. En la primera instalas las dependencias de desarrollo y compilas (TypeScript, un bundler); en la última partes de una imagen limpia y copias solo lo necesario para ejecutar, con COPY --from=build.
La imagen final no lleva compiladores, código fuente ni dependencias de desarrollo: pesa menos, se descarga antes y ofrece menos superficie de ataque. Las etapas intermedias no se publican, aunque su caché se conserva para el siguiente build.
Úsalo cuando
Siempre que haya un paso de compilación (TypeScript, un frontend con Vite, Go, Java) y quieras una imagen de producción pequeña.
Ten en cuenta
Copia a la etapa final solo lo imprescindible: un COPY --from=build /app /app lo arrastra todo y anula la ventaja. Y cuidado con los secretos que uses al construir: si los copias en una capa, quedan dentro de la imagen aunque luego los borres.
# Etapa 1: compilar con todas las dependencias
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Etapa 2: solo lo necesario para ejecutar
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]Interactivo
La caché de capas de Docker
Elige el orden del Dockerfile y qué ha cambiado desde el último build. Después, construye y mira qué capas se reutilizan.
- 1
FROM node:22-alpine? - 2
WORKDIR /app? - 3
COPY package*.json ./? - 4
RUN npm ci? - 5
COPY . .? - 6
RUN npm run build? - 7
CMD ["node", "dist/server.js"]?
Elige qué cambió y pulsa «Construir». Regla de oro: en cuanto una capa se reconstruye, todas las que vienen detrás también.
Tiempos orientativos de un proyecto mediano. En la salida real de docker build verás CACHED junto a cada paso reutilizado.
Datos, puertos y varios contenedores
Volúmenes, puertos y redes
Cómo guardar datos que sobrevivan al contenedor y cómo se comunican los contenedores.
- Puertos:
- -p anfitrión:contenedor
- Persistencia:
- Volúmenes
Todo lo que un contenedor escribe en su capa se pierde al borrarlo. Para conservar datos hay dos opciones. Un volumen con nombre lo gestiona Docker (-v datos-db:/var/lib/postgresql/data) y es lo ideal para bases de datos. Un bind mount monta una carpeta de tu equipo dentro del contenedor (-v "$(pwd)/src":/app/src) y es lo ideal en desarrollo, para que el contenedor vea tus cambios al momento.
Cada contenedor tiene su propia red. Para llegar a él desde tu equipo, publicas un puerto con -p 8080:3000: el puerto 8080 del anfitrión se redirige al 3000 del contenedor. Entre contenedores, lo habitual es crear una red propia: dentro de ella, cada contenedor encuentra a los demás por su nombre, como si fuera un dominio.
Úsalo cuando
Volúmenes con nombre para datos que deben sobrevivir (bases de datos, archivos subidos); bind mounts para desarrollar con recarga en caliente; redes propias para que tu API hable con su base de datos sin publicar el puerto de esta.
Ten en cuenta
Dentro de un contenedor, localhost es el propio contenedor, no tu equipo ni otro contenedor: usa el nombre del servicio. Y -p 5432:5432 expone la base de datos en todas las interfaces de red del anfitrión; si solo la necesitas en local, usa -p 127.0.0.1:5432:5432.
# Una red propia y una base de datos con su volumen
docker network create tienda
docker run -d --name db --network tienda \
-e POSTGRES_PASSWORD=secreto \
-v datos-db:/var/lib/postgresql/data postgres:17
# La API llega a la base de datos por su nombre: «db»
docker run -d --name api --network tienda -p 8080:3000 \
-e DATABASE_URL=postgres://postgres:secreto@db:5432/postgres \
tienda-api:1.4.0Docker Compose
Describe en un archivo YAML todos los servicios de tu aplicación y levántalos con un comando.
- Archivo:
- compose.yaml
- Comando:
- docker compose up
En lugar de recordar cinco docker run con sus opciones, Docker Compose los describe en compose.yaml: cada servicio con su imagen o su Dockerfile, sus puertos, variables y volúmenes. docker compose up construye lo necesario, crea una red común y arranca todo; docker compose down lo detiene y lo elimina.
Dentro de la red de Compose, cada servicio es accesible por su nombre. depends_on ordena el arranque y, con condition: service_healthy, espera a que la base de datos pase su comprobación de salud antes de arrancar la API.
Úsalo cuando
Para el entorno de desarrollo (la app, su base de datos y Redis con un solo comando) y para pruebas de integración en CI. También para despliegues sencillos en un único servidor.
Ten en cuenta
Compose gestiona contenedores en una sola máquina: no reparte la carga entre servidores ni recoloca los contenedores si la máquina cae. Para eso está la orquestación, que verás en la siguiente lección. El campo version: de los archivos antiguos ya no hace falta, y el comando actual es docker compose, con espacio.
services:
api:
build: .
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://postgres:secreto@db:5432/postgres
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: secreto
volumes:
- datos-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
volumes:
datos-db:Pruébalo
Con el entorno levantado, docker compose logs -f api sigue los registros de la API y docker compose exec db psql -U postgres abre una consola dentro de la base de datos, sin instalar PostgreSQL en tu equipo.
Interactivo
Contenedor frente a máquina virtual
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Contenedor | Máquina virtual |
|---|---|---|
| Qué virtualiza | Procesos aislados dentro de un sistema | Un equipo completo, hardware incluido |
| Sistema operativo | Comparte el núcleo del anfitrión | Cada VM arranca el suyo |
| Arranque | Segundos o menos | De segundos a minutos |
| Tamaño típico | De decenas a cientos de MB | Varios GB |
| Aislamiento | Bueno, aunque menor que una VM | Muy fuerte: núcleos separados |
| Uso típico | Empaquetar y desplegar aplicaciones | Otros sistemas operativos, entornos multicliente, sistemas heredados |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Cada vez que cambias una línea de código, el build de tu imagen tarda tres minutos porque vuelve a ejecutar npm ci.
Para recordar
- Un contenedor es un proceso aislado que comparte el núcleo del anfitrión; una máquina virtual emula un equipo entero.
- Una imagen son capas de solo lectura: en producción usa etiquetas concretas o digests, no
latest. - Ordena el Dockerfile de lo que menos cambia a lo que más: cuando una capa se reconstruye, todas las siguientes también.
- Los builds multietapa dejan fuera de la imagen final todo lo que solo sirve para compilar.
- Los datos que deben sobrevivir van en volúmenes, y Docker Compose levanta todos tus servicios con un solo comando.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué comparten todos los contenedores que se ejecutan en una misma máquina Linux?
2.En un Dockerfile, la capa de
COPY . .se reconstruye. ¿Qué pasa con las instrucciones que vienen después?3.¿Para qué sirve
.dockerignore?4.Ejecutas
docker run -p 8080:3000 mi-api. ¿Dónde abres la app en tu navegador?5.¿Qué gana una imagen de producción con un build multietapa?