← Teoría

Práctica 33: Docker Desktop en Windows 11

Módulo: Práctica adicional - virtualización ligera y contenedores
Relación con el temario: complementa UF0861 (instalación y configuración de SO) y UF0853 (explotación del sistema)
Sesiones: práctica extra
Duración estimada: 7-8 horas (puede partirse en dos sesiones)
Modalidad: trabajo individual
Herramientas: equipo físico con Windows 11 Pro o Home (no VM), virtualización habilitada en BIOS/UEFI, WSL2, Docker Desktop, conexión a internet
De qué va esta práctica: hasta ahora has trabajado dentro de máquinas virtuales con VirtualBox para no tocar tu equipo real. En esta práctica vas a salir de la VM y trabajar directamente sobre tu Windows 11 anfitrión, porque Docker necesita virtualización anidada del hardware (WSL2) y eso se complica mucho dentro de una VM. Vas a instalar Docker Desktop con backend WSL2, levantar contenedores reales (servidores web, bases de datos), construir tus propias imágenes con Dockerfile, gestionar volúmenes y redes, y al final vas a tú mismo a provocar fallos típicos (puerto en uso, contenedor que crece sin parar, build que falla) para diagnosticarlos con la misma metodología que en la práctica 32. Todo el trabajo es con software gratuito y sin hardware adicional.
Por qué Docker fuera del temario oficial: Docker no está en el currículo de IFCT0309 pero es la tecnología más demandada en ofertas de soporte/sysadmin junior desde 2018. Saber arrancar un contenedor para probar software, montar un entorno de desarrollo aislado o sustituir un servicio físico por uno containerizado es una habilidad transversal que se pide a todo perfil técnico. Esta práctica te da una base sólida en una jornada.
Antes de empezar - requisitos imprescindibles: esta práctica no se puede hacer dentro de una VirtualBox con el rendimiento aceptable. Necesitas Windows 11 instalado directamente en hardware (tu portátil del aula o tu equipo personal). Si tu equipo es muy antiguo (CPU sin VT-x/AMD-V, menos de 8 GB de RAM, sin SSD) avisa al profesor: la práctica se podrá seguir mirando los pasos en pareja con un compañero.

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 stop y docker rm con 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, EXPOSE y CMD; construir la imagen con docker 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

AspectoMáquina virtualContenedor
AislamientoTotal (hardware virtualizado)Procesos aislados con namespaces y cgroups
KernelCada VM lleva el suyoComparten el del anfitrión
Arranque30 s a varios minutosDécimas de segundo
Tamaño en disco10-50 GB típico10-500 MB típico
RAM por instancia1-8 GB reservados10-200 MB según uso
Cuántas puedes arrancar a la vez2-5 en un portátil normal20-100 sin despeinarse
Uso típicoSistemas operativos completos, lab de redes, malwareServicios 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:

  1. WSL2 (Windows Subsystem for Linux 2) arranca una VM Linux ligera y oculta gestionada por Hyper-V.
  2. Dentro de esa VM corre el dockerd (daemon de Docker) y por debajo containerd + runc (el runtime que de verdad lanza los contenedores).
  3. Docker Desktop es la aplicación que orquesta todo lo anterior y te da una GUI bonita en Windows.
  4. Cuando ejecutas docker run desde PowerShell o CMD de Windows, el comando viaja a través de un canal a dockerd dentro de WSL2, que arranca el contenedor.
Por qué importa entender esto: cuando algo falla (un contenedor no arranca, un puerto no responde, una carpeta no se monta), la causa suele estar en la "frontera" entre Windows y WSL2: rutas, permisos, firewall, recursos asignados a WSL. Si sabes que hay una VM Linux en medio, los síntomas dejan de parecer mágicos.

0.4 - Vocabulario imprescindible

ConceptoQué esAnalogía
ImagenPlantilla inmutable: el sistema de archivos y la configuración para crear contenedoresEl instalador .iso de Windows
ContenedorInstancia en ejecución (o parada) creada a partir de una imagenEl 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ágenesBandejas transparentes apiladas, ves la suma de todas
RegistryAlmacé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
TagEtiqueta de versión de una imagen (nginx:1.25, nginx:latest, nginx:alpine)El número de versión del paquete
DockerfileReceta en texto plano para construir una imagen propiaUn script de instalación + configuración
VolumenEspacio en disco persistente que sobrevive al contenedorUna carpeta compartida del NAS montada en un PC desechable
RedRed virtual a la que se conectan los contenedores para hablar entre ellosVLAN 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 ligero
  • httpd - Apache
  • mariadb, postgres, redis - bases de datos
  • python, node, php - intérpretes para tus apps
  • ubuntu, 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:

TipoDónde se guardaCuá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 mountUna ruta concreta de tu sistema de archivos del anfitriónDesarrollo: editas un archivo en Windows y el contenedor lo ve al instante
tmpfsRAM (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:

  1. Lanzar contenedores ya hechos (Parte 2): nginx, hello-world, ubuntu. Te acostumbras a la sintaxis de docker run sin nada más.
  2. Persistir datos (Parte 3): cuando ya tienes contenedores que arrancan, aprendes a no perder el trabajo. Volúmenes y bind mounts.
  3. Construir tu propia imagen (Parte 4): cuando dominas lo anterior, pasas a Dockerfile.
  4. 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.
  5. Orquestación básica (Parte 6): cuando tienes que arrancar dos o tres contenedores que hablan entre sí, Compose te ahorra escribir muchos comandos.
Idea clave: el 90% del trabajo diario con Docker es 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

  1. Versión de Windows. Abre PowerShell y ejecuta winver. Necesitas Windows 11 (cualquier edición, incluida Home).
  2. Virtualización en BIOS/UEFI. Abre el Administrador de tareas (Ctrl+Shift+Esc) → pestaña RendimientoCPU. 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).
  3. 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.

  1. 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
Detalle importante: Docker Desktop no necesita que tengas una distribución de Linux instalada (mete la suya propia, docker-desktop), pero tener Ubuntu a mano es muy útil para probar comandos Linux nativos y para entender de dónde vienen las cosas. Déjala instalada.

1.3 - Descargar e instalar Docker Desktop

  1. Abre el navegador y ve a docker.com/products/docker-desktop. Descarga el instalador para Windows (Intel chip o ARM según tu equipo).
  2. 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.
  3. Cuando termine, cierra sesión de Windows y vuelve a entrar (lo pide el instalador).
  4. Arranca Docker Desktop desde el menú Inicio. Acepta los términos y, si te pide tutorial, sáltalo (lo vas a hacer aquí).
  5. Espera al icono de la ballena en la bandeja del sistema. Cuando deje de animarse y ponga "Docker Desktop is running", está listo.
Licencia comercial de Docker Desktop: Docker Desktop es gratuito para uso personal, educativo y open source, y para empresas pequeñas (menos de 250 empleados y menos de 10 M$ de facturación). Empresas más grandes necesitan suscripción de pago. Para esta práctica (curso, uso educativo) estás claramente dentro del uso gratuito.

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

MensajeCausa probableSolución
"WSL 2 installation is incomplete"Falta el paquete del kernel de WSL2Ejecutar wsl --update en PowerShell
"Hardware assisted virtualization and data execution protection must be enabled"Virtualización deshabilitada en BIOSEntrar en UEFI y activarla
"Docker Desktop is starting..." que no termina nuncaWSL2 lento al arrancar o conflicto con antivirusEsperar 2 min, reiniciar Docker Desktop, comprobar antivirus
"error during connect: ... The system cannot find the file specified"Cliente docker está, pero el daemon no respondeAbrir Docker Desktop manualmente y esperar al "running"
"unauthorized: incorrect username or password" al hacer pullEstás intentando bajar una imagen privada sin logindocker 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 → SettingsResources.

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 en http://localhost:8080.
  • nginx: la imagen a usar.
  1. Ejecuta el comando. La terminal queda "pegada" mostrando los logs de nginx.
  2. Abre el navegador y entra a http://localhost:8080. Tienes que ver la página de bienvenida de nginx.
  3. Recarga unas cuantas veces. En la terminal vas viendo las peticiones GET en tiempo real.
  4. 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.

Diferencia entre 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.

En PowerShell, el subshell $(...) 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:

  1. Crea una base de datos dentro de db2 (igual que en 2.6).
  2. Borra el contenedor: docker rm -f db2.
  3. 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.
  4. 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:

  1. Crea una carpeta en tu equipo, por ejemplo C:\docker\web.
  2. Dentro, crea un index.html con el texto que quieras:
    <h1>Hola desde mi bind mount</h1>
  3. 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.

Detalle de Docker Desktop: los bind mounts entre Windows y WSL2 pasan por una traducción de rutas. Funcionan, pero son más lentos que un volumen nativo. Para muchos archivos pequeños (un 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

DirectivaQué hace
FROMImagen base. La primera línea de cualquier Dockerfile.
WORKDIRCambia el directorio de trabajo dentro de la imagen. Equivale a cd.
COPYCopia archivos desde tu equipo a la imagen.
RUNEjecuta un comando al construir la imagen. Útil para instalar paquetes, compilar, etc.
EXPOSEDocumenta qué puerto escucha la app. No publica nada; es informativo (publicar se hace con -p al correr).
ENVDefine variables de entorno persistentes en la imagen.
CMDQué comando se ejecuta cuando arranca el contenedor. Sólo se ejecuta una vez por contenedor.
ENTRYPOINTVariante 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.

  1. Crea una carpeta nueva: C:\docker\miweb.
  2. Dentro pon dos archivos: index.html con cualquier contenido, y Dockerfile (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 -alpine o -slim cuando 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 nodeuser tras instalar. Reduce el daño si alguien escapa del contenedor.
  • Tags semánticos: miapp:1.2.3, no sólo miapp: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).
Truco profesional: ejecuta 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.

Antes de empezar: deja al menos un contenedor de los anteriores parado (no hace falta limpiar nada). Para cada caso vas a usar contenedores nuevos y desechables; al final basta con 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.

  1. 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 en http://localhost:8080.
  2. 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ó:

  1. Síntoma: el comando docker run falla con un mensaje sobre puertos.
  2. docker ps: ¿hay otro contenedor publicando el mismo puerto? Mira la columna PORTS.
  3. 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.
  4. Comprobación cruzada: el contenedor que arrancó es el que viste en docker ps en 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:

  1. 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.
  2. docker logs caja_rota: muestra hola y nada más. Confirmas que ejecutó lo que pediste y terminó.
  3. 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.

  1. 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:

  1. Síntoma: la web devuelve 403 Forbidden o "Index of /" sin archivos.
  2. 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.
  3. 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.
  4. 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:

  1. Síntoma: el disco del anfitrión va menguando sin razón aparente.
  2. docker ps --size: identifica contenedores grandes.
  3. docker system df: te dice cuánto ocupan imágenes, contenedores, volúmenes y caché de build.
  4. Si un contenedor concreto sospechoso tiene muchísimos logs, ese es el culpable.

Repara:

  1. Parar y borrar el contenedor problemático: docker rm -f spam.
  2. 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.
  3. 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.

  1. Crea una carpeta C:\docker\roto.
  2. 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:

  1. 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.
  2. Arregla esa línea (cámbiala por curl u otro paquete real) y vuelve a construir.
  3. 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.
  4. Crea un index.html vací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

CasoTiempo 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
Reto adicional (opcional, +30 min): arranca un MariaDB sin volumen, crea una base de datos con varias tablas y datos, bórralo con 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: valor separa con dos puntos seguidos de un espacio.
  • Las listas se marcan con un guión y un espacio: - elemento.
Cómo editar archivos YAML en Windows: usa Visual Studio Code (gratis, lo verás instalado en muchas empresas) o Notepad++. Evita el Bloc de notas clásico para YAML: a veces guarda con BOM o saltos de línea raros y Compose se queja sin que entiendas el motivo.

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:

BloqueQué 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 webBind 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 dbNamed 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_db y lo ha montado en la BD.
  • Ha respetado depends_on arrancando 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:

  1. nginx sirve tu HTML. Abre el navegador en http://localhost:8090. Debes ver el "Hola desde Docker Compose" del archivo que creaste.
  2. 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.
  3. 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 usando db como hostname, sin saber su IP. Eso es lo que hace utilísimo Compose.
¿Por qué la BD no está publicada en ningún puerto del anfitrión? Porque no hace falta. Sólo el web necesita acceder a ella, y lo hace por la red interna del proyecto. Publicar puertos de bases de datos hacia el exterior ("por si acaso") es uno de los errores más típicos en entornos de desarrollo que acaban expuestos en internet. En Compose, mientras no pongas 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

ComandoPara qué
docker compose up -dLevanta el stack en segundo plano
docker compose psEstado de los servicios del proyecto actual
docker compose logs -fLogs combinados de todos los servicios en streaming
docker compose exec web shShell dentro del servicio web
docker compose restart dbReinicia un servicio
docker compose pullActualiza las imágenes a la última versión disponible
docker compose downPara 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º _______

Por qué documentar Docker tan al detalle: los problemas con contenedores suelen ser "extraños" (un puerto se queda colgado, un volumen no actualiza, una imagen funciona en otro equipo pero no en el tuyo). Sin documentar los pasos exactos, tres meses después no recuerdas qué hiciste. Una ficha bien hecha es la diferencia entre 5 minutos y 2 horas la próxima vez.

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

AspectoLogrado
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
Volver al índice