Objetivos de la práctica
- Entender qué es un contenedor, en qué se diferencia de una máquina virtual y qué problema resuelve.
- Conocer la arquitectura de Docker en Windows: Docker Desktop, dockerd, containerd, runc y el papel de WSL2.
- Diferenciar con soltura imagen, contenedor, capa, registry y Dockerfile.
- Instalar WSL2 y Docker Desktop en Windows 11 y comprobar que funcionan.
- Usar
docker run,docker ps,docker logs,docker exec,docker stopydocker rmcon sus flags habituales. - Gestionar imágenes:
docker pull,docker images,docker rmi,docker system prune. - Distinguir bind mounts, volúmenes con nombre y tmpfs; saber cuándo se usa cada uno.
- Trabajar con las redes de Docker (bridge por defecto, redes definidas por el usuario, modo host) y conectar contenedores entre sí.
- Escribir tu primer Dockerfile con
FROM,RUN,COPY,WORKDIR,EXPOSEyCMD; construir la imagen condocker build. - Provocar y diagnosticar cinco fallos típicos: puerto en uso, contenedor que no arranca, volumen con permisos, contenedor que consume todo el disco, build que falla.
- Conocer Docker Compose a nivel básico: levantar una aplicación multi-servicio con un único archivo YAML.
- Documentar incidencias relacionadas con contenedores en una ficha técnica profesional.
Parte 0 - Conceptos previos (60 min)
0.1 - Qué es un contenedor y por qué nació Docker
Un contenedor es un proceso (o grupo de procesos) que se ejecuta en el sistema operativo del anfitrión pero aislado del resto: ve su propio sistema de archivos, sus propios procesos, su propia red. No es una máquina virtual: comparte el kernel del anfitrión y por eso arranca en milisegundos y consume muy poca RAM.
Antes de los contenedores, instalar un programa con sus dependencias podía romper otros programas del mismo equipo (versión de Python distinta, una librería del sistema que se actualizaba, configuraciones que se pisaban). El típico "en mi máquina funciona" era el dolor de cabeza permanente. Docker resuelve eso encapsulando cada aplicación con todo lo que necesita en una imagen autocontenida.
0.2 - Contenedor vs máquina virtual
| Aspecto | Máquina virtual | Contenedor |
|---|---|---|
| Aislamiento | Total (hardware virtualizado) | Procesos aislados con namespaces y cgroups |
| Kernel | Cada VM lleva el suyo | Comparten el del anfitrión |
| Arranque | 30 s a varios minutos | Décimas de segundo |
| Tamaño en disco | 10-50 GB típico | 10-500 MB típico |
| RAM por instancia | 1-8 GB reservados | 10-200 MB según uso |
| Cuántas puedes arrancar a la vez | 2-5 en un portátil normal | 20-100 sin despeinarse |
| Uso típico | Sistemas operativos completos, lab de redes, malware | Servicios concretos (web, BD, microservicios) |
Conclusión práctica: las VMs siguen siendo la herramienta correcta para emular un PC completo (lo que has hecho en las prácticas anteriores). Los contenedores son la herramienta correcta cuando lo único que quieres es un proceso aislado (un servidor web de pruebas, un MariaDB temporal, una herramienta sin instalar).
0.3 - Arquitectura de Docker en Windows
En Windows, Docker no corre nativamente sobre el kernel de Windows (los contenedores Linux necesitan un kernel Linux). El truco es:
- WSL2 (Windows Subsystem for Linux 2) arranca una VM Linux ligera y oculta gestionada por Hyper-V.
- Dentro de esa VM corre el dockerd (daemon de Docker) y por debajo containerd + runc (el runtime que de verdad lanza los contenedores).
- Docker Desktop es la aplicación que orquesta todo lo anterior y te da una GUI bonita en Windows.
- Cuando ejecutas
docker rundesde PowerShell o CMD de Windows, el comando viaja a través de un canal adockerddentro de WSL2, que arranca el contenedor.
0.4 - Vocabulario imprescindible
| Concepto | Qué es | Analogía |
|---|---|---|
| Imagen | Plantilla inmutable: el sistema de archivos y la configuración para crear contenedores | El instalador .iso de Windows |
| Contenedor | Instancia en ejecución (o parada) creada a partir de una imagen | El Windows ya instalado y arrancado |
| Capa (layer) | Cada cambio sobre la imagen base se guarda como una capa apilable. Permite compartir partes comunes entre imágenes | Bandejas transparentes apiladas, ves la suma de todas |
| Registry | Almacén remoto de imágenes. El público es Docker Hub; las empresas tienen los suyos (Harbor, GitHub Container Registry, AWS ECR...) | App Store / Play Store |
| Tag | Etiqueta de versión de una imagen (nginx:1.25, nginx:latest, nginx:alpine) | El número de versión del paquete |
| Dockerfile | Receta en texto plano para construir una imagen propia | Un script de instalación + configuración |
| Volumen | Espacio en disco persistente que sobrevive al contenedor | Una carpeta compartida del NAS montada en un PC desechable |
| Red | Red virtual a la que se conectan los contenedores para hablar entre ellos | VLAN dedicada a un grupo de servidores |
0.5 - Por qué un contenedor "vacío" no sirve para casi nada
Una imagen sin la aplicación dentro no tiene sentido. Por eso casi siempre partes de una imagen existente:
nginx- servidor web ligerohttpd- Apachemariadb,postgres,redis- bases de datospython,node,php- intérpretes para tus appsubuntu,debian,alpine- base genérica si construyes tu propia imagen
Una imagen sale de un registry. Docker viene configurado por defecto contra Docker Hub, así que docker pull nginx baja la imagen oficial de nginx de Docker Hub.
0.6 - Almacenamiento: ¿dónde va lo que escribe un contenedor?
Por defecto, los cambios que un contenedor escribe en su sistema de archivos viven sólo mientras el contenedor exista. Si lo borras, los datos se van. Para datos que tienen que persistir hay tres opciones:
| Tipo | Dónde se guarda | Cuándo usarlo |
|---|---|---|
| Volumen con nombre (volume) | Carpeta gestionada por Docker (/var/lib/docker/volumes dentro de WSL2) | Datos de aplicaciones y bases de datos en producción. Lo normal. |
| Bind mount | Una ruta concreta de tu sistema de archivos del anfitrión | Desarrollo: editas un archivo en Windows y el contenedor lo ve al instante |
| tmpfs | RAM (no se escribe en disco) | Datos sensibles que no deben tocar disco, o archivos temporales que no quieres persistir |
0.7 - Metodología de aprendizaje y de diagnóstico
Para no perderte, ve siempre de menos a más:
- Lanzar contenedores ya hechos (Parte 2): nginx, hello-world, ubuntu. Te acostumbras a la sintaxis de
docker runsin nada más. - Persistir datos (Parte 3): cuando ya tienes contenedores que arrancan, aprendes a no perder el trabajo. Volúmenes y bind mounts.
- Construir tu propia imagen (Parte 4): cuando dominas lo anterior, pasas a Dockerfile.
- Diagnóstico (Parte 5): provocas fallos y los resuelves siguiendo el orden de la práctica 32 - de lo barato y rápido a lo caro y lento.
- Orquestación básica (Parte 6): cuando tienes que arrancar dos o tres contenedores que hablan entre sí, Compose te ahorra escribir muchos comandos.
docker run, docker ps y docker logs. Si dominas esos tres comandos resuelves casi todo. El resto son detalles que vas aprendiendo cuando aparecen.
Parte 1 - Instalar WSL2 y Docker Desktop (45 min)
Vas a preparar tu Windows 11 anfitrión. Es la única parte de la práctica que toca configuración del sistema; el resto es trabajo con comandos.
1.1 - Comprobar requisitos
- Versión de Windows. Abre PowerShell y ejecuta
winver. Necesitas Windows 11 (cualquier edición, incluida Home). - Virtualización en BIOS/UEFI. Abre el Administrador de tareas (
Ctrl+Shift+Esc) → pestaña Rendimiento → CPU. En la parte inferior derecha aparece "Virtualización: Habilitada / Deshabilitada". Si está deshabilitada, hay que entrar a la UEFI al arrancar (F2/Supr/F10 según marca) y activar VT-x (Intel) o SVM/AMD-V (AMD). - RAM y disco. Mínimo razonable: 8 GB de RAM y 20 GB libres en C:.
| Versión de Windows 11 (winver) | |
| Virtualización (Admin. tareas) | |
| RAM total | |
| Espacio libre en C: |
1.2 - Instalar WSL2
Desde Windows 11 instalar WSL2 es un solo comando. Abre PowerShell como administrador:
wsl --install
Este comando hace varias cosas a la vez: activa las características de Windows necesarias (Plataforma de máquina virtual y Subsistema de Windows para Linux), descarga el kernel de WSL2, lo establece como predeterminado, y descarga e instala Ubuntu como distribución por defecto. Es probable que te pida reiniciar.
Tras reiniciar, al abrir Ubuntu por primera vez te pedirá crear un usuario y una contraseña para esa distribución Linux. Apunta el nombre de usuario.
- Verifica que WSL2 está activo:
wsl --status wsl --list --verbose
En la salida de wsl -l -v tienes que ver tu Ubuntu con VERSION 2. Si pone VERSION 1, conviértelo:
wsl --set-version Ubuntu 2
| Distribución instalada | |
| Versión de WSL | |
| Usuario creado en Ubuntu |
1.3 - Descargar e instalar Docker Desktop
- Abre el navegador y ve a docker.com/products/docker-desktop. Descarga el instalador para Windows (Intel chip o ARM según tu equipo).
- Ejecuta el instalador. En la pantalla de configuración deja marcada la opción Use WSL 2 instead of Hyper-V. Esto es crítico: si no la marcas, Docker Desktop intentará usar Hyper-V puro y necesitarás Windows 11 Pro. Con WSL2 funciona también en Home.
- Cuando termine, cierra sesión de Windows y vuelve a entrar (lo pide el instalador).
- Arranca Docker Desktop desde el menú Inicio. Acepta los términos y, si te pide tutorial, sáltalo (lo vas a hacer aquí).
- Espera al icono de la ballena en la bandeja del sistema. Cuando deje de animarse y ponga "Docker Desktop is running", está listo.
1.4 - Comprobar que Docker funciona
Abre una nueva PowerShell (no la elevada anterior, una normal). Ejecuta los siguientes comandos uno a uno y anota la versión que reportan:
docker version docker info docker run hello-world
El último comando descarga la imagen hello-world (un par de MB) y la ejecuta. Si todo va bien, ves un mensaje que empieza por "Hello from Docker!" explicando los pasos que acaba de hacer (cliente, daemon, registry, contenedor).
| Versión de Docker client | |
| Versión de Docker server (daemon) | |
Server OS/Arch (qué dice docker info en "Operating System") | |
| ¿Salió correctamente el mensaje de hello-world? |
1.5 - Códigos de error frecuentes en la instalación
| Mensaje | Causa probable | Solución |
|---|---|---|
| "WSL 2 installation is incomplete" | Falta el paquete del kernel de WSL2 | Ejecutar wsl --update en PowerShell |
| "Hardware assisted virtualization and data execution protection must be enabled" | Virtualización deshabilitada en BIOS | Entrar en UEFI y activarla |
| "Docker Desktop is starting..." que no termina nunca | WSL2 lento al arrancar o conflicto con antivirus | Esperar 2 min, reiniciar Docker Desktop, comprobar antivirus |
| "error during connect: ... The system cannot find the file specified" | Cliente docker está, pero el daemon no responde | Abrir Docker Desktop manualmente y esperar al "running" |
| "unauthorized: incorrect username or password" al hacer pull | Estás intentando bajar una imagen privada sin login | docker login o usar imagen pública |
1.6 - Recursos asignados a WSL2
Docker Desktop limita por defecto cuánta RAM y CPU puede usar WSL2. Para verlo y ajustarlo: icono de la ballena → Settings → Resources.
Si vas a trabajar con varios contenedores a la vez, sube la RAM a 4-6 GB. Si tu equipo tiene poca, déjalo en 2 GB y arranca pocos contenedores a la vez.
¿Por qué Docker Desktop necesita WSL2 (o Hyper-V) para correr contenedores Linux en Windows? ¿Por qué no puede ejecutarlos directamente sobre el kernel de Windows?
Parte 2 - Imágenes y contenedores básicos (60 min)
Aquí entras en el flujo real: descargar imágenes, arrancar contenedores, mirarlos, pararlos, borrarlos. Todo desde PowerShell. Mantén una ventana abierta durante toda esta parte.
2.1 - Descargar imágenes (pull) y listarlas
docker pull nginx docker pull ubuntu docker pull mariadb:11 docker images
Observa la salida de docker images. Cada imagen tiene un REPOSITORY, un TAG (versión; latest si no especificas), un IMAGE ID corto y un SIZE.
El concepto del tag es importante: nginx es equivalente a nginx:latest, pero "latest" no es ninguna garantía de estabilidad: simplemente es la etiqueta que el mantenedor ha asignado a la última versión que él considera estable. En producción siempre se fija una versión concreta (nginx:1.25.3-alpine) para que el comportamiento no cambie sin avisar.
| Tamaño de la imagen nginx | |
| Tamaño de la imagen ubuntu | |
| Tamaño de la imagen mariadb | |
| ¿Cuál es la más pequeña? ¿Por qué crees que es así? |
2.2 - Tu primer contenedor: nginx en primer plano
docker run --rm -p 8080:80 nginx
Lee la línea entera antes de pulsar enter. Significa:
docker run: crea y arranca un contenedor.--rm: borra automáticamente el contenedor cuando se pare. Útil para pruebas; sin esto se quedan acumulados.-p 8080:80: publica el puerto. Lado izquierdo = puerto en tu Windows; lado derecho = puerto dentro del contenedor. El nginx escucha en su puerto 80, y tú lo vas a ver enhttp://localhost:8080.nginx: la imagen a usar.
- Ejecuta el comando. La terminal queda "pegada" mostrando los logs de nginx.
- Abre el navegador y entra a
http://localhost:8080. Tienes que ver la página de bienvenida de nginx. - Recarga unas cuantas veces. En la terminal vas viendo las peticiones GET en tiempo real.
- Vuelve a la terminal y pulsa
Ctrl+C. El contenedor se para; como pusiste--rm, se borra solo.
2.3 - El contenedor en segundo plano (detached)
docker run -d --name web1 -p 8080:80 nginx docker ps
Los flags nuevos:
-d: detached, en segundo plano. La terminal te devuelve el control inmediatamente.--name web1: le pones un nombre legible. Sin esto Docker te asigna nombres aleatorios graciosos (silly_einstein, nostalgic_curie...).
docker ps lista contenedores en marcha. Con docker ps -a ves también los parados. Anota lo que devuelve el primero:
| CONTAINER ID corto | |
| STATUS | |
| PORTS |
2.4 - Ver logs y entrar al contenedor
docker logs web1 docker logs -f web1 # streaming, Ctrl+C para salir docker logs --tail 20 web1
docker logs muestra todo lo que el contenedor ha escrito a stdout/stderr. Para nginx, eso son las peticiones HTTP. Esta es la primera herramienta de diagnóstico que usarás siempre que un contenedor "no funciona".
Para entrar dentro del contenedor (como si fuese SSH a un Linux):
docker exec -it web1 bash # ahora estás dentro del contenedor cat /etc/nginx/nginx.conf | head -10 ls /usr/share/nginx/html exit # vuelves a Windows
Los flags -it son interactive + tty: necesarios para tener una shell interactiva. El comando final (bash) es lo que quieres ejecutar dentro del contenedor.
run y exec: run crea un contenedor nuevo a partir de una imagen. exec ejecuta un comando en un contenedor que ya está en marcha. Confundirlos es un error muy típico de principiante.
2.5 - Parar, arrancar, reiniciar y borrar
docker stop web1 docker ps # ya no aparece docker ps -a # sí aparece, con STATUS "Exited" docker start web1 docker restart web1 docker stop web1 docker rm web1 # ahora sí desaparece del todo docker ps -a
Si intentas docker rm sobre un contenedor en marcha, falla. Para forzarlo: docker rm -f web1 (lo para y lo borra en un solo paso).
2.6 - Variables de entorno y bases de datos
La mayoría de imágenes oficiales se configuran por variables de entorno. Vas a levantar un MariaDB:
docker run -d --name db1 -e MARIADB_ROOT_PASSWORD=secreto -p 3306:3306 mariadb:11 docker logs db1 | tail -20 # espera a ver "ready for connections"
Para comprobar que funciona, entra al contenedor y abre el cliente:
docker exec -it db1 mariadb -uroot -psecreto # dentro del cliente: SHOW DATABASES; CREATE DATABASE pruebas; SHOW DATABASES; EXIT;
Acabas de levantar un servidor MariaDB completo en menos de un minuto, sin instalar nada en tu Windows. Cuando pares y borres el contenedor todo desaparece (lo que es bueno para pruebas y malo si querías guardar la BD; eso lo resuelve la Parte 3).
2.7 - Limpieza periódica
Las imágenes y contenedores acumulados se comen el disco. Para limpiar:
docker ps -a # lista contenedores (también parados) docker rm $(docker ps -aq) # borra todos los contenedores (en PowerShell: docker rm @(docker ps -aq)) docker images # lista imágenes docker rmi nombre_imagen docker system prune # borra contenedores parados, redes no usadas y caché de build docker system prune -a --volumes # más agresivo: incluye imágenes sin contenedor y volúmenes huérfanos
El system prune -a --volumes es la opción nuclear. Útil al final del trimestre cuando tu C: está al 90%; peligroso en un servidor con datos importantes.
$(...) existe pero los flags se evalúan distinto. Si docker rm $(docker ps -aq) te falla, usa la forma de PowerShell: docker rm @(docker ps -aq) o bien docker ps -aq | ForEach-Object { docker rm $_ }.
Espacio que recupera docker system prune -a en tu equipo |
Parte 3 - Persistencia y redes (50 min)
El sistema de archivos de un contenedor es desechable. Si quieres que la BD sobreviva al docker rm, tienes que sacar los datos fuera.
3.1 - Volumen con nombre (la opción habitual)
docker volume create datos_db docker volume ls docker volume inspect datos_db
Y ahora arranca MariaDB pidiéndole que guarde los datos en ese volumen:
docker run -d --name db2 -e MARIADB_ROOT_PASSWORD=secreto ` -v datos_db:/var/lib/mysql -p 3307:3306 mariadb:11
(En PowerShell el acento grave ` sirve para partir un comando en varias líneas. En CMD se usa ^. En una sola línea también funciona.)
El flag -v datos_db:/var/lib/mysql dice "monta el volumen datos_db en la ruta /var/lib/mysql dentro del contenedor". MariaDB guarda sus datos justo ahí, así que ahora los datos viven en el volumen, no en el contenedor.
Pruébalo:
- Crea una base de datos dentro de
db2(igual que en 2.6). - Borra el contenedor:
docker rm -f db2. - Crea otro contenedor montando el mismo volumen:
docker run -d --name db3 -e MARIADB_ROOT_PASSWORD=secreto -v datos_db:/var/lib/mysql -p 3307:3306 mariadb:11. - Entra al cliente y comprueba que la base de datos que habías creado sigue ahí.
| Comando exacto que confirmó que los datos sobrevivieron | |
¿Dónde guarda Docker el volumen físicamente? (pista: docker volume inspect → Mountpoint) |
3.2 - Bind mount (la opción de desarrollo)
Un bind mount monta una carpeta de tu Windows dentro del contenedor. Lo que edites en Windows lo ve el contenedor al instante. Ideal para servir tu propia web:
- Crea una carpeta en tu equipo, por ejemplo
C:\docker\web. - Dentro, crea un
index.htmlcon el texto que quieras:<h1>Hola desde mi bind mount</h1>
- Levanta un nginx montando esa carpeta como su raíz web:
docker run -d --name web2 -v C:\docker\web:/usr/share/nginx/html:ro -p 8081:80 nginx
El :ro al final monta la carpeta de sólo lectura, lo que es buena práctica cuando el contenedor no debe poder modificar tus archivos. Si quieres permitir escritura, omítelo.
Abre http://localhost:8081 y verás tu HTML. Ahora edita el HTML desde Notepad y recarga la página: cambia al instante.
node_modules con 30 mil archivos) la diferencia es enorme. La práctica habitual con stacks de desarrollo en Docker en Windows es poner el código dentro de WSL2 (en \\wsl$\Ubuntu\home\tu_usuario) y ahí el rendimiento es nativo.
3.3 - Red por defecto y red personalizada
Lista las redes que Docker creó al instalarse:
docker network ls
Por defecto verás tres: bridge, host y none. bridge es donde se conectan tus contenedores si no especificas nada.
Problema con la bridge por defecto: los contenedores no pueden encontrarse entre sí por nombre. Para que web pueda conectarse a db usando "db" como hostname, hay que crear una red personalizada:
docker network create miapp docker network ls docker network inspect miapp
3.4 - Conectar dos contenedores
Vas a hacer un mini-stack: una BD MariaDB y un contenedor cliente que se conecta a ella usando el nombre.
docker run -d --name db --network miapp -e MARIADB_ROOT_PASSWORD=secreto mariadb:11 docker run --rm -it --network miapp mariadb:11 mariadb -hdb -uroot -psecreto
Fíjate en dos cosas:
- La BD no exporta el puerto al anfitrión (no hay
-p): no hace falta, porque sólo le va a hablar otro contenedor dentro de la misma red. - El cliente le habla a
-h db: db es el nombre del contenedor, y Docker resuelve eso a su IP automáticamente dentro de la red miapp.
Esta es la base de cómo hablan los servicios containerizados en producción. En Compose y en Kubernetes el mecanismo es el mismo: nombre del servicio → resolución DNS interna.
¿Por qué exponer un puerto con -p sólo tiene sentido si quieres acceder al contenedor desde fuera (el host o internet)? ¿Qué pasaría si publicas 3306 a internet con la contraseña "secreto"?
Parte 4 - Construir tu propia imagen con Dockerfile (40 min)
Hasta aquí has usado imágenes oficiales. En el trabajo real lo normal es que tengas que construir la imagen de tu aplicación. Un Dockerfile es la receta para hacerlo.
4.1 - Anatomía de un Dockerfile
| Directiva | Qué hace |
|---|---|
FROM | Imagen base. La primera línea de cualquier Dockerfile. |
WORKDIR | Cambia el directorio de trabajo dentro de la imagen. Equivale a cd. |
COPY | Copia archivos desde tu equipo a la imagen. |
RUN | Ejecuta un comando al construir la imagen. Útil para instalar paquetes, compilar, etc. |
EXPOSE | Documenta qué puerto escucha la app. No publica nada; es informativo (publicar se hace con -p al correr). |
ENV | Define variables de entorno persistentes en la imagen. |
CMD | Qué comando se ejecuta cuando arranca el contenedor. Sólo se ejecuta una vez por contenedor. |
ENTRYPOINT | Variante de CMD: define el ejecutable; CMD aporta argumentos por defecto. |
4.2 - Tu primer Dockerfile
Vas a crear una mini-web personalizada empaquetada en su propia imagen.
- Crea una carpeta nueva:
C:\docker\miweb. - Dentro pon dos archivos:
index.htmlcon cualquier contenido, yDockerfile(sin extensión) con este contenido:
FROM nginx:1.25-alpine COPY index.html /usr/share/nginx/html/index.html EXPOSE 80 # CMD está implícito en la imagen base (arranca nginx). No hace falta repetirlo.
Construye la imagen:
cd C:\docker\miweb docker build -t miweb:1.0 .
El punto final es importante: es el contexto de build, la carpeta cuyo contenido Docker va a tener disponible para copiar. La -t miweb:1.0 es el tag (nombre + versión).
Arranca un contenedor de tu imagen y comprueba que sirve tu HTML:
docker run -d --name miweb1 -p 8082:80 miweb:1.0 # abre http://localhost:8082
4.3 - Capas y caché
Cada directiva de un Dockerfile crea una capa. Docker cachea las capas: si construyes la imagen dos veces seguidas sin cambiar nada, la segunda vez es instantánea ("Using cache" en cada paso).
La regla práctica de oro: pon las directivas que cambian poco al principio del Dockerfile, y las que cambian mucho al final. Así aprovechas la caché.
Ejemplo: en una app Python, el patrón canónico es
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "main.py"]
Razón: las dependencias (requirements.txt) cambian poco; el código de la app cambia constantemente. Si pusieras COPY . . antes del pip install, cada vez que tocases un comentario en tu .py se reinstalarían todas las dependencias.
4.4 - .dockerignore
El contexto de build (el "." final) se envía completo al daemon antes de empezar. Si tu carpeta tiene un .git de 1 GB o un node_modules gordo, el build empieza copiando todo eso aunque no lo necesites.
Solución: crea un .dockerignore en la raíz con la sintaxis de .gitignore:
.git node_modules *.log .env Dockerfile.dev
4.5 - Buenas prácticas de Dockerfile
- Imagen base ligera: elige variantes
-alpineo-slimcuando puedas. La nginx oficial pasa de 200 MB a 50 MB usando Alpine. - Una sola responsabilidad por contenedor: no metas nginx + PHP-FPM + MySQL en la misma imagen. Cada cosa en un contenedor.
- No corras como root: usa
USER nodeusertras instalar. Reduce el daño si alguien escapa del contenedor. - Tags semánticos:
miapp:1.2.3, no sólomiapp:latest. - Limpia en la misma capa:
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*(todo en un mismo RUN para que la limpieza esté en la misma capa).
docker image history miweb:1.0. Te muestra cada capa con su tamaño. Es la mejor forma de detectar capas innecesariamente gordas y entender qué ocupa tu imagen.
Tamaño de tu imagen miweb:1.0 | |
Número de capas que muestra docker image history |
Parte 5 - Provocar y diagnosticar tus propios fallos (90 min)
Igual que en la práctica 32, vas a provocar tú mismo cinco fallos típicos, simular que los acabas de encontrar y aplicar la metodología de diagnóstico paso a paso. Después comparas el resultado con lo que sabes que rompiste.
docker rm -f para limpiarlos.
5.1 - Caso A - Puerto ya en uso
Qué rompes: levantas dos contenedores que intentan publicar el mismo puerto en Windows. El segundo no arranca.
- Asegúrate de que tienes un nginx en marcha con el puerto 8080:
docker run -d --name webA -p 8080:80 nginx. Comprueba que responde enhttp://localhost:8080. - Intenta arrancar otro al mismo puerto:
docker run -d --name webB -p 8080:80 nginx
Verás un error del tipo "Error response from daemon: ports are not available: ... bind: An attempt was made to access a socket in a way forbidden by its access permissions".
Diagnostica como si no supieras qué pasó:
- Síntoma: el comando
docker runfalla con un mensaje sobre puertos. docker ps: ¿hay otro contenedor publicando el mismo puerto? Mira la columna PORTS.- Si no es otro contenedor, busca qué proceso de Windows está usando el puerto:
netstat -ano | findstr :8080
El PID que devuelve lo cruzas con el Administrador de tareas (pestaña Detalles, columna PID) para saber el ejecutable. - Comprobación cruzada: el contenedor que sí arrancó es el que viste en
docker psen el paso 2.
Repara: tres opciones, según el caso real:
- Cambiar el puerto del segundo contenedor:
docker run -d --name webB -p 8090:80 nginx. - Parar el primero si ya no lo necesitas:
docker stop webA. - Si el puerto lo ocupaba un proceso de Windows, cerrar ese proceso o cambiar el puerto del contenedor.
| Mensaje exacto que devolvió docker | |
| ¿Era otro contenedor o un proceso de Windows? | |
| Solución aplicada |
5.2 - Caso B - Contenedor que se cierra inmediatamente
Qué rompes: arrancas un contenedor con un CMD que falla. El contenedor pasa de "Up" a "Exited" en menos de un segundo y parece que "no funciona Docker".
docker run -d --name caja_rota ubuntu echo "hola" docker ps
docker ps no muestra ese contenedor. Parece que no se creó. ¿Se ha perdido?
Diagnostica:
docker ps -a: aparece, con STATUS "Exited (0) hace 5 segundos". Pista clave: el código de salida 0 = terminó correctamente, no es un error real.docker logs caja_rota: muestra hola y nada más. Confirmas que ejecutó lo que pediste y terminó.- Explicación: un contenedor vive mientras viva su proceso principal. Le pediste a Ubuntu que ejecutara
echo "hola", lo hizo, el proceso terminó, el contenedor se paró. No es un fallo; es el comportamiento esperado.
Reproduce ahora un fallo real:
docker run -d --name caja_rota2 ubuntu comando_que_no_existe docker ps -a docker logs caja_rota2
Verás STATUS "Exited (127)" y en los logs "exec: 'comando_que_no_existe': executable file not found in $PATH". El código 127 en convenciones POSIX es "comando no encontrado".
Repara: arregla el CMD para que ejecute algo real. Si quieres un Ubuntu vivo para hacer pruebas, usa docker run -it --rm ubuntu bash: el -it mantiene la shell abierta como proceso principal.
STATUS de caja_rota | |
STATUS y código de salida de caja_rota2 | |
| Comando que produce un contenedor Ubuntu que se mantiene en marcha |
5.3 - Caso C - Bind mount con permisos / archivo no encontrado
Qué rompes: intentas montar una ruta de Windows que no existe o que está mal escrita. El contenedor arranca, pero la web sirve "403 Forbidden" o el directorio sale vacío.
- Monta una carpeta inexistente:
docker run -d --name webC -v C:\docker\carpeta_que_no_existe:/usr/share/nginx/html:ro -p 8083:80 nginx
Docker no se queja: crea la carpeta vacía y monta una carpeta vacía dentro del contenedor. La web abre pero da 403 o "index of /" vacío.
Diagnostica:
- Síntoma: la web devuelve 403 Forbidden o "Index of /" sin archivos.
docker exec -it webC ls /usr/share/nginx/html: la carpeta dentro está vacía. Aquí confirmas que el montaje "funcionó" pero la carpeta origen no tenía nada útil.docker inspect webC --format '{{json .Mounts}}': te muestra el Source exacto. Compáralo con la ruta que querías montar; lo más común es un typo (carpeta_que_no_existe en lugar de web) o una mayúscula mal.- Mira tu unidad C: en el Explorador. Verás que Docker te creó la carpeta vacía. Este es el comportamiento "amable" que confunde: Docker crea el origen si no existe en vez de fallar.
Repara: borra el contenedor, comprueba la ruta correcta en el Explorador, vuelve a arrancarlo con la ruta buena.
Source que reportó docker inspect | |
| Causa concreta del fallo |
5.4 - Caso D - Contenedor que llena el disco
Qué rompes: arrancas un contenedor que genera muchos logs sin parar. Es una causa real de incidentes en producción: un servicio en debug log durante semanas y el disco lleno sin que nadie se entere.
docker run -d --name spam ubuntu bash -c "while true; do echo 'gasto disco con logs sin parar'; sleep 0.1; done"
Déjalo unos minutos y mira:
docker ps -s # añade columna SIZE
docker inspect spam --format '{{.LogPath}}'
docker logs spam | wc -l # cuántas líneas lleva (en PowerShell: (docker logs spam).Count)
Verás que el campo SIZE no crece (el contenedor en sí no escribe a disco), pero el archivo de logs (LogPath) sí. Va dentro de WSL2, en /var/lib/docker/containers/<id>/<id>-json.log.
Diagnostica:
- Síntoma: el disco del anfitrión va menguando sin razón aparente.
docker ps --size: identifica contenedores grandes.docker system df: te dice cuánto ocupan imágenes, contenedores, volúmenes y caché de build.- Si un contenedor concreto sospechoso tiene muchísimos logs, ese es el culpable.
Repara:
- Parar y borrar el contenedor problemático:
docker rm -f spam. - Limitar logs en el futuro: al arrancar el contenedor pásale
--log-opt max-size=10m --log-opt max-file=3. Eso le pone tope de tamaño y rotación. - O configurarlo a nivel global en Docker Desktop → Settings → Docker Engine añadiendo al JSON:
"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}.
| Tamaño del archivo de logs al detectarlo | |
| Comando exacto con límite de logs |
5.5 - Caso E - Dockerfile que falla en build
Qué rompes: creas un Dockerfile con un error típico (paquete que no existe, archivo que no se copia bien, sintaxis mal). El build casca y hay que leer el error.
- Crea una carpeta
C:\docker\roto. - Dentro, un Dockerfile con este contenido:
FROM ubuntu:24.04 RUN apt-get update && apt-get install -y paquete_inventado_xyz COPY index.html /var/www/html/ CMD ["/bin/sh", "-c", "tail -f /dev/null"]
Sin crear el index.html a propósito, lanza el build:
cd C:\docker\roto docker build -t roto:1.0 .
Diagnostica:
- El build se para en la línea del RUN. Mira el error: "E: Unable to locate package paquete_inventado_xyz". Eso es lo más fácil: nombre de paquete incorrecto.
- Arregla esa línea (cámbiala por
curlu otro paquete real) y vuelve a construir. - Ahora el build se para en el COPY: "failed to compute cache key: '/index.html' not found". Pista: el archivo no existe en el contexto.
- Crea un
index.htmlvacío en la carpeta y vuelve a construir. Esta vez termina.
Idea de diagnóstico clave: los errores de docker build siempre te dicen qué línea del Dockerfile ha fallado ("ERROR [3/4] COPY..."). El número entre corchetes es el paso. Cuenta líneas en tu Dockerfile y verás cuál es. Eso elimina el 90% del trabajo de diagnóstico.
| Primer error que dio el build | |
| Segundo error tras la primera corrección | |
| ¿En qué paso del Dockerfile estaba cada uno? |
5.6 - Tabla resumen de los cinco casos
| Caso | Tiempo total (min) | Comando de diagnóstico que dio la pista clave | ¿Lo resolverías hoy en 5 minutos? |
|---|---|---|---|
| A - Puerto en uso | |||
| B - Contenedor que se cierra | |||
| C - Bind mount mal | |||
| D - Logs llenan disco | |||
| E - Build roto |
docker rm -f y comprueba que efectivamente los datos se han perdido. Después repite el experimento con volumen y verifica que sobreviven. Documenta los dos casos con la misma estructura que los anteriores.
Parte 6 - Docker Compose: varios contenedores con un solo archivo (30 min)
Hasta ahora has lanzado cada contenedor con su propio docker run y sus flags. Para uno o dos sueltos es razonable, pero cuando un entorno necesita arrancar a la vez web + BD + Redis + worker, escribir cuatro docker run largos cada vez que reinicias el equipo es repetitivo y se cometen errores (flags olvidados, redes mal conectadas, orden de arranque...). Docker Compose resuelve eso: describes todo el conjunto en un archivo YAML versionable y lo levantas o tiras con un solo comando.
6.1 - Antes del YAML: tres reglas mínimas del formato
YAML es un formato de texto plano para describir datos estructurados. Lo usan Docker Compose, Kubernetes, GitHub Actions y muchas otras herramientas. Con tres reglas tienes suficiente para esta práctica:
- Indentación con espacios, no con tabuladores, y todos los hijos del mismo padre con la misma cantidad de espacios (lo normal: 2 espacios). Si mezclas tabs y espacios, falla y el mensaje no siempre es claro.
clave: valorsepara con dos puntos seguidos de un espacio.- Las listas se marcan con un guión y un espacio:
- elemento.
6.2 - Crear los archivos del proyecto
Vas a montar un stack pequeño: un servidor web nginx que sirve un HTML estático y una base de datos MariaDB. Al principio la web no usa la BD (es estática), pero levantar las dos juntas te servirá para ver la red interna que crea Compose entre los contenedores. Abre PowerShell y crea la estructura:
New-Item -ItemType Directory -Path C:\docker\compose-demo\html -Force Set-Location C:\docker\compose-demo
Esto crea C:\docker\compose-demo\ con una subcarpeta html\ dentro, y te deja con esa carpeta como directorio actual. Comprueba con Get-Location (alias pwd) que estás dentro.
Crea el archivo del stack con tu editor:
code docker-compose.yml # si usas VS Code # o si no: notepad docker-compose.yml
Pega este contenido y guárdalo:
services:
web:
image: nginx:1.25-alpine
ports:
- "8090:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- db
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: secreto
MARIADB_DATABASE: pruebas
volumes:
- datos_db:/var/lib/mysql
volumes:
datos_db:
Crea ahora html\index.html con cualquier contenido (lo importante es que el archivo exista para que nginx tenga algo que servir). Si no se te ocurre nada, pega esto:
<!DOCTYPE html> <html lang="es"> <body> <h1>Hola desde Docker Compose</h1> <p>Esta página la sirve un contenedor nginx levantado con docker compose up.</p> </body> </html>
6.3 - El docker-compose.yml por bloques
Antes de levantarlo, asegúrate de entender qué hace cada bloque. Esta es la parte que vas a leer y modificar mil veces en tu vida laboral:
| Bloque | Qué significa |
|---|---|
services: | Sección donde se declaran los contenedores del stack. Cada hijo (web, db) es un servicio; el nombre que le pongas será también el nombre por el que el resto de contenedores podrá encontrarlo por red. |
image: | Qué imagen usar. Aquí nginx:1.25-alpine (servidor web ligero) y mariadb:11 (base de datos). Si no las tienes localmente, Compose las descarga sola la primera vez. |
ports: | Publicación de puertos al anfitrión, formato "puerto_windows:puerto_contenedor". "8090:80" = abre el 8090 en tu Windows y reenvíalo al 80 de nginx. La BD no publica ningún puerto a propósito: sólo será accesible desde otros contenedores del mismo stack, que es lo seguro. |
volumes: en web | Bind mount: ./html:/usr/share/nginx/html:ro monta la carpeta html de tu Windows dentro del contenedor en la ruta donde nginx busca el sitio, en modo solo lectura (:ro). |
volumes: en db | Named volume: datos_db:/var/lib/mysql usa un volumen llamado datos_db gestionado por Docker para que los datos de la BD sobrevivan si paras y vuelves a arrancar. |
environment: | Variables de entorno que el contenedor recibe al arrancar. MARIADB_ROOT_PASSWORD y MARIADB_DATABASE son parámetros estándar de la imagen mariadb: configuran la contraseña de root y crean una base de datos vacía con ese nombre en el primer arranque. |
depends_on: | Orden de arranque: web no arranca hasta que el contenedor db esté creado. Ojo: no espera a que MariaDB esté lista para aceptar conexiones, sólo a que el contenedor exista. Para esperar "lista de verdad" hace falta healthcheck, que queda fuera del alcance de esta práctica. |
volumes: al final del archivo (en la raíz, no dentro de un servicio) | Declaración del volumen con nombre datos_db. Si lo usas en un servicio tienes que declararlo aquí, si no Compose se queja. |
6.4 - Levantar el stack
Desde C:\docker\compose-demo:
docker compose up -d docker compose ps
El primer comando descarga las imágenes (la primera vez tarda unos minutos), crea la red, los volúmenes y los contenedores, y los arranca en segundo plano (-d = detached). El segundo te muestra el estado: dos servicios, ambos en running.
Compose ha hecho varias cosas a la vez:
- Ha creado una red propia llamada
compose-demo_default(Compose crea una red por proyecto, y el nombre del proyecto por defecto es el de la carpeta). - Ha levantado dos contenedores con nombres predecibles: compose-demo-web-1 y compose-demo-db-1.
- Ha creado el volumen
compose-demo_datos_dby lo ha montado en la BD. - Ha respetado
depends_onarrancando primero db y después web.
Echa un vistazo a los logs para confirmar que MariaDB ha arrancado bien:
docker compose logs db docker compose logs -f web # Ctrl+C para salir del modo seguir
6.5 - Verificar que funciona
Tres comprobaciones rápidas para ver que el stack está vivo y, sobre todo, que los dos contenedores comparten una red interna donde se encuentran por nombre:
- nginx sirve tu HTML. Abre el navegador en
http://localhost:8090. Debes ver el "Hola desde Docker Compose" del archivo que creaste. - La BD responde y existe la base "pruebas".
docker compose exec db mariadb -uroot -psecreto -e "SHOW DATABASES;"
Debe listar information_schema, mysql, performance_schema, sys y la tuya, pruebas. - El contenedor web "ve" al contenedor db por su nombre de servicio. Esto es lo realmente potente de Compose: la red interna trae DNS automático para los nombres de los servicios.
docker compose exec web ping -c 2 db # si tu imagen no tuviera ping, vale también: docker compose exec web nslookup db
Debes ver una IP del rango 172.x.x.x respondiendo. Si tuvieras una aplicación PHP/Node/Python en el contenedor web, podría conectar a la BD usandodbcomo hostname, sin saber su IP. Eso es lo que hace utilísimo Compose.
ports: en el servicio db, la BD queda aislada del exterior.
6.6 - Parar y limpiar
docker compose down # para y borra contenedores y la red, conserva los volúmenes (los datos sobreviven) docker compose down -v # incluye los volúmenes (pierdes los datos de la BD)
Tras un docker compose down normal puedes hacer otra vez docker compose up -d y volverás a tener el stack con los datos intactos. Es la forma habitual de trabajar día a día.
6.7 - Operaciones habituales de Compose
| Comando | Para qué |
|---|---|
docker compose up -d | Levanta el stack en segundo plano |
docker compose ps | Estado de los servicios del proyecto actual |
docker compose logs -f | Logs combinados de todos los servicios en streaming |
docker compose exec web sh | Shell dentro del servicio web |
docker compose restart db | Reinicia un servicio |
docker compose pull | Actualiza las imágenes a la última versión disponible |
docker compose down | Para y limpia (sin borrar volúmenes) |
6.8 - Compose en el día a día
En proyectos reales el docker-compose.yml suele estar en el repositorio git del proyecto y es el primer archivo que un nuevo compañero ejecuta el día 1 para tener el entorno listo. Saber leerlo, entender cada bloque y modificarlo (cambiar una imagen, añadir una variable de entorno, abrir un puerto) es una habilidad de empleabilidad pura.
| URL que abriste en el navegador y resultado obtenido | |
| Comando con el que listaste las bases de datos dentro del contenedor db | |
| Comando con el que comprobaste que el contenedor web resuelve el nombre db por la red de Compose | |
Nombre del volumen que Compose creó en tu sistema (lo ves con docker volume ls) |
Parte 7 - Documentar como un profesional (25 min)
Igual que en la práctica 32, rellena una ficha completa para el caso que más te haya costado de la Parte 5. La estructura sirve para cualquier incidencia que documentes en el futuro, tenga o no que ver con Docker.
FICHA DE INCIDENCIA Nº _______
Conclusiones y autoevaluación
De los cinco fallos que has provocado en la Parte 5, ¿cuál te ha costado más diagnosticar y por qué? ¿Qué comando concreto te dio la pista definitiva?
Un compañero te dice: "tengo una app que funciona en mi portátil pero en el servidor falla". Sabes que ambos usan Docker. Describe los tres primeros pasos que darías para acotar el problema antes de tocar nada.
Diferencia entre una imagen y un contenedor con tus propias palabras. ¿Por qué docker rm borra un contenedor pero la imagen sigue ahí?
Compara la práctica 32 (drivers y diagnóstico de hardware) con esta 33 (contenedores). ¿Qué método de diagnóstico tienen en común y en qué se diferencian las herramientas concretas que usas?
Te encargan montar un entorno de pruebas para que un programador trabaje en una app Python con MariaDB. ¿Por qué usarías Docker y no una VM completa? ¿Qué pondrías en el docker-compose.yml en cinco líneas?
Autoevaluación
| Aspecto | Logrado |
|---|---|
| Entiendo qué es un contenedor y en qué se diferencia de una VM | |
| He instalado WSL2 y Docker Desktop en mi Windows 11 y arrancado hello-world | |
Sé hacer docker pull, docker run, docker ps y entender la salida | |
Manejo docker logs y docker exec para diagnosticar contenedores | |
| Distingo bind mount, volumen con nombre y tmpfs y sé cuándo usar cada uno | |
| He conectado dos contenedores en una red personalizada por nombre de servicio | |
| He escrito mi primer Dockerfile, construido la imagen y servido contenido propio | |
Entiendo el papel de las capas y la caché en docker build | |
| He provocado y resuelto los cinco fallos de la Parte 5 | |
Sé identificar quién ocupa un puerto en Windows con netstat -ano | |
| He levantado un stack multi-servicio con Docker Compose | |
Soy capaz de limpiar el sistema con docker system prune sin miedo a borrar lo importante | |
| He rellenado al menos una ficha de incidencia completa |