Tu imagen auroralibros/aurora-api:1.1.0 está publicada y es impecable: usuario sin privilegios, metadatos OCI, healthcheck y parada limpia en 0,3 segundos. Y sigue devolviendo ECONNREFUSED en /libros, exactamente como te anunció la lección anterior. Este módulo entero existe para arreglar eso, y empieza por donde hay que empezar: entender de verdad el comando que llevas usando desde la lección 01-06 sin haberlo mirado nunca de cerca. docker run no es un comando, es dos: crea un contenedor y lo arranca. Y admite más de un centenar de opciones, de las que en el trabajo diario se usan unas quince. En esta lección vas a ver qué hace run por dentro, vas a aprender esas quince opciones agrupadas por familias, vas a sobrescribir el CMD y el ENTRYPOINT de una imagen desde la línea de comandos, y vas a levantar por fin aurora-db y aurora-cache como contenedores reales. Al terminar tendrás tres contenedores vivos... y la API seguirá sin funcionar. Ese fallo, con su mensaje de error cambiado, es la pista con la que arranca el resto del módulo.

Contenido

  1. Qué hace docker run realmente: create + start
  2. Anatomía del comando y el orden de los argumentos
  3. Familia identidad: --name y --hostname
  4. Familia ejecución: -d, -it, --rm, -w, -u, --entrypoint
  5. Familia red y puertos: lo mínimo para trabajar
  6. Familia configuración: -e y --env-file
  7. Familia datos: -v en su versión mínima
  8. Sobrescribir el CMD y el ENTRYPOINT desde la línea de comandos
  9. Primer plano, segundo plano y cómo desacoplarse
  10. docker attach frente a docker exec
  11. Práctica: la base de datos y la caché de Aurora Libros

  1. Qué hace docker run realmente: create + start

docker run es un atajo. Por debajo ejecuta dos operaciones distintas que puedes invocar por separado:

docker create --name prueba-ciclo -p 8080:80 nginx:alpine
7f3a9c1e8b2d4a6f05c7e91b3d8a4b2c6e0b7d9a1f3c5e7b9d1a3f5c7e9b1d3a

Docker imprime el ID completo del contenedor (64 caracteres hexadecimales) y devuelve el control. Fíjate en lo que ha ocurrido y en lo que no:

docker ps -a --filter name=prueba-ciclo --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES           STATUS    PORTS
prueba-ciclo    Created

El contenedor existe, tiene su capa de escritura reservada y toda su configuración guardada (puertos, variables, comando), pero el estado es Created y la columna PORTS está vacía: no hay ningún proceso en marcha y el puerto 8080 no está escuchando en tu máquina. Compruébalo:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080
000

Ahora arráncalo:

docker start prueba-ciclo
prueba-ciclo

docker start imprime el nombre del contenedor arrancado. Y ahora sí:

docker ps --filter name=prueba-ciclo --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080
NAMES           STATUS         PORTS
prueba-ciclo    Up 3 seconds   0.0.0.0:8080->80/tcp
200

Esta separación no es un detalle académico: explica el reparto de responsabilidades que vas a usar durante todo el módulo.

Fase Qué hace Qué se puede cambiar después
create Reserva la capa de escritura, guarda la configuración (nombre, puertos, variables, montajes, comando, límites) Casi nada: puertos, variables y montajes quedan fijados para siempre
start Crea los namespaces, aplica los cgroups y lanza el proceso PID 1 Se puede repetir tantas veces como quieras (stop/start)

La consecuencia práctica, que conviene grabarse ya: no puedes añadir un puerto ni una variable de entorno a un contenedor que ya existe. Si te has equivocado, se borra y se crea de nuevo. Es exactamente por eso que en el módulo 4 acabarás escribiendo esa configuración en un fichero en vez de en la línea de comandos.

Limpia la demostración:

docker rm -f prueba-ciclo

El flujo completo, ahora que conoces las piezas de la lección 01-03:

flowchart TD
    A["docker run -d -p 8080:80 nginx:alpine"] --> B{"¿Está la imagen<br/>en local?"}
    B -- No --> C["docker pull implícito<br/>desde el registro"]
    B -- Sí --> D["FASE CREATE"]
    C --> D
    D --> D1["Capa de escritura (copy-on-write)"]
    D --> D2["Configuración: nombre, puertos,<br/>variables, comando"]
    D1 --> E["FASE START"]
    D2 --> E
    E --> E1["Namespaces: pid, net, mnt, uts, ipc"]
    E --> E2["Cgroups: memoria y CPU"]
    E --> E3["Reglas de red y publicación de puertos"]
    E1 --> F["runc lanza el proceso PID 1"]
    E2 --> F
    E3 --> F
    F --> G["Contenedor en estado running"]

  1. Anatomía del comando y el orden de los argumentos

docker run [OPCIONES] IMAGEN [COMANDO] [ARGUMENTOS...]

Hay una sola regla de oro y provoca la mitad de los errores de los principiantes:

Todo lo que va antes de la imagen son opciones de Docker. Todo lo que va después de la imagen es el comando que se ejecuta dentro del contenedor.

Míralo fallar:

docker run alpine:3.20 -e MENSAJE=hola echo prueba
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "-e": executable file not found in $PATH

Docker no ha interpretado -e como una opción suya: como venía después de alpine:3.20, ha intentado ejecutar dentro del contenedor un programa llamado -e. La versión correcta:

docker run --rm -e MENSAJE=hola alpine:3.20 sh -c 'echo $MENSAJE'
hola

Aquí --rm y -e MENSAJE=hola son opciones de Docker (van antes de la imagen) y sh -c 'echo $MENSAJE' es el comando dentro del contenedor (va después). Necesitamos sh -c porque la expansión de $MENSAJE la hace un shell, y sin él Docker pasaría la cadena literal.

Un vistazo general a las familias que vas a ver en las secciones siguientes:

Familia Opciones Para qué
Identidad --name, --hostname, --label Poder referirte al contenedor y organizarlo
Ejecución -d, -it, --rm, -w, -u, --entrypoint Cómo y con qué identidad corre el proceso
Red y puertos -p, -P, --expose, --network Quién puede hablar con él (a fondo en 03-05)
Configuración -e, --env-file Qué valores lee la aplicación
Datos -v, --mount, --tmpfs Qué sobrevive al contenedor (a fondo en 03-06)
Recursos y resiliencia -m, --cpus, --restart Cuánto puede consumir y qué pasa si falla (a fondo en 03-07)

  1. Familia identidad: --name y --hostname

--name

Sin --name, Docker inventa un nombre de dos palabras (nostalgic_hopper, elegant_bardeen) y te obliga a copiar IDs. Con nombre, todos los comandos siguientes se vuelven legibles y guionizables:

docker run -d --name aurora-cache redis:7-alpine
docker logs aurora-cache
docker stop aurora-cache

Reglas de los nombres:

Regla Detalle
Caracteres válidos [a-zA-Z0-9][a-zA-Z0-9_.-]*
Único en toda la máquina Incluidos los contenedores parados
Sirve como nombre DNS En una red propia, otro contenedor lo resuelve por ese nombre (lección 03-05)

Ese último punto es la clave de todo el módulo, y por eso los nombres de Aurora Libros (aurora-db, aurora-cache, aurora-api, aurora-web) no son decorativos: dentro de poco serán nombres de host reales.

El error más habitual con --name:

docker run -d --name aurora-cache redis:7-alpine
docker: Error response from daemon: Conflict. The container name "/aurora-cache" is already in use by container "a1b2c3d4e5f6".
You have to remove (or rename) that container to be able to reuse that name.

Ojo: el conflicto ocurre aunque el contenedor anterior esté parado. Un contenedor Exited sigue ocupando su nombre. Lo resuelves con docker rm aurora-cache (o docker rm -f si está corriendo).

--hostname

Cambia el nombre que el contenedor ve de sí mismo dentro de su propio namespace UTS:

docker run --rm --name prueba-host --hostname aurora-nodo-1 alpine:3.20 hostname
aurora-nodo-1

Sin --hostname, el hostname es el ID corto del contenedor:

docker run --rm --name prueba-host2 alpine:3.20 hostname
9c4e1a7b2f83

--name y --hostname son cosas distintas: --name es cómo lo llamas tú desde fuera (y cómo lo llaman otros contenedores por DNS); --hostname es cómo se llama el contenedor a sí mismo. Se usa poco, pero aparece en logs de aplicaciones y en clústeres de bases de datos.

  1. Familia ejecución: -d, -it, --rm, -w, -u, --entrypoint

Opción Qué hace Cuándo la usas
-d, --detach Arranca en segundo plano e imprime el ID Servicios: bases de datos, APIs, servidores web
-i, --interactive Mantiene la entrada estándar abierta Cuando vas a escribir algo al proceso
-t, --tty Asigna un pseudo-terminal (prompt, colores, Ctrl+C) Shells interactivas
-it La combinación de las dos anteriores Entrar en un contenedor con sh o bash
--rm Borra el contenedor al terminar Comandos de un solo uso y pruebas
-w, --workdir Directorio de trabajo, sobrescribe el WORKDIR de la imagen Ejecutar algo en otra ruta sin reconstruir
-u, --user Usuario/UID con el que corre el proceso Ajustar permisos o entrar como root a una imagen sin privilegios
--entrypoint Sustituye el ENTRYPOINT de la imagen Depurar una imagen cuyo ejecutable fijo estorba

-d frente a primer plano

docker run -d --name web-fondo -p 8080:80 nginx:alpine
c3f8a1b7e2d94c6501fa7b3e8d2c9a4b6e0f1d3a5c7e9b1d3f5a7c9e1b3d5f7a

Devuelve el ID y te deja la terminal libre. Sin -d, la terminal queda ocupada mostrando la salida del proceso y Ctrl+C lo detiene.

-it y por qué se necesitan las dos letras

docker run --rm -it alpine:3.20 sh
/ # whoami
root
/ # exit

Prueba a quitar una de las dos letras para entender qué aporta cada una:

Comando Resultado
docker run --rm alpine:3.20 sh El shell arranca, no encuentra entrada y termina de inmediato. Vuelves al prompt del host
docker run --rm -i alpine:3.20 sh Funciona: puedes escribir comandos, pero sin prompt, sin colores y sin historial
docker run --rm -t alpine:3.20 sh Ves el prompt / #, pero lo que escribes no llega: el contenedor no tiene stdin. Queda colgado
docker run --rm -it alpine:3.20 sh Sesión interactiva completa

Regla práctica: -it para personas, -d para servicios, nada para comandos de un solo disparo. Y nunca -it en un script automatizado o en CI: sin terminal real, -t provoca the input device is not a TTY.

--rm

docker run --rm alpine:3.20 date -u
Tue Aug  4 19:32:11 UTC 2026

El contenedor se ejecuta, imprime y desaparece: no queda en docker ps -a ni ocupa capa de escritura. Es la opción que evita acumular decenas de contenedores muertos.

Dos avisos importantes:

  • --rm es incompatible con la depuración: si el contenedor falla, se borra junto con sus logs y su inspect. Cuando algo va mal, quita el --rm para poder investigar (lección 03-04).
  • --rm borra también los volúmenes anónimos asociados. Con volúmenes con nombre no pasa nada, pero es un detalle que se retoma en la lección 03-06.

-w y -u

docker run --rm -w /tmp alpine:3.20 pwd
docker run --rm -u 1000:1000 alpine:3.20 id
docker run --rm -u root auroralibros/aurora-api:1.1.0 id
/tmp
uid=1000 gid=1000 groups=1000
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
  • -w /tmp sobrescribe el WORKDIR /app que fijaste en el Dockerfile.
  • -u 1000:1000 fuerza un UID:GID concreto aunque ese usuario no exista dentro de la imagen (por eso id muestra los números sin nombre).
  • -u root anula el USER node de tu imagen. Es útilísimo para depurar (instalar una herramienta dentro de un contenedor sin privilegios), y a la vez es un recordatorio de que USER es una defensa en profundidad, no una barrera infranqueable: quien puede lanzar contenedores puede elegir el usuario.

--entrypoint

Ya lo usaste en la lección 02-04 y aquí formaliza su sitio:

docker run --rm -it --entrypoint sh auroralibros/aurora-api:1.1.0
/app $ ls
Dockerfile  node_modules  package-lock.json  package.json  server.js
/app $ node --version
v22.13.0

Sin --entrypoint, tu imagen tiene ENTRYPOINT ["node"] fijo y cualquier cosa que escribieras después de la imagen sería un argumento de node. Con --entrypoint sh entras a mirar. Es la puerta de servicio de toda imagen bien construida.

  1. Familia red y puertos: lo mínimo para trabajar

Aquí solo lo imprescindible; el porqué completo llega en la lección 03-05.

docker run -d --name aurora-web-demo -p 8080:80 nginx:alpine

-p HOST:CONTENEDOR significa "todo lo que llegue al puerto 8080 de mi máquina, redirígelo al puerto 80 de este contenedor". El orden nunca se invierte: primero el host, después el contenedor.

Sintaxis Significado
-p 8080:80 Puerto 8080 de todas las interfaces del host → puerto 80 del contenedor
-p 127.0.0.1:8080:80 Solo desde tu propia máquina; no visible en la red local
-p 80 Puerto 80 del contenedor a un puerto aleatorio libre del host
-p 8080:80/udp Publica UDP en vez de TCP
-P Publica todos los puertos declarados con EXPOSE, cada uno en un puerto aleatorio
--expose 9000 Declara el puerto en los metadatos, sin publicarlo en el host

Comprueba -P con tu propia imagen, que declara EXPOSE 3000:

docker run -d --name api-aleatoria -P auroralibros/aurora-api:1.1.0
docker port api-aleatoria
3000/tcp -> 0.0.0.0:32768

Docker ha elegido el 32768 del rango efímero. docker port es el comando que responde a "¿en qué puerto del host quedó esto?".

Dos ideas que conviene fijar desde ya, aunque se desarrollen en 03-05:

  • EXPOSE en el Dockerfile y --expose en run no abren nada: son documentación que consumen -P y otras herramientas.
  • Publicar un puerto sirve para que tú, desde el host, alcances el contenedor. Para que dos contenedores se hablen entre sí no hace falta publicar nada. Esta distinción es la que está a punto de resolver el misterio de la API.

Limpia:

docker rm -f aurora-web-demo api-aleatoria

  1. Familia configuración: -e y --env-file

Tu server.js no tiene ni un valor fijo escrito: lee PORT, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME y REDIS_HOST del entorno. Esa decisión de la lección 01-07 es la que permite que la misma imagen sirva para tu portátil, para pruebas y para producción.

-e en sus tres formas

docker run --rm -e SALUDO="Hola Aurora" alpine:3.20 printenv SALUDO
export TOKEN_LOCAL=abc123
docker run --rm -e TOKEN_LOCAL alpine:3.20 printenv TOKEN_LOCAL
docker run --rm auroralibros/aurora-api:1.1.0 --version
Hola Aurora
abc123
v22.13.0
Forma Efecto
-e CLAVE=valor Define la variable con ese valor literal
-e CLAVE Copia el valor que tenga esa variable en el shell del host
Sin -e La aplicación usa el ENV de la imagen o su valor por defecto en el código

La segunda forma es muy práctica para no escribir secretos en la línea de comandos, donde quedarían en el historial del shell.

Las variables definidas con -e sobrescriben las del ENV del Dockerfile. Compruébalo con tu imagen, que trae ENV PORT=3000:

docker run --rm -e PORT=4000 --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT
4000

Y vuelve a ver toda la escalera de prioridad:

Prioridad Origen Ejemplo
1 (gana) -e / --env-file en docker run -e PORT=4000
2 ENV del Dockerfile ENV PORT=3000
3 Valor por defecto en el código process.env.PORT || 3000

--env-file

Cuando pasas de tres variables, la línea de comandos se vuelve ilegible. Crea ~/aurora-libros/aurora.env:

# Configuración local de Aurora Libros — NO se hornea en la imagen
PORT=3000
DB_HOST=aurora-db
DB_PORT=5432
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_libros
REDIS_HOST=aurora-cache
REDIS_PORT=6379

Y úsalo:

docker run --rm --env-file ~/aurora-libros/aurora.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 DB_HOST DB_NAME
aurora-db
aurora_libros

Las reglas del formato son estrictas y distintas de las de un script de shell. Este es un punto donde se pierde mucho tiempo:

Regla Correcto Incorrecto y por qué
Una variable por línea, CLAVE=valor DB_USER=aurora export DB_USER=aurora → la clave sería export DB_USER
Sin comillas salvo que formen parte del valor DB_PASSWORD=aurora_secreta DB_PASSWORD="aurora_secreta" → la contraseña incluiría las comillas
Sin expansión de variables RUTA=/app/datos RUTA=$HOME/datos → el valor literal sería $HOME/datos
Comentarios con # a principio de línea # comentario DB_PORT=5432 # el puerto → el valor sería 5432 # el puerto
Sin espacios alrededor del = PORT=3000 PORT = 3000 → la clave sería PORT

Y una regla de seguridad que ya conoces del módulo 2, ahora con su consecuencia concreta:

echo "aurora.env" >> ~/aurora-libros/.gitignore
echo "aurora.env" >> ~/aurora-libros/api/.dockerignore

El fichero de entorno contiene una contraseña: no va al repositorio de Git ni al contexto de construcción. La imagen se queda sin secretos —como debe ser— y la configuración viaja por fuera, inyectada en el arranque. Puedes combinar ambas opciones: --env-file para el grueso y -e para lo que quieras sobrescribir puntualmente, porque -e gana sobre --env-file independientemente del orden en que los escribas.

  1. Familia datos: -v en su versión mínima

Lo usaste en la lección 01-06 para servir tu index.html con Nginx:

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 sintaxis mínima es -v ORIGEN:DESTINO[:opciones], con la ruta del host siempre absoluta y :ro para montar en solo lectura. Con eso te basta hasta la lección 03-06, donde verás los tres tipos de montaje, los volúmenes gestionados y por qué el catálogo de libros que estás a punto de crear desaparecerá si no haces nada al respecto. De momento, quédate con la idea de la lección 01-05: todo lo que un contenedor escribe fuera de un montaje vive en su capa de escritura y muere con él.

  1. Sobrescribir el CMD y el ENTRYPOINT desde la línea de comandos

Retomamos la tabla de la lección 02-04, ahora desde el lado de la ejecución. Tu imagen declara:

ENTRYPOINT ["node"]
CMD ["server.js"]

El comando efectivo es la concatenación ENTRYPOINT + CMDnode server.js. Y desde docker run puedes tocar cada mitad por separado:

Comando ENTRYPOINT CMD Se ejecuta Resultado
docker run IMG node server.js node server.js Arranca la API
docker run IMG --version node --version node --version Imprime v22.13.0
docker run IMG -e "console.log(2+2)" node -e console.log(2+2) node -e "console.log(2+2)" Imprime 4
docker run --entrypoint sh IMG sh (anulado) sh Shell interactiva
docker run --entrypoint sh IMG -c "ls /app" sh -c ls /app sh -c "ls /app" Lista el directorio
docker run --entrypoint "" IMG ls -la /app (ninguno) ls -la /app ls -la /app Ejecuta ls directamente

Compruébalo:

docker run --rm auroralibros/aurora-api:1.1.0 -e "console.log('Aurora ' + (2+2))"
docker run --rm --entrypoint sh auroralibros/aurora-api:1.1.0 -c "ls /app | head -3"
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -la /app/server.js
Aurora 4
Dockerfile
node_modules
package-lock.json
-rw-r--r--    1 node     node          4187 Aug  4 19:12 /app/server.js

Dos matices que causan sorpresas:

  • Sobrescribir el ENTRYPOINT descarta el CMD de la imagen. En la fila 4 de la tabla, el CMD ["server.js"] desaparece: si no quieres que sh reciba server.js como argumento no tienes que hacer nada, ya no está.
  • --entrypoint "" (cadena vacía) es la forma de dejar la imagen sin ejecutable fijo, de modo que lo que escribas tras la imagen sea el comando completo. Es el truco que salva el día con imágenes cuyo entrypoint es un script complicado.

  1. Primer plano, segundo plano y cómo desacoplarse

Cuando arrancas sin -d, tu terminal queda enganchada a la entrada y la salida del PID 1:

docker run --name web-primer-plano -p 8080:80 nginx:alpine
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 19:41:07 [notice] 1#1: start worker processes

Ahí se queda. Si pulsas Ctrl+C, envías SIGINT al proceso y el contenedor se para. Eso rara vez es lo que quieres con un servicio.

La alternativa, si arrancaste con -it, es desacoplarte sin matarlo con la secuencia Ctrl+P seguida de Ctrl+Q:

docker run -it --name alpine-desacople alpine:3.20 sh
/ # sleep 300
<pulsas Ctrl+P y luego Ctrl+Q>
read escape sequence
docker ps --filter name=alpine-desacople --format "{{.Names}}: {{.Status}}"
alpine-desacople: Up 22 seconds

Sigue vivo. Tres condiciones para que la secuencia funcione, y su ausencia explica el 90 % de los "a mí no me va":

  1. El contenedor debe haberse arrancado con -t (necesita el pseudo-terminal).
  2. Debe estar enganchado a stdin, es decir, con -i.
  3. La secuencia se pulsa dentro de la sesión, no en otra terminal.

Si Ctrl+P te hace falta para otra cosa (en bash recupera el comando anterior), cámbiala:

docker run -it --detach-keys="ctrl-e,e" --name alpine-teclas alpine:3.20 sh

Ahora la combinación es Ctrl+E seguida de e. Puedes hacerlo permanente en ~/.docker/config.json:

{
  "detachKeys": "ctrl-e,e"
}

Resumen de las tres formas de arrancar:

Forma Comando Terminal Ctrl+C
Primer plano docker run IMG Ocupada mostrando la salida Para el contenedor
Primer plano interactivo docker run -it IMG sh Ocupada, con sesión Va al proceso de dentro
Segundo plano docker run -d IMG Libre de inmediato No aplica

Limpia:

docker rm -f web-primer-plano alpine-desacople alpine-teclas

  1. docker attach frente a docker exec

Un contenedor en segundo plano se puede "volver a mirar" de dos formas radicalmente distintas:

docker run -d --name aurora-cache-demo redis:7-alpine
docker attach aurora-cache-demo

docker attach te conecta a la entrada y salida del proceso PID 1 que ya existe. No lanza nada nuevo. Y ahí está su peligro:

1:M 04 Aug 2026 19:45:03.221 * Ready to accept connections tcp
<pulsas Ctrl+C>
docker ps -a --filter name=aurora-cache-demo --format "{{.Names}}: {{.Status}}"
aurora-cache-demo: Exited (0) 4 seconds ago

Has parado Redis. Ctrl+C durante un attach va directo al PID 1. Para salir sin matarlo hay que usar Ctrl+P Ctrl+Q, o directamente conectarse en modo seguro:

docker attach --sig-proxy=false aurora-cache-demo

Con --sig-proxy=false, Ctrl+C te devuelve el prompt sin enviar la señal al contenedor.

docker exec, en cambio, lanza un proceso nuevo dentro del contenedor:

docker start aurora-cache-demo
docker exec -it aurora-cache-demo redis-cli PING
PONG

Ese redis-cli no tiene nada que ver con el PID 1: es un segundo proceso que comparte los namespaces del contenedor y que puede terminar sin afectar al servicio.

docker attach docker exec
Qué hace Se conecta al PID 1 existente Crea un proceso nuevo
¿Se puede usar varias veces a la vez? Sí, pero todos ven lo mismo Sí, independientes
Ctrl+C Mata el servicio (salvo --sig-proxy=false) Termina solo tu proceso
Salir sin daño Ctrl+P Ctrl+Q exit
Uso típico Ver la salida en directo de un proceso interactivo Abrir una shell, ejecutar diagnósticos
Recomendación Evitarlo salvo casos concretos La opción por defecto

La regla práctica: para mirar la salida usa docker logs y para entrar usa docker exec. Ambos se estudian a fondo en la lección 03-04. docker attach se reserva para procesos que de verdad esperan tu entrada por teclado.

docker rm -f aurora-cache-demo

  1. Práctica: la base de datos y la caché de Aurora Libros

Es el momento. Hasta ahora, la API arrancaba sola y fallaba porque no había ni PostgreSQL ni Redis. Vamos a ponerlos.

La base de datos

docker run -d \
  --name aurora-db \
  -e POSTGRES_USER=aurora \
  -e POSTGRES_PASSWORD=aurora_secreta \
  -e POSTGRES_DB=aurora_libros \
  -p 127.0.0.1:5432:5432 \
  postgres:16-alpine

Repasemos cada línea, porque cada una tiene su motivo:

Fragmento Por qué
-d Es un servicio: no queremos la terminal ocupada
--name aurora-db Dentro de dos lecciones este nombre será el hostname que use la API
-e POSTGRES_USER/PASSWORD/DB Son las tres variables que la imagen oficial de PostgreSQL usa para inicializarse la primera vez. Coinciden con lo que espera server.js
-p 127.0.0.1:5432:5432 Publicamos solo en local para poder conectar con un cliente desde el host. Si escribieras -p 5432:5432, tu base de datos quedaría accesible desde toda la red local
postgres:16-alpine La misma imagen que descargaste en el módulo 2

Comprueba que está vivo:

docker ps --filter name=aurora-db --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker exec aurora-db pg_isready -U aurora
NAMES       IMAGE                STATUS
aurora-db   postgres:16-alpine   Up 12 seconds
/var/run/postgresql:5432 - accepting connections

pg_isready es la herramienta oficial de PostgreSQL para preguntar "¿aceptas conexiones?". La ejecutamos dentro del contenedor con docker exec, sin necesidad de tener PostgreSQL instalado en el host. Ese detalle —usar las herramientas que ya vienen en la imagen— es una de las comodidades más infravaloradas de Docker.

Y comprueba que la base de datos existe, aunque todavía esté vacía:

docker exec aurora-db psql -U aurora -d aurora_libros -c "\dt"
Did not find any relations.

Correcto: la base de datos aurora_libros está creada, pero no hay tablas. Falta ejecutar db/init.sql, y eso se hará de forma automática y elegante en la lección 03-06, cuando montes ese fichero dentro del contenedor.

La caché

docker run -d \
  --name aurora-cache \
  -p 127.0.0.1:6379:6379 \
  redis:7-alpine

Redis no necesita ninguna variable: arranca con su configuración por defecto.

docker exec aurora-cache redis-cli PING
docker exec aurora-cache redis-cli SET prueba "Aurora Libros"
docker exec aurora-cache redis-cli GET prueba
PONG
OK
"Aurora Libros"

Redis funciona. Dos contenedores vivos.

Y ahora la API... que sigue sin funcionar

docker run -d \
  --name aurora-api \
  -p 3000:3000 \
  --env-file ~/aurora-libros/aurora.env \
  auroralibros/aurora-api:1.1.0
b7e3d1a9f4c25e8b06d3a7f1c9e5b2d4a8f0c6e3b1d7a5f9c3e1b7d5a9f3c1e7

Docker ha aceptado el comando sin protestar. Pero:

sleep 3
docker ps -a --filter name=aurora-api --format "table {{.Names}}\t{{.Status}}"
NAMES        STATUS
aurora-api   Exited (1) 2 seconds ago

El contenedor ni siquiera se ha quedado en marcha. Ha arrancado y ha muerto con código de salida 1. Veamos por qué:

docker logs aurora-api
[cache] error: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] fallo al arrancar: getaddrinfo ENOTFOUND aurora-cache

Fíjate bien en el mensaje, porque ha cambiado respecto al módulo 2 y ese cambio es una pista de primer orden:

Configuración Error Qué significa
DB_HOST=localhost (módulo 2) ECONNREFUSED 127.0.0.1:5432 El nombre se resolvió (a sí mismo), pero nadie escucha en ese puerto dentro del contenedor
DB_HOST=aurora-db (ahora) getaddrinfo ENOTFOUND aurora-db El nombre ni siquiera se pudo resolver: para este contenedor, aurora-db no existe

Y aquí está la paradoja que da sentido al resto del módulo: aurora-db y aurora-cache están corriendo ahora mismo, en la misma máquina, publicando sus puertos. Puedes comprobarlo desde el host:

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES          STATUS          PORTS
aurora-cache   Up 4 minutes    127.0.0.1:6379->6379/tcp
aurora-db      Up 6 minutes    127.0.0.1:5432->5432/tcp

Están vivos, están publicados, y aun así aurora-api no los encuentra. ¿Por qué?

La respuesta corta: porque no comparten red. Cada contenedor tiene su propia pila de red, y en la red por defecto de Docker no hay resolución de nombres entre contenedores. La respuesta larga, con la solución completa y el curl que devuelve los ocho libros, es la lección 03-05. De momento, deja la incógnita planteada y no borres nada: estos dos contenedores te acompañan durante todo el módulo.

docker rm aurora-api

Borramos solo la API (está parada y no sirve de nada). aurora-db y aurora-cache se quedan.

Errores Comunes y Consejos

  • Poner opciones después de la imagen. docker run alpine -e VAR=1 sh intenta ejecutar un programa llamado -e. Las opciones van antes de la imagen, siempre.
  • Esperar poder añadir un puerto o una variable a un contenedor existente. No se puede: se fijan en la fase create. La solución es docker rm y volver a crear. Cuando esto te canse, ya estarás listo para el módulo 4.
  • Usar --rm mientras depuras. Si el contenedor falla y se borra, te quedas sin logs y sin inspect. Durante la investigación, quita el --rm.
  • -p 5432:5432 en una base de datos. Publica PostgreSQL en todas las interfaces, incluida la wifi del bar. Usa -p 127.0.0.1:5432:5432, o directamente no lo publiques: los contenedores no necesitan puertos publicados para hablar entre sí.
  • Invertir el orden en -p. -p 80:8080 con Nginx no funciona y el error es confuso, porque Docker publica alegremente el 80 del host hacia un 8080 en el que no escucha nadie. Primero el host, después el contenedor.
  • -t en scripts y CI. the input device is not a TTY. En automatización usa -i o nada, nunca -t.
  • Comillas en un --env-file. DB_PASSWORD="aurora_secreta" guarda la contraseña con las comillas incluidas y provoca un password authentication failed desconcertante. Sin comillas.
  • Consejo: usa --name siempre. Un contenedor sin nombre es un contenedor que dentro de diez minutos buscarás por ID en un docker ps -a con veinte líneas.
  • Consejo: escribe los docker run largos en varias líneas con \ al final, y guárdalos en un fichero de notas. Vas a repetirlos muchas veces, y en la lección 03-07 verás cuánto han crecido.

Ejercicios

Ejercicio 1: demuestra la separación entre create y start

Sin usar docker run en ningún momento, crea un contenedor de nginx:alpine llamado ejercicio-ciclo publicado en el puerto 8090 y con la variable ENTORNO=pruebas. Antes de arrancarlo, responde con comandos: ¿en qué estado está?, ¿responde el puerto 8090?, ¿aparece en docker ps? Después arráncalo, comprueba las tres cosas de nuevo, e intenta añadirle un segundo puerto (8091) sin borrarlo. Explica qué ocurre y por qué.

Ejercicio 2: domina la sobrescritura de ENTRYPOINT y CMD

Usando exclusivamente la imagen auroralibros/aurora-api:1.1.0 y sin reconstruirla, consigue estas cinco salidas, escribiendo el comando exacto en cada caso:

  1. La versión de Node instalada.
  2. El contenido de /app/package.json.
  3. Una shell interactiva dentro del contenedor.
  4. El resultado de console.log(process.env.DB_HOST) con DB_HOST valiendo aurora-db.
  5. La lista de ficheros de /app ejecutando ls directamente, sin que intervenga node ni sh.

Ejercicio 3: configuración por fichero para dos entornos

Crea dos ficheros de entorno, aurora-dev.env y aurora-pruebas.env, que difieran en PORT (3000 y 3100), DB_NAME (aurora_libros y aurora_libros_test) y REDIS_HOST. Arranca dos contenedores de la API con el mismo comando salvo el --env-file y el --name, verifica con printenv que cada uno tiene su configuración, y después arranca un tercero que use aurora-dev.env pero con PORT=3200 sobrescrito desde la línea de comandos. Responde: ¿cuántas imágenes distintas has necesitado?, y ¿qué línea de tu .gitignore impide que esto acabe en el repositorio?

Soluciones

Solución al ejercicio 1

docker create --name ejercicio-ciclo -p 8090:80 -e ENTORNO=pruebas nginx:alpine
2d8f4a1c7e93b5061fa8c2e7d4b9a6f3c1e8d5b2a9f7c4e1b8d5a2f9c6e3b1d7

Estado antes de arrancar:

docker ps -a --filter name=ejercicio-ciclo --format "table {{.Names}}\t{{.State}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker ps --filter name=ejercicio-ciclo -q
NAMES             STATE     PORTS
ejercicio-ciclo   created

000

Las tres respuestas: estado created, el puerto no responde (código 000, conexión rechazada) y no aparece en docker ps (la salida del tercer comando está vacía), porque docker ps sin -a solo lista contenedores en ejecución. La configuración está guardada, pero no hay proceso ni regla de red.

docker start ejercicio-ciclo
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker exec ejercicio-ciclo printenv ENTORNO
ejercicio-ciclo
200
pruebas

Ahora sí: aparece en docker ps, el puerto devuelve 200 y la variable que definiste en la fase create está presente.

El intento de añadir un puerto:

docker update -p 8091:80 ejercicio-ciclo
unknown flag: -p

No existe forma de hacerlo. docker update solo modifica recursos (memoria, CPU, política de reinicio — lección 03-07), nunca puertos, variables ni montajes: esos se fijan al crear el contenedor porque implican namespaces de red y reglas de reenvío que se construyen en el arranque. La única salida es recrear:

docker rm -f ejercicio-ciclo
docker run -d --name ejercicio-ciclo -p 8090:80 -p 8091:80 -e ENTORNO=pruebas nginx:alpine
docker rm -f ejercicio-ciclo

Esta rigidez es precisamente el argumento a favor de declarar la configuración en un fichero versionado en vez de en la línea de comandos, que es lo que hace Docker Compose en el módulo 4.

Solución al ejercicio 2

# 1. Versión de Node: el CMD "server.js" se sustituye por "--version",
#    y el ENTRYPOINT ["node"] sigue en su sitio → node --version
docker run --rm auroralibros/aurora-api:1.1.0 --version

# 2. Contenido de package.json: necesitamos "cat", que no es node.
#    Anulamos el entrypoint por completo.
docker run --rm --entrypoint cat auroralibros/aurora-api:1.1.0 /app/package.json

# 3. Shell interactiva: entrypoint sh + las dos letras de interactividad
docker run --rm -it --entrypoint sh auroralibros/aurora-api:1.1.0

# 4. Evaluar código con una variable de entorno: -e de Docker (antes de la
#    imagen) y -e de node (después). El mismo guion, dos significados.
docker run --rm -e DB_HOST=aurora-db auroralibros/aurora-api:1.1.0 \
  -e "console.log(process.env.DB_HOST)"

# 5. Ejecutar ls directamente, sin node ni sh de por medio
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -1 /app
v22.13.0
{
  "name": "aurora-api",
  ...
}
/app $
aurora-db
Dockerfile
node_modules
package-lock.json
package.json
server.js

El caso 4 es el más instructivo del ejercicio: hay dos -e en el mismo comando y significan cosas distintas. El primero está antes de la imagen, así que es la opción --env de Docker; el segundo está después, así que es un argumento que Docker entrega tal cual al ENTRYPOINT ["node"], y ahí -e es la opción de Node para evaluar código. La regla del apartado 2 lo explica sin ambigüedad.

El caso 5 muestra la diferencia entre --entrypoint ls y --entrypoint "". Con --entrypoint ls tendrías que escribir los argumentos después de la imagen igualmente, pero con --entrypoint "" la línea se lee de forma natural: la imagen deja de imponer ejecutable y todo lo que sigue es el comando completo.

Solución al ejercicio 3

cat > ~/aurora-libros/aurora-dev.env <<'EOF'
PORT=3000
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_libros
REDIS_HOST=aurora-cache
EOF

cat > ~/aurora-libros/aurora-pruebas.env <<'EOF'
PORT=3100
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_libros_test
REDIS_HOST=aurora-cache-test
EOF

Verificación con printenv, sin arrancar la API (que fallaría por lo que ya sabes):

docker run --rm --env-file ~/aurora-libros/aurora-dev.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOST

docker run --rm --env-file ~/aurora-libros/aurora-pruebas.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOST
3000
aurora_libros
aurora-cache
3100
aurora_libros_test
aurora-cache-test

El tercero, con sobrescritura puntual:

docker run --rm --env-file ~/aurora-libros/aurora-dev.env -e PORT=3200 \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME
3200
aurora_libros

PORT vale 3200 (ganó el -e) y DB_NAME conserva el valor del fichero. -e siempre tiene prioridad sobre --env-file, con independencia del orden en que los escribas en la línea de comandos: prueba a ponerlos al revés y obtendrás el mismo resultado.

Las dos respuestas finales:

  • Una sola imagen. auroralibros/aurora-api:1.1.0 ha servido para tres configuraciones distintas sin reconstruirse ni una vez. Este es el principio de construir una vez, desplegar en todas partes que viste en la lección 02-06: lo que cambia entre entornos es la configuración inyectada, jamás la imagen.
  • La línea del .gitignore es *.env (o aurora-*.env). Estos ficheros contienen DB_PASSWORD=aurora_secreta; en el repositorio no entran, y en el .dockerignore tampoco, para que no acaben en el contexto de construcción ni, por accidente, dentro de una capa de la imagen.

Conclusión

docker run ha dejado de ser una fórmula mágica. Sabes que son dos operaciones, create y start, y lo has demostrado ejecutándolas por separado: la fase create reserva la capa de escritura y congela la configuración —por eso no puedes añadir un puerto ni una variable a un contenedor existente—, y la fase start monta namespaces, cgroups y reglas de red y lanza el PID 1. Conoces la regla de oro del orden de los argumentos, que separa las opciones de Docker del comando de dentro, y tienes las cinco familias de opciones ordenadas: identidad, ejecución, red, configuración y datos.

Manejas -d para servicios, -it para personas y --rm para comandos de usar y tirar, sabiendo que ese --rm es tu enemigo en cuanto algo falla. Sabes sobrescribir la mitad CMD y la mitad ENTRYPOINT de una imagen por separado, incluida la anulación total con --entrypoint "". Sabes desacoplarte de una sesión con Ctrl+P Ctrl+Q sin matar el proceso, y por qué docker attach es una herramienta afilada que puede tumbarte un servicio con un Ctrl+C distraído, frente a docker exec, que lanza un proceso independiente. Y sabes llevar la configuración fuera de la imagen con --env-file, con sus reglas de formato estrictas y su contraseña que nunca entra ni en Git ni en el contexto de construcción.

Sobre todo, la plataforma ha empezado a moverse: aurora-db y aurora-cache están corriendo ahora mismo en tu máquina, con PostgreSQL 16 aceptando conexiones y Redis respondiendo PONG. Y aurora-api ya no da ECONNREFUSED: da getaddrinfo ENOTFOUND aurora-db, un error distinto que dice algo muy concreto —el nombre ni siquiera se resuelve— y que apunta directamente a la solución. Además, el contenedor no se quedó unhealthy: murió en dos segundos con código de salida 1.

Y eso plantea la pregunta con la que sigue el curso: ¿por qué unos contenedores se quedan corriendo indefinidamente y otros mueren al instante? En la siguiente lección, Ciclo de Vida del Contenedor, vas a ver los siete estados por los que pasa un contenedor y las transiciones entre ellos, la regla de que un contenedor vive exactamente lo que viva su proceso PID 1, la diferencia entre una parada ordenada con SIGTERM y un SIGKILL a los diez segundos, y la tabla de códigos de salida —ese 1, y también el 125, el 137 y el 143— que convierte un número críptico en un diagnóstico. Y le enseñarás a server.js a apagarse limpiamente, cerrando el pool de PostgreSQL y el cliente de Redis antes de irse.

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