Llega el momento de la práctica seria. En esta lección no vas a leer teoría: vas a teclear, a equivocarte y a ver resultados en tu navegador. Empezarás con el contenedor más simple posible —uno que imprime algo y muere— para entender el ciclo más corto que existe. Después levantarás un servidor web real en segundo plano, publicarás su puerto y lo visitarás desde el navegador, aprendiendo por el camino qué significa exactamente -p 8080:80, que es probablemente la opción que más veces escribirás en tu carrera. Recorrerás el ciclo completo de ese contenedor: verlo, leer sus logs, pararlo, arrancarlo y borrarlo. Luego entrarás dentro de un contenedor con un shell interactivo para tocar su sistema de archivos aislado con tus propias manos. Y terminarás sirviendo la primera página web de Aurora Libros desde un contenedor, montando un fichero de tu máquina dentro de él.

Contenido

  1. Antes de empezar: comprobaciones
  2. Tu primer contenedor: en primer plano
  3. Qué ocurre cuando el proceso termina
  4. Un contenedor de servicio en segundo plano
  5. El mapeo de puertos explicado
  6. El ciclo completo: logs, stop, start y rm
  7. Un contenedor interactivo: entrar dentro
  8. Sirviendo la página de Aurora Libros con -v
  9. Limpieza final

  1. Antes de empezar: comprobaciones

Asegúrate de que todo está en orden:

docker version
docker ps

El primero debe mostrar los bloques Client y Server. El segundo, una tabla (probablemente vacía) sin errores. Si algo falla, vuelve a la lección 01-02.

Comprueba también que el puerto que vamos a usar está libre:

ss -tln | grep 8080 || echo "Puerto 8080 libre"

ss -tln lista los puertos TCP en escucha (-t TCP, -l en escucha, -n sin resolver nombres). Si no hay coincidencias, grep falla y el || ejecuta el echo. Si algo estuviera escuchando en 8080, usa 8081 en todos los comandos de la lección.

  1. Tu primer contenedor: en primer plano

Empecemos por lo mínimo:

docker run alpine:3.20 echo "Hola desde Aurora Libros"
Hola desde Aurora Libros

Parece trivial, pero han pasado muchas cosas. Desglose de la línea:

Parte Significado
docker run Crea un contenedor nuevo y lo arranca
alpine:3.20 La imagen a partir de la cual crearlo
echo "Hola desde Aurora Libros" El comando a ejecutar dentro del contenedor, sustituyendo al comando por defecto de la imagen

Y la secuencia interna, que ya conoces de la lección 01-03: el cliente llamó al daemon, el daemon comprobó que tenía alpine:3.20 en local, creó un contenedor apilando una capa de escritura sobre las capas de la imagen, pidió a containerd que arrancara el proceso echo, este escribió en su salida estándar, y esa salida se transmitió por el socket hasta tu terminal.

Como no has usado -d, el contenedor se ejecuta en primer plano: tu terminal queda conectada a su salida y no recupera el control hasta que el proceso termina.

Probemos algo un poco más largo:

docker run alpine:3.20 sh -c "echo 'Catálogo:'; echo '- Rayuela'; echo '- El jardín de senderos que se bifurcan'"
Catálogo:
- Rayuela
- El jardín de senderos que se bifurcan

Aquí el comando es sh -c "...": se lanza un shell dentro del contenedor y se le pasa una cadena con varias órdenes. Es el patrón habitual cuando necesitas encadenar comandos, porque docker run solo acepta un ejecutable con sus argumentos, no una línea de shell con ; o &&.

Y algo que dura un rato, para que veas el primer plano de verdad:

docker run alpine:3.20 sh -c "for i in 1 2 3 4 5; do echo \"Procesando pedido \$i\"; sleep 1; done"
Procesando pedido 1
Procesando pedido 2
Procesando pedido 3
Procesando pedido 4
Procesando pedido 5

Las líneas salen de una en una, con un segundo de pausa. Tu terminal está bloqueada mientras tanto: eso es primer plano. Si pulsas Ctrl+C durante la ejecución, envías una señal de interrupción al proceso y el contenedor termina antes de tiempo.

  1. Qué ocurre cuando el proceso termina

Esta es la regla más importante de toda la lección:

Un contenedor vive exactamente lo que vive su proceso principal. Cuando ese proceso termina, el contenedor pasa a estado Exited. No hay nada más que "apagar".

Compruébalo:

docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Vacío: ninguno de los tres contenedores anteriores sigue vivo.

docker ps -a --format "table {{.Names}}\t{{.Command}}\t{{.Status}}"
NAMES               COMMAND                  STATUS
happy_bardeen       "sh -c 'for i in 1 2…"   Exited (0) 10 seconds ago
festive_curie       "sh -c 'echo Catálog…"   Exited (0) 1 minute ago
vibrant_torvalds    "echo 'Hola desde Au…"   Exited (0) 2 minutes ago

Los tres están ahí, parados, con código de salida 0 (terminaron bien). Ocupan un poco de disco cada uno.

Este comportamiento explica un tropiezo clásico de principiantes:

docker run alpine:3.20
docker ps

El segundo comando no muestra nada, y la reacción típica es "el contenedor no arranca". Sí arrancó: la imagen alpine tiene como comando por defecto /bin/sh, un shell que, al no tener terminal interactiva ni entrada, no encuentra nada que hacer y termina inmediatamente. El contenedor hizo su trabajo y murió en milisegundos.

Para que un contenedor siga vivo necesita un proceso que no termine: un servidor web esperando peticiones, una base de datos, o un shell con una terminal interactiva conectada. Eso es lo que veremos ahora.

  1. Un contenedor de servicio en segundo plano

Vamos a levantar un servidor web Nginx, que es exactamente eso: un proceso que se queda escuchando indefinidamente.

docker run -d --name aurora-web-demo -p 8080:80 nginx:alpine
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
f18232174bc9: Already exists
2c8d4f7e1a09: Pull complete
...
Status: Downloaded newer image for nginx:alpine
7c3e9a1f5b2d84e0a6c7d9f3b1e5a7c9d2f4b6e8a0c2d4f6b8e0a2c4d6f8b0e2

Analicemos opción por opción, porque cada una resuelve un problema concreto:

Opción Nombre largo Qué hace
-d --detach Ejecuta el contenedor en segundo plano y te devuelve el control de la terminal inmediatamente, imprimiendo su ID
--name aurora-web-demo Le da un nombre legible en lugar de uno aleatorio. Podrás referirte a él por ese nombre en todos los comandos
-p 8080:80 --publish Publica el puerto 80 del contenedor en el puerto 8080 de tu máquina
nginx:alpine La imagen. No indicamos comando, así que se usa el de la imagen: arrancar Nginx

Esa larga cadena hexadecimal de la última línea es el ID completo del contenedor. Como usaste --name, no te hará falta.

Ahora sí:

docker ps
CONTAINER ID   IMAGE          COMMAND                  STATUS         PORTS                                     NAMES
7c3e9a1f5b2d   nginx:alpine   "/docker-entrypoint.…"   Up 8 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   aurora-web-demo

Up 8 seconds: está vivo, y seguirá vivo porque Nginx no termina nunca por sí solo. Nota que docker ps sin -a ya lo muestra, a diferencia de los contenedores anteriores.

  1. El mapeo de puertos explicado

La columna PORTS merece un apartado propio porque es donde más gente se atasca.

Un contenedor tiene su propia pila de red: su propia interfaz, su propia dirección IP interna y sus propios puertos. Nginx escucha en el puerto 80 de esa red interna, que desde tu máquina no es accesible directamente. La opción -p crea un puente.

flowchart LR
    NAV["Tu navegador<br/>http://localhost:8080"]
    subgraph HOST["Tu máquina (host)"]
        P8080["Puerto 8080<br/>(reservado por Docker)"]
        subgraph NET["Red bridge de Docker · 172.17.0.0/16"]
            subgraph CNT["Contenedor aurora-web-demo · 172.17.0.2"]
                P80["Puerto 80<br/>Nginx escuchando"]
            end
        end
    end
    NAV --> P8080
    P8080 -->|"-p 8080:80<br/>reenvío"| P80

La sintaxis se lee siempre de fuera hacia dentro:

-p PUERTO_DEL_HOST:PUERTO_DEL_CONTENEDOR
  • El primero es el de tu máquina: el que escribes en el navegador. Lo eliges tú y debe estar libre.
  • El segundo es el del contenedor: en el que escucha la aplicación. No lo eliges tú, lo determina la aplicación (Nginx usa el 80, PostgreSQL el 5432, Redis el 6379, y la API de Aurora Libros usará el 3000).

Variantes útiles:

Sintaxis Efecto
-p 8080:80 Puerto 8080 de todas las interfaces del host → 80 del contenedor
-p 127.0.0.1:8080:80 Solo accesible desde tu propia máquina, no desde la red local. Más seguro
-p 80:80 Puerto 80 del host (en Linux puede requerir privilegios)
-p 8080:80 -p 8443:443 Varios puertos publicados a la vez
-P Publica todos los puertos declarados por la imagen en puertos aleatorios altos

Sin -p, el contenedor funciona perfectamente pero es inalcanzable desde fuera. Es lo correcto para servicios internos: en el módulo 4, aurora-db y aurora-cache no publicarán puertos, porque solo deben ser accesibles desde aurora-api, no desde Internet.

Comprobación con curl y con el navegador

curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</html>

Solo las cabeceras, para ver el estado HTTP:

curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.27.4
Content-Type: text/html
Content-Length: 615

-I pide solo las cabeceras (método HEAD). El 200 OK confirma que el servidor responde.

Abre ahora http://localhost:8080 en tu navegador: verás la página de bienvenida de Nginx. Acabas de servir una web desde un contenedor sin haber instalado Nginx en tu sistema. Si ejecutas nginx -v en tu terminal, lo más probable es que no exista el comando: todo está dentro del contenedor.

  1. El ciclo completo: logs, stop, start y rm

Ver los logs

docker logs aurora-web-demo
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 10:15:03 [notice] 1#1: nginx/1.27.4
2026/08/04 10:15:03 [notice] 1#1: start worker processes
172.17.0.1 - - [04/Aug/2026:10:16:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
172.17.0.1 - - [04/Aug/2026:10:16:45 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0 ..."

Ahí están tus dos visitas: la de curl y la del navegador, identificadas por su user agent. La IP 172.17.0.1 es la de tu máquina vista desde la red interna de Docker.

Opciones muy útiles:

docker logs -f aurora-web-demo        # seguir en vivo (Ctrl+C para salir)
docker logs --tail 20 aurora-web-demo # solo las 20 últimas líneas
docker logs -t aurora-web-demo        # con marca de tiempo de Docker
docker logs --since 5m aurora-web-demo # solo los últimos 5 minutos

Prueba docker logs -f aurora-web-demo, recarga el navegador y verás aparecer la línea en tiempo real. Ctrl+C corta el seguimiento sin afectar al contenedor (una duda muy habitual: no, no lo paras).

Parar el contenedor

docker stop aurora-web-demo
aurora-web-demo

Devuelve el nombre de lo que ha parado. Verifica:

docker ps
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES              STATUS                     PORTS
aurora-web-demo    Exited (0) 5 seconds ago

Dos cosas: docker ps ya no lo muestra, y en docker ps -a la columna PORTS está vacía (el mapeo del puerto 8080 se ha liberado). Confírmalo:

curl http://localhost:8080
curl: (7) Failed to connect to localhost port 8080 after 0 ms: Connection refused

Nadie escucha. El contenedor sigue existiendo, pero no está en ejecución.

Arrancarlo de nuevo

docker start aurora-web-demo
curl -I http://localhost:8080
aurora-web-demo
HTTP/1.1 200 OK

Vuelve a funcionar, con la misma configuración. Y aquí hay un detalle conceptual importante: no tuviste que repetir -p 8080:80. La configuración se fijó al crear el contenedor y forma parte de él. docker start solo lo reanuda.

Corolario: no puedes cambiar el mapeo de puertos de un contenedor existente. Si necesitas otro puerto, hay que borrarlo y crear uno nuevo. Es una manifestación de la filosofía de contenedores desechables.

docker logs aurora-web-demo

Verás los logs del primer arranque y los del segundo, uno detrás de otro: los logs se conservan mientras el contenedor exista.

Borrarlo

docker stop aurora-web-demo
docker rm aurora-web-demo
aurora-web-demo
aurora-web-demo

Si intentas docker rm sin parar primero:

Error response from daemon: cannot remove container "aurora-web-demo": container is running:
stop the container before removing or force remove

Puedes forzarlo con docker rm -f aurora-web-demo, que para y borra en un paso. Es cómodo en desarrollo, pero mata el proceso sin darle tiempo a cerrar ordenadamente.

Verifica:

docker ps -a
docker images

El contenedor ha desaparecido de la lista; la imagen nginx:alpine sigue ahí. Una vez más: borrar un contenedor no borra su imagen.

  1. Un contenedor interactivo: entrar dentro

Hasta ahora ejecutabas comandos sueltos. Ahora vas a abrir un shell dentro de un contenedor y moverte por él.

docker run -it --rm alpine:3.20 sh

Tres opciones nuevas:

Opción Nombre largo Qué hace
-i --interactive Mantiene abierta la entrada estándar, para que puedas escribir
-t --tty Asigna una terminal virtual: prompt, colores, formato
--rm Borra el contenedor automáticamente al salir

-i y -t casi siempre van juntos (-it): con -i sin -t podrías escribir pero sin prompt ni formato; con -t sin -i verías el prompt pero no podrías escribir.

Tu prompt cambia:

/ #

Estás dentro del contenedor. Explora:

hostname
4a7f2b9e1c03

El nombre de host es el ID corto del contenedor, no el de tu máquina.

ls /
bin    dev    etc    home   lib    media  mnt    opt    proc   root
run    sbin   srv    sys    tmp    usr    var

Un sistema de archivos Linux completo... que no es el tuyo. Compruébalo:

cat /etc/os-release
NAME="Alpine Linux"
PRETTY_NAME="Alpine Linux v3.20"

Alpine, aunque tu máquina sea Ubuntu, Windows o macOS.

ls /home
ls /root

Vacíos: tus documentos no están aquí. El aislamiento del sistema de archivos es real.

ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps aux

Esto es revelador: solo hay dos procesos, y el shell tiene el PID 1. En tu máquina hay cientos de procesos y el PID 1 es systemd. El contenedor tiene su propio espacio de nombres de procesos y no ve los del anfitrión (el mecanismo exacto, en la lección 05-07).

Y sin embargo:

uname -r
6.8.0-52-generic

El kernel es el de tu anfitrión. Espacio de usuario de Alpine, kernel de Ubuntu. Esa frase resume qué es un contenedor.

Instalar algo dentro

Alpine usa apk como gestor de paquetes:

curl --version
sh: curl: not found

No está instalado. Instalémoslo:

apk add --no-cache curl
fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz
(1/8) Installing ca-certificates (20240705-r0)
...
(8/8) Installing curl (8.12.1-r0)
Executing busybox-1.36.1-r29.trigger
OK: 13 MiB in 23 packages
  • apk add instala paquetes (el equivalente de apt install).
  • --no-cache evita guardar el índice de paquetes en disco; es la opción estándar en contenedores porque reduce el tamaño.
curl --version
curl 8.12.1 (x86_64-alpine-linux-musl) libcurl/8.12.1 ...

Funciona. Y algo importante: has instalado curl en el contenedor, no en tu máquina. Tu sistema no se ha tocado.

Crea también un fichero para la siguiente comprobación:

echo "Aurora Libros S.L." > /prueba.txt
cat /prueba.txt

Ahora sal:

exit

Vuelves a tu prompt normal.

La magia de --rm

docker ps -a --filter "ancestor=alpine:3.20"

No aparece. Gracias a --rm, el contenedor se eliminó automáticamente al terminar su proceso. Con él se fueron su capa de escritura, el curl que instalaste y el fichero /prueba.txt.

Confírmalo abriendo otro:

docker run -it --rm alpine:3.20 sh
curl --version
cat /prueba.txt
exit
sh: curl: not found
cat: can't open '/prueba.txt': No such file or directory

Es la lección 01-05 en carne y hueso: cada contenedor nace limpio de la imagen inmutable. Los cambios viven en su capa de escritura y mueren con él.

--rm es una opción excelente para pruebas y comandos puntuales: te ahorra la acumulación de contenedores parados. No la uses en servicios que quieras poder reanudar.

  1. Sirviendo la página de Aurora Libros con -v

Nginx sirviendo su página por defecto está bien para probar, pero queremos nuestro contenido. Vamos a crear la primera página de Aurora Libros.

Crear la página

mkdir -p ~/aurora-libros/web
cd ~/aurora-libros/web

Crea index.html con este contenido:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Aurora Libros · Librería online</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 42rem; margin: 3rem auto; padding: 0 1rem; color: #222; }
    h1 { color: #6b3fa0; }
    li { margin-bottom: .6rem; }
    footer { margin-top: 2rem; font-size: .85rem; color: #666; }
  </style>
</head>
<body>
  <h1>Aurora Libros</h1>
  <p>Tu librería online. Catálogo destacado:</p>
  <ul>
    <li><strong>El jardín de senderos que se bifurcan</strong> — Jorge Luis Borges — 14,50 €</li>
    <li><strong>Rayuela</strong> — Julio Cortázar — 19,90 €</li>
    <li><strong>Cien años de soledad</strong> — Gabriel García Márquez — 17,95 €</li>
  </ul>
  <footer>Aurora Libros S.L. · Servido desde un contenedor Docker</footer>
</body>
</html>

Montarla en el contenedor

docker run -d --name aurora-web-demo -p 8080:80 \
  -v ~/aurora-libros/web/index.html:/usr/share/nginx/html/index.html:ro \
  nginx:alpine

La opción nueva es -v (--volume), y su sintaxis se lee igual que -p, de fuera hacia dentro:

-v RUTA_EN_TU_MAQUINA:RUTA_DENTRO_DEL_CONTENEDOR[:opciones]
Parte Significado
~/aurora-libros/web/index.html El fichero de tu máquina. Debe ser una ruta absoluta (~ la expande el shell)
/usr/share/nginx/html/index.html Dónde aparecerá dentro del contenedor. Esta ruta es donde la imagen de Nginx busca sus ficheros
:ro read-only: el contenedor puede leerlo pero no modificarlo. Buena práctica cuando no debe escribir

Compruébalo:

curl http://localhost:8080
<!DOCTYPE html>
<html lang="es">
...
  <h1>Aurora Libros</h1>
...

Y en el navegador, en http://localhost:8080, verás la página de Aurora Libros con su catálogo. El fichero sigue estando en tu disco; el contenedor simplemente lo ve montado en su sistema de archivos.

La ventaja: edición en vivo

Edita index.html en tu editor y añade un libro:

    <li><strong>La sombra del viento</strong> — Carlos Ruiz Zafón — 21,00 €</li>

Guarda y recarga el navegador. El cambio aparece al instante, sin reconstruir nada ni reiniciar el contenedor. El montaje es una ventana en vivo entre tu disco y el contenedor.

Este patrón —montar el código fuente dentro del contenedor para desarrollar sin reconstruir— es la base del flujo de desarrollo local que verás en la lección 04-07.

Nota importante. Este uso de -v es puramente introductorio: es un bind mount, que monta una ruta de tu máquina. Existe otro tipo, los volúmenes gestionados por Docker, que es lo que necesitará aurora-db para no perder el catálogo. Las diferencias, cuándo usar cada uno y las implicaciones de permisos y rendimiento son el contenido completo de la lección 03-06.

  1. Limpieza final

Dejemos el sistema como estaba:

docker stop aurora-web-demo
docker rm aurora-web-demo
docker container prune -f
  • Los dos primeros paran y borran el contenedor de Nginx.
  • container prune -f elimina cualquier otro contenedor parado que hayas dejado por el camino.

Comprueba:

docker ps -a
docker images
docker system df

Sin contenedores; las imágenes (alpine:3.20, nginx:alpine, hello-world) intactas, listas para reutilizarse sin descargarlas de nuevo. Tu fichero ~/aurora-libros/web/index.html también sigue en su sitio: lo necesitarás en las próximas lecciones.

Errores Comunes y Consejos

  • Bind for 0.0.0.0:8080 failed: port is already allocated. El puerto del host ya está ocupado, casi siempre por otro contenedor tuyo. Búscalo con docker ps y páralo, o publica en otro puerto (-p 8081:80).
  • Invertir el orden de -p. -p 80:8080 con Nginx no funciona: estarías reenviando el puerto 80 del host al 8080 del contenedor, donde no escucha nadie. Recuerda: host:contenedor, de fuera hacia dentro.
  • docker run -d con un comando que termina. docker run -d alpine echo hola arranca y muere al instante. -d no mantiene vivo nada: solo desconecta tu terminal. Para seguir vivo hace falta un proceso que no termine.
  • Rutas relativas en -v. -v ./web:/usr/share/nginx/html puede fallar según la versión y el shell. Usa siempre rutas absolutas: $(pwd)/web o ~/....
  • Montar un directorio sobre uno que ya tiene contenido. El montaje oculta lo que hubiera en esa ruta dentro de la imagen. Si montas un directorio vacío sobre /usr/share/nginx/html, Nginx devolverá 403 o 404 porque ya no ve su index.html.
  • Confundir Ctrl+C con parar el contenedor. En docker logs -f o docker attach, Ctrl+C solo corta tu conexión. En un contenedor en primer plano, sí interrumpe el proceso y lo termina.
  • Consejo: usa --rm para todo lo desechable. Pruebas, comandos puntuales, shells exploratorios. Te ahorrará limpiezas.
  • Consejo: nombra siempre con --name. aurora-web-demo es infinitamente más manejable que stupefied_swanson, y hace que los filtros por nombre funcionen.
  • Consejo: si un contenedor muere nada más arrancar, docker logs primero. Los logs se conservan aunque esté parado, y ahí está casi siempre la explicación.

Ejercicios

Ejercicio 1: dos escaparates a la vez

Levanta dos contenedores de Nginx simultáneamente a partir de la misma imagen: uno llamado aurora-web-es en el puerto 8080 y otro aurora-web-en en el 8081, cada uno sirviendo un index.html distinto (uno en español y otro en inglés). Comprueba con curl que cada puerto devuelve su página. Después responde: ¿cuántas copias de la imagen nginx:alpine hay en tu disco?, ¿cuánto ocupa la capa de escritura de cada contenedor (docker ps -as)? Al terminar, bórralos ambos en un único comando.

Ejercicio 2: el detective de logs

Ejecuta este contenedor, que está mal configurado a propósito:

docker run -d --name aurora-fallo -p 8080:80 \
  -v /tmp/carpeta-que-no-uso:/usr/share/nginx/html:ro \
  nginx:alpine

(Crea antes /tmp/carpeta-que-no-uso vacía con mkdir -p /tmp/carpeta-que-no-uso.)

Visita http://localhost:8080 y observa el error. Después: (a) ¿está el contenedor en ejecución?, (b) ¿qué código HTTP devuelve y por qué?, (c) usa docker logs para encontrar el mensaje de error exacto de Nginx, y (d) explica en términos de montajes qué ha pasado y cómo lo arreglarías.

Ejercicio 3: exploración interactiva comparada

Abre un contenedor interactivo de alpine:3.20 y otro de nginx:alpine (en dos terminales, ambos con --rm). En cada uno, averigua: (a) el sistema operativo del espacio de usuario, (b) la versión del kernel, (c) el PID 1 y cuántos procesos hay, y (d) si existe el directorio /usr/share/nginx/html y qué contiene. Explica las diferencias y las coincidencias entre los dos contenedores, y qué te dice cada una sobre la naturaleza de los contenedores.

Soluciones

Solución al ejercicio 1

mkdir -p ~/aurora-libros/web-es ~/aurora-libros/web-en
echo '<h1>Aurora Libros</h1><p>Bienvenido a nuestra librería.</p>' > ~/aurora-libros/web-es/index.html
echo '<h1>Aurora Books</h1><p>Welcome to our bookstore.</p>' > ~/aurora-libros/web-en/index.html

docker run -d --name aurora-web-es -p 8080:80 \
  -v ~/aurora-libros/web-es:/usr/share/nginx/html:ro nginx:alpine

docker run -d --name aurora-web-en -p 8081:80 \
  -v ~/aurora-libros/web-en:/usr/share/nginx/html:ro nginx:alpine

curl -s http://localhost:8080
curl -s http://localhost:8081
<h1>Aurora Libros</h1><p>Bienvenido a nuestra librería.</p>
<h1>Aurora Books</h1><p>Welcome to our bookstore.</p>

Aquí se montan directorios en lugar de ficheros sueltos, que es lo habitual con Nginx. Los dos contenedores usan puertos distintos del host (8080 y 8081) pero ambos el 80 dentro: no hay conflicto, porque cada contenedor tiene su propia pila de red y su propio puerto 80.

docker images nginx
docker ps -as --format "table {{.Names}}\t{{.Size}}"
REPOSITORY   TAG      IMAGE ID       SIZE
nginx        alpine   3f8a4339aadd   52.5MB
NAMES            SIZE
aurora-web-en    1.09kB (virtual 52.5MB)
aurora-web-es    1.09kB (virtual 52.5MB)

Respuestas: hay una sola copia de la imagen en disco, compartida por los dos contenedores. Cada capa de escritura ocupa alrededor de 1 kB. El "virtual 52.5MB" incluye las capas compartidas de la imagen, que no se cuentan dos veces. Levantar el segundo contenedor costó prácticamente cero disco: es la densidad de la lección 01-01, medida.

docker rm -f aurora-web-es aurora-web-en

rm -f para y borra en un paso, y acepta varios nombres.

Solución al ejercicio 2

(a) Sí, el contenedor está en ejecución:

docker ps --filter "name=aurora-fallo"
CONTAINER ID   IMAGE          STATUS         PORTS                  NAMES
b2c8f4a1e7d9   nginx:alpine   Up 20 seconds  0.0.0.0:8080->80/tcp   aurora-fallo

Nginx arrancó correctamente. El problema no es el arranque, sino el contenido que sirve.

(b) Devuelve 403 Forbidden:

curl -I http://localhost:8080
HTTP/1.1 403 Forbidden
Server: nginx/1.27.4

Es 403 y no 404 porque el directorio raíz existe (lo montaste) pero está vacío: no hay index.html que servir y Nginx no permite listar directorios por defecto.

(c) Los logs:

docker logs aurora-fallo
2026/08/04 11:02:41 [error] 30#30: *1 directory index of "/usr/share/nginx/html/" is forbidden,
client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"

El mensaje es explícito: directory index ... is forbidden.

(d) Qué ha pasado: el montaje oculta el contenido original de esa ruta en la imagen. La imagen nginx:alpine trae su propio index.html en /usr/share/nginx/html, pero al montar encima un directorio vacío de tu máquina, ese contenido deja de ser visible (no se borra: sigue en la capa de la imagen, simplemente queda tapado). Nginx encuentra un directorio vacío y responde 403.

Arreglos posibles: poner un index.html en /tmp/carpeta-que-no-uso, montar un directorio que sí tenga contenido, o no montar nada si querías la página por defecto.

echo '<h1>Aurora Libros</h1>' > /tmp/carpeta-que-no-uso/index.html
curl -s http://localhost:8080
docker rm -f aurora-fallo

Nota que no hace falta reiniciar el contenedor: el montaje es en vivo, así que en cuanto el fichero existe en tu disco, Nginx lo sirve.

Solución al ejercicio 3

# Terminal 1
docker run -it --rm alpine:3.20 sh

# Terminal 2
docker run -it --rm nginx:alpine sh

(La imagen de Nginx también trae un shell, así que se puede entrar en ella igual.)

En cada uno:

cat /etc/os-release | head -2
uname -r
ps aux
ls /usr/share/nginx/html

Resultados y su lectura:

Comprobación alpine:3.20 nginx:alpine Qué significa
(a) SO del espacio de usuario Alpine Linux v3.20 Alpine Linux (versión de la imagen base de Nginx) Ambas se construyen sobre Alpine: comparten capas base, como viste en 01-05
(b) Kernel El de tu anfitrión (p. ej. 6.8.0-52-generic) El mismo Confirma que ambos contenedores comparten el kernel del host. No hay SO invitado
(c) PID 1 y nº de procesos sh como PID 1, 2 procesos sh como PID 1, 2 procesos Cada contenedor tiene su propio espacio de nombres de procesos, aislado. Y ojo: aquí Nginx no está corriendo, porque al pasar sh sustituiste el comando por defecto de la imagen
(d) /usr/share/nginx/html No existe (ls: ... No such file or directory) Existe, con index.html y 50x.html El sistema de archivos lo aporta la imagen: son distintos porque las imágenes traen contenido distinto

Las conclusiones importantes: los contenedores comparten el kernel (b) pero tienen sistemas de archivos y espacios de procesos aislados (c, d); dos imágenes pueden compartir su base y diferenciarse solo en las capas superiores (a); y pasar un comando a docker run sustituye el comando por defecto de la imagen, por lo que entrar con sh en la imagen de Nginx te da el sistema de archivos de Nginx sin Nginx ejecutándose.

Conclusión

Ya has creado, gestionado y destruido contenedores con tus propias manos. Te llevas la regla que gobierna todo lo demás: un contenedor vive lo que vive su proceso principal; si ese proceso termina, el contenedor pasa a Exited, y por eso docker run alpine "no hace nada" mientras docker run nginx:alpine se queda vivo indefinidamente.

Sabes lanzar contenedores en primer plano y en segundo plano con -d, nombrarlos con --name, y sobre todo publicar puertos con -p host:contenedor, leyendo la sintaxis siempre de fuera hacia dentro; has comprobado que el mapeo se fija al crear el contenedor y que docker start lo reanuda con la misma configuración. Has recorrido el ciclo completo —logs, stop, start, rm—, has entrado dentro de un contenedor con -it para ver con tus ojos el aislamiento del sistema de archivos y de los procesos junto al kernel compartido, y has confirmado con --rm que todo lo que escribes dentro muere con el contenedor. Y has servido la primera página de Aurora Libros montando un fichero de tu disco con -v, con edición en vivo incluida.

Tienes las tres piezas del rompecabezas —imágenes, contenedores y comandos— y una página web funcionando. Lo que aún no tienes es el problema real que justifica todo esto. En la última lección del módulo, El Proyecto del Curso: la Plataforma Aurora Libros, conocerás la empresa, su arquitectura objetivo y el código completo de su API, e intentarás arrancarla sin Docker para vivir en primera persona el calvario que el resto del curso va a resolver.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados