1. El fin de «en mi ordenador funciona»
Un contenedor empaqueta una aplicación junto con todo lo que necesita para ejecutarse: el intérprete en la versión exacta, las librerías del sistema, las dependencias y la configuración por defecto. Ese paquete se ejecuta igual en el portátil de cualquier desarrollador, en el servidor de pruebas y en producción. Docker popularizó la idea en 2013 y hoy es la forma estándar de distribuir y desplegar aplicaciones.
Los contenedores de los barcos
Contenedores y máquinas virtuales
Una máquina virtual emula un ordenador completo con su propio sistema operativo, lo que la hace pesada (gigas de disco, minutos para arrancar). Un contenedor, en cambio, comparte el núcleo del sistema anfitrión y solo aísla los procesos, la red y los ficheros mediante funciones del núcleo de Linux (namespaces y cgroups). Por eso ocupa megas y arranca en segundos, y en un servidor caben decenas.
| Máquina virtual | Contenedor | |
|---|---|---|
| Qué incluye | Sistema operativo completo | Solo la aplicación y sus dependencias |
| Tamaño típico | Gigabytes | Megabytes |
| Arranque | Minutos | Segundos o menos |
| Aislamiento | Muy fuerte (núcleo propio) | Bueno, pero comparte el núcleo |
| Uso típico | Distintos sistemas operativos, aislamiento máximo | Desplegar aplicaciones y sus servicios |
2. Imágenes, contenedores, volúmenes y redes
Cuatro conceptos bastan para entender Docker. Una imagen es la plantilla de solo lectura (como una clase): se construye a partir de un Dockerfile y se guarda en un registro como Docker Hub. Un contenedor es una instancia en ejecución de una imagen (como un objeto): puedes lanzar muchos a partir de la misma. Los contenedores son desechables, así que los datos que deben sobrevivir se guardan en volúmenes. Y las redes de Docker permiten que los contenedores se hablen entre sí por su nombre.
1docker run -d --name web -p 8080:80 nginx:1.27 # descarga la imagen y lanza un contenedor
2docker ps # contenedores en marcha
3docker logs -f web # su salida
4docker exec -it web sh # abrir una terminal dentro
5docker stop web && docker rm web # pararlo y borrarlo
6docker images # imágenes descargadasLa opción -p 8080:80 publica el puerto: lo que llegue al 8080 del anfitrión se envía al 80 del contenedor. Dos contenedores no pueden publicar el mismo puerto del anfitrión, algo que comprobarás en uno de los retos.
3. Construir tu propia imagen: el Dockerfile
Un Dockerfile es la receta de una imagen: cada instrucción añade una capa. Docker guarda en caché las capas y, al reconstruir, reutiliza todas las que no han cambiado. De ahí sale la buena práctica más importante: copiar primero solo los ficheros de dependencias e instalarlas, y copiar el resto del código después. Así, cambiar una línea de tu código no obliga a reinstalar todas las dependencias.
1# Etapa 1: construir
2FROM node:22-alpine AS build
3WORKDIR /app
4COPY package.json package-lock.json ./
5RUN npm ci
6COPY . .
7RUN npm run build
8
9# Etapa 2: imagen final, solo con lo necesario para ejecutar
10FROM node:22-alpine
11WORKDIR /app
12ENV NODE_ENV=production
13COPY package.json package-lock.json ./
14RUN npm ci --omit=dev
15COPY /app/dist ./dist
16USER node
17EXPOSE 3000
18HEALTHCHECK CMD wget -qO- http://localhost:3000/salud || exit 1
19CMD ["node", "dist/server.js"]- Versión concreta en FROM (
node:22-alpine, nuncalatest): la imagen debe ser reproducible. - Construcción en varias etapas: las herramientas de compilación se quedan en la primera etapa y la imagen final es pequeña.
- USER distinto de root: si alguien compromete la aplicación, no controla el contenedor.
- .dockerignore con node_modules, .git y .env, para no copiar basura ni secretos a la imagen.
- Con
apt-get, instala y limpia la caché en el mismo RUN (rm -rf /var/lib/apt/lists/*).
4. Varios contenedores juntos: Docker Compose
Una aplicación real necesita varios servicios: la aplicación, la base de datos, quizá Redis y un Nginx delante. Docker Compose los describe en un único fichero YAML y los levanta juntos con docker compose up -d, creando una red común en la que cada servicio se encuentra por su nombre.
1services:
2 proxy:
3 image: nginx:1.27-alpine
4 ports: ["80:80", "443:443"]
5 volumes: ["./nginx.conf:/etc/nginx/conf.d/default.conf:ro"]
6 depends_on: [app]
7
8 app:
9 build: .
10 env_file: .env # DB_HOST=db, DB_PASS=…
11 depends_on:
12 db: { condition: service_healthy }
13 restart: unless-stopped
14
15 db:
16 image: mariadb:11
17 environment:
18 MARIADB_DATABASE: tienda
19 MARIADB_USER: tienda
20 MARIADB_PASSWORD_FILE: /run/secrets/db_pass
21 MARIADB_RANDOM_ROOT_PASSWORD: "1"
22 secrets: [db_pass]
23 volumes: ["datos:/var/lib/mysql"] # los datos sobreviven al contenedor
24 healthcheck:
25 test: ["CMD", "healthcheck.sh", "--connect"]
26
27volumes:
28 datos:
29
30secrets:
31 db_pass: { file: ./secretos/db_pass.txt }Fíjate en que solo el proxy publica puertos: la aplicación y la base de datos quedan en la red interna, sin exponerse a internet. Para escalar a muchos servidores se usan orquestadores como Kubernetes, que reparten los contenedores entre máquinas, los reinician si fallan y hacen despliegues progresivos, pero los conceptos son los mismos que acabas de ver.
Un contenedor no es una máquina virtual
5. Reto: revisa un Dockerfile
Las herramientas como Hadolint revisan los Dockerfile en busca de malas prácticas. En este reto programas una versión sencilla que detecta las cuatro más frecuentes.
Retos de Bash en el IDE
Escribe el script y pulsa Ejecutar tests: se ejecuta en un Linux real con Bash y se prueba con varios casos, algunos ocultos.
La entrada es un Dockerfile. Escribe un aviso por línea, en este orden, cuando corresponda: «Imagen base sin versión concreta» (algún FROM sin etiqueta o con :latest), «apt-get install sin limpiar la caché» (un RUN con apt-get install sin rm -rf /var/lib/apt/lists), «Se copia todo el código antes de instalar las dependencias» (un COPY . aparece antes del RUN que ejecuta npm install/npm ci, composer install o pip install) y «El contenedor se ejecuta como root» (no hay ninguna instrucción USER). Si no hay ningún aviso, escribe «Sin avisos».