Aprende Docker

Guía de aprendizaje

Aprende Docker

Docker resuelve un problema viejo: que algo funcione en tu máquina y no en el servidor. Lo hace empaquetando la aplicación con todo lo que necesita para correr, de modo que lo que pruebas en tu portátil es exactamente lo mismo que acaba en producción. Esta guía va de construir esas imágenes y de llevarlas a QA y a producción sin sorpresas.

La ruta de aprendizaje

Diez pasos, de los primeros comandos al despliegue en producción. Los tres primeros están explicados más abajo, en esta misma página; cada uno de los demás tendrá su propia lección.

1

Imágenes y contenedores

La diferencia que lo explica todo, y los primeros comandos.

Aquí

2

Capas y caché

Por qué el orden de las instrucciones cambia el tiempo de construcción.

Aquí

3

La misma imagen en todos los entornos

El principio que hace fiable el despliegue.

Aquí

4

El Dockerfile a fondo

FROM, RUN, COPY, CMD y ENTRYPOINT, y cuándo usar cada uno.

Leer lección →

5

Imágenes pequeñas

Construcción en varias etapas y .dockerignore.

Leer lección →

6

Configuración por entorno

Variables, archivos .env y por qué los secretos no van en la imagen.

Leer lección →

7

Volúmenes y datos

Qué sobrevive al contenedor y qué desaparece con él.

Leer lección →

8

Varios servicios con Compose

Aplicación, base de datos y red, en un solo archivo.

Leer lección →

9

Etiquetar y publicar en un registro

Etiquetas por commit, latest y por qué importa la distinción.

Leer lección →

10

Desplegar en QA y en producción

Promover la misma imagen, reversión y comprobaciones de salud.

Leer lección →

Todo lo de esta guía está ejecutado. Los comandos y sus salidas se probaron con Docker 29.8.1 antes de publicarla, y los tamaños de imagen están medidos, no estimados.

Por qué Docker cambia el despliegue

Antes de los contenedores, desplegar consistía en preparar un servidor hasta que la aplicación arrancara: instalar la versión correcta del lenguaje, las bibliotecas del sistema, las variables, los permisos. Ese servidor se volvía único e irrepetible, y reproducirlo para pruebas era un trabajo manual que nunca salía idéntico. De ahí el «en mi máquina funciona»: no era mentira, es que las dos máquinas eran distintas.

Una imagen de Docker invierte el planteamiento. En vez de preparar el servidor para la aplicación, se empaqueta la aplicación con su entorno entero —sistema de archivos, dependencias, configuración— en un archivo inmutable. El servidor solo necesita saber ejecutar contenedores, y lo que ejecuta es bit a bit lo mismo que probaste.

Esa inmutabilidad es lo que hace posible la parte que más interesa aquí: que la imagen que validaste en QA sea literalmente la que se publica en producción, sin reconstruir nada por el camino.

La primera idea: la imagen es la plantilla, el contenedor es la ejecución

Es la distinción que más confusión causa al principio, y sin ella nada de lo demás encaja. Una imagen es un paquete inmutable y de solo lectura. Un contenedor es una ejecución de esa imagen, con una capa escribible propia que desaparece cuando el contenedor se elimina.

Imagen y contenedores LA IMAGEN mi-app:1.4.2 inmutable, de solo lectura se construye una vez LOS CONTENEDORES contenedor en tu portátil contenedor en QA contenedor en producción cada uno añade una capa escribible lo que escriba dentro desaparece al eliminarlo, salvo que uses un volumen
Una sola imagen, muchos contenedores. La imagen nunca cambia; los contenedores van y vienen.

De ahí salen dos consecuencias prácticas. La primera es que un contenedor no se actualiza, se reemplaza: para cambiar la aplicación se construye una imagen nueva y se levanta un contenedor nuevo. La segunda es que todo lo que el contenedor escriba en su propio sistema de archivos se pierde al eliminarlo, y por eso las bases de datos y los archivos subidos necesitan volúmenes, que es el paso 7 de la ruta.

La segunda idea: las capas y la caché

Una imagen no es un bloque único: es una pila de capas, una por cada instrucción del Dockerfile. Docker guarda cada capa y la reutiliza si nada ha cambiado, lo que hace que una segunda construcción tarde segundos en vez de minutos.

La regla que se deriva de esto es la que más tiempo ahorra en el día a día: cuando una capa cambia, todas las de debajo se invalidan y hay que rehacerlas. Por eso el orden de las instrucciones importa tanto.

El orden de las instrucciones y la caché ORDEN EQUIVOCADO FROM node:22-alpine COPY . . RUN npm install Tocas una línea de código y reinstala todas las dependencias otra vez. ORDEN CORRECTO FROM node:22-alpine COPY package*.json ./ RUN npm install Las dependencias solo se reinstalan cuando cambia package.json. Lo que cambia poco va arriba; lo que cambia en cada commit, abajo. Es la regla que más minutos ahorra.
Dos Dockerfiles que producen la misma imagen, pero uno tarda segundos y el otro minutos en cada cambio.

La tercera idea: la misma imagen en QA y en producción

Aquí está el punto que convierte Docker en una herramienta de despliegue y no solo de desarrollo. La tentación natural es construir una imagen para QA y, cuando se aprueba, construir otra para producción. Eso anula la mayor parte del beneficio: entre las dos construcciones pueden cambiar una dependencia, una versión del sistema base o un archivo del repositorio, y lo que se publica ya no es lo que se validó.

La práctica correcta es construir una sola vez, etiquetar la imagen con algo que la identifique sin ambigüedad —normalmente el identificador del commit—, publicarla en un registro, y que cada entorno descargue esa misma imagen. Lo único que cambia entre QA y producción es la configuración que se le pasa al arrancar.

Promover la misma imagen entre entornos 1. SE CONSTRUYE una sola vez, en CI 2. SE ETIQUETA Y PUBLICA mi-app:2bbc62e 3. QA descarga esa imagen con las variables de QA 4. Producción, la MISMA con las variables de producción Nunca se reconstruye para promover. Si QA aprobó esa imagen, esa es la que se publica. Lo único que cambia entre entornos es la configuración que recibe al arrancar.
Construir una vez y promover. Reconstruir para cada entorno es volver al «en mi máquina funciona».
Por qué latest no sirve para desplegar. La etiqueta latest no significa nada en Docker: es solo la etiqueta por defecto y apunta a lo último que se subió con ese nombre. Si producción arranca mi-app:latest, nadie puede decir con certeza qué versión está corriendo, ni volver atrás con seguridad. Para desplegar hay que usar una etiqueta inmutable, como el identificador del commit, y dejar latest solo como comodidad de desarrollo.

Tu primer contenedor

Lo que sigue se ejecutó con Docker 29.8.1; las salidas están copiadas tal cual. Primero, una imagen mínima que escribe un archivo y lo muestra al arrancar:

FROM alpine:3.22
RUN echo "hola desde la imagen" > /mensaje.txt
CMD ["cat", "/mensaje.txt"]

Guardado como Dockerfile, se construye y se ejecuta con dos órdenes:

docker build -t mi-demo:1 .
docker run --rm mi-demo:1
salidahola desde la imagen

El -t pone nombre y etiqueta a la imagen, el punto final indica que el contexto de construcción es la carpeta actual, y --rm elimina el contenedor al terminar para no ir dejando restos.

Algo que se pueda visitar

Un contenedor que sirve una página, para ver el mapeo de puertos, que es el otro concepto imprescindible:

FROM alpine:3.22
RUN apk add --no-cache busybox-extras \
 && mkdir -p /sitio \
 && echo '<h1>hola desde Docker</h1>' > /sitio/index.html
EXPOSE 80
CMD ["httpd", "-f", "-p", "80", "-h", "/sitio"]
docker build -t mi-demo:web .
docker run -d --name demo-web -p 18080:80 mi-demo:web
docker ps
curl http://127.0.0.1:18080/
salidademo-web  mi-demo:web  Up 2 seconds  0.0.0.0:18080->80/tcp
<h1>hola desde Docker</h1>

El -p 18080:80 es lo que conecta el puerto 18080 de tu máquina con el 80 de dentro del contenedor. Sin esa conexión el servidor funciona, pero nadie puede llegar a él: un contenedor está aislado de la red del anfitrión salvo que se le abra una puerta explícitamente.

Para limpiar, porque un contenedor arrancado con -d sigue corriendo:

docker rm -f demo-web

El tamaño sí importa

Esas dos imágenes pesan 12,8 MB porque parten de Alpine, una distribución pensada para esto. Para comparar, una imagen construida desde scratch —literalmente la nada— con un único binario estático de 5,3 MB dentro, da este resultado:

docker imagesmi-demo:scratch   6.91MB
mi-demo:1        12.8MB

La diferencia parece pequeña en estos números, pero la misma aplicación empaquetada sobre una imagen de sistema completa ronda los cientos de megabytes. El tamaño se paga en cada descarga, en cada despliegue y en cada escalado, y es el tema del paso 5 de la ruta.

Los comandos que se usan a diario

Qué quieres Orden
Construir una imagen docker build -t nombre:etiqueta .
Ejecutarla y que se borre al salir docker run --rm nombre:etiqueta
Dejarla corriendo en segundo plano docker run -d --name x nombre
Publicar un puerto -p 8080:80
Pasar una variable de entorno -e CLAVE=valor
Ver lo que está corriendo docker ps
Ver las imágenes locales docker images
Ver los registros de un contenedor docker logs -f nombre
Entrar en un contenedor vivo docker exec -it nombre sh
Pararlo y eliminarlo docker rm -f nombre
Subirla a un registro docker push registro/nombre:etiqueta
Cuidado con docker system prune. Es la orden que todo el mundo recomienda para liberar espacio, y borra imágenes, contenedores parados, redes y, con -a, también todo lo que no esté en uso en ese momento. En una máquina de desarrollo con trabajo a medias puede llevarse por delante imágenes que tardaron media hora en construirse. Conviene mirar antes qué va a borrar con docker system df.

Dónde seguir mientras tanto

La documentación oficial es buena y está en parte traducida. La guía de inicio de Docker cubre lo básico con ejemplos, y la referencia del Dockerfile es la página que más vas a consultar una vez empieces a escribir los tuyos.

Un consejo que ahorra tiempo desde el principio: no intentes meter tu aplicación entera en un contenedor el primer día. Empieza por contenerizar algo que ya funcione y sea pequeño, haz que construya y arranque, y solo después añade la base de datos, los volúmenes y el resto. Docker tiene muchas piezas y aprenderlas de una en una es bastante más rápido que depurar cinco a la vez.