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
- Qué hace
docker runrealmente:create+start - Anatomía del comando y el orden de los argumentos
- Familia identidad:
--namey--hostname - Familia ejecución:
-d,-it,--rm,-w,-u,--entrypoint - Familia red y puertos: lo mínimo para trabajar
- Familia configuración:
-ey--env-file - Familia datos:
-ven su versión mínima - Sobrescribir el
CMDy elENTRYPOINTdesde la línea de comandos - Primer plano, segundo plano y cómo desacoplarse
docker attachfrente adocker exec- Práctica: la base de datos y la caché de Aurora Libros
- Qué hace
docker run realmente: create + start
docker run realmente: create + startdocker run es un atajo. Por debajo ejecuta dos operaciones distintas que puedes invocar por separado:
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:
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:
Ahora arráncalo:
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:8080Esta 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:
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"]
- Anatomía del comando y el orden de los 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: 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 $PATHDocker 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:
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) |
- Familia identidad:
--name y --hostname
--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:
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: 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:
Sin --hostname, el hostname es el ID corto del contenedor:
--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.
- Familia ejecución:
-d, -it, --rm, -w, -u, --entrypoint
-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
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
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
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:
--rmes incompatible con la depuración: si el contenedor falla, se borra junto con sus logs y suinspect. Cuando algo va mal, quita el--rmpara poder investigar (lección 03-04).--rmborra 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-w /tmpsobrescribe elWORKDIR /appque fijaste en el Dockerfile.-u 1000:1000fuerza un UID:GID concreto aunque ese usuario no exista dentro de la imagen (por esoidmuestra los números sin nombre).-u rootanula elUSER nodede tu imagen. Es útilísimo para depurar (instalar una herramienta dentro de un contenedor sin privilegios), y a la vez es un recordatorio de queUSERes 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:
/app $ ls
Dockerfile node_modules package-lock.json package.json server.js
/app $ node --version
v22.13.0Sin --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.
- Familia red y puertos: lo mínimo para trabajar
Aquí solo lo imprescindible; el porqué completo llega en la lección 03-05.
-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 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:
EXPOSEen el Dockerfile y--exposeenrunno abren nada: son documentación que consumen-Py 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:
- Familia configuración:
-e y --env-file
-e y --env-fileTu 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| 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:
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=6379Y úsalo:
docker run --rm --env-file ~/aurora-libros/aurora.env \
--entrypoint printenv auroralibros/aurora-api:1.1.0 DB_HOST DB_NAMELas 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/.dockerignoreEl 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.
- Familia datos:
-v en su versión mínima
-v en su versión mínimaLo 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:alpineLa 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.
- Sobrescribir el
CMD y el ENTRYPOINT desde la línea de comandos
CMD y el ENTRYPOINT desde la línea de comandosRetomamos la tabla de la lección 02-04, ahora desde el lado de la ejecución. Tu imagen declara:
El comando efectivo es la concatenación ENTRYPOINT + CMD → node 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.jsAurora 4
Dockerfile
node_modules
package-lock.json
-rw-r--r-- 1 node node 4187 Aug 4 19:12 /app/server.jsDos matices que causan sorpresas:
- Sobrescribir el
ENTRYPOINTdescarta elCMDde la imagen. En la fila 4 de la tabla, elCMD ["server.js"]desaparece: si no quieres queshrecibaserver.jscomo 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.
- 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-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 19:41:07 [notice] 1#1: start worker processesAhí 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:
Sigue vivo. Tres condiciones para que la secuencia funcione, y su ausencia explica el 90 % de los "a mí no me va":
- El contenedor debe haberse arrancado con
-t(necesita el pseudo-terminal). - Debe estar enganchado a stdin, es decir, con
-i. - 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:
Ahora la combinación es Ctrl+E seguida de e. Puedes hacerlo permanente en ~/.docker/config.json:
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 attach frente a docker exec
docker attach frente a docker execUn contenedor en segundo plano se puede "volver a mirar" de dos formas radicalmente distintas:
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:
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:
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:
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.
- 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-alpineRepasemos 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 auroraNAMES IMAGE STATUS
aurora-db postgres:16-alpine Up 12 seconds
/var/run/postgresql:5432 - accepting connectionspg_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:
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é
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 pruebaRedis 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.0Docker ha aceptado el comando sin protestar. Pero:
El contenedor ni siquiera se ha quedado en marcha. Ha arrancado y ha muerto con código de salida 1. Veamos por qué:
[cache] error: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] fallo al arrancar: getaddrinfo ENOTFOUND aurora-cacheFí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 sí 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:
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/tcpEstá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.
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 shintenta 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 esdocker rmy volver a crear. Cuando esto te canse, ya estarás listo para el módulo 4. - Usar
--rmmientras depuras. Si el contenedor falla y se borra, te quedas sin logs y sininspect. Durante la investigación, quita el--rm. -p 5432:5432en 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:8080con 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. -ten scripts y CI.the input device is not a TTY. En automatización usa-io nada, nunca-t.- Comillas en un
--env-file.DB_PASSWORD="aurora_secreta"guarda la contraseña con las comillas incluidas y provoca unpassword authentication faileddesconcertante. Sin comillas. - Consejo: usa
--namesiempre. Un contenedor sin nombre es un contenedor que dentro de diez minutos buscarás por ID en undocker ps -acon veinte líneas. - Consejo: escribe los
docker runlargos 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:
- La versión de Node instalada.
- El contenido de
/app/package.json. - Una shell interactiva dentro del contenedor.
- El resultado de
console.log(process.env.DB_HOST)conDB_HOSTvaliendoaurora-db. - La lista de ficheros de
/appejecutandolsdirectamente, sin que intervenganodenish.
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
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 -qLas 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 ENTORNOAhora 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:
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-cicloEsta 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 /appv22.13.0
{
"name": "aurora-api",
...
}
/app $
aurora-db
Dockerfile
node_modules
package-lock.json
package.json
server.jsEl 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
EOFVerificació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_HOSTEl 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_NAMEPORT 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.0ha 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
.gitignorees*.env(oaurora-*.env). Estos ficheros contienenDB_PASSWORD=aurora_secreta; en el repositorio no entran, y en el.dockerignoretampoco, 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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
