SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Docker Compose exec: abrir un shell interactivo

Aprenda a abrir un shell en un servicio activo con docker compose exec y cuándo usar run --rm si está detenido o no debe interrumpirse.

Obtenga un shell interactivo con docker compose exec

docker compose exec web bash abre un shell interactivo dentro del contenedor que ya está en ejecución como el servicio web. El nombre que aparece después de exec es el nombre del servicio definido en compose.yaml, no el nombre del contenedor. Si la imagen no incluye bash, solicite sh en su lugar.

docker compose ps
docker compose exec web bash

Ejecute primero docker compose ps. Debe mostrar web con el estado running. Después, el segundo comando le sitúa en un indicador dentro del contenedor; exit o Ctrl-D le devuelve al host. El servicio sigue en ejecución después de salir porque exec inició un segundo proceso junto al proceso principal. Cerrar el shell no afecta a PID 1 (ID de proceso 1), que es el proceso para el que se creó el contenedor.

Esta es una de las dos formas de acceder al contenedor. exec se conecta a un contenedor que ya existe. docker compose run crea un contenedor nuevo a partir de la misma definición de servicio. Casi todo lo demás de esta guía se deriva de esa única diferencia.

Por qué -it es opcional en Compose, pero obligatorio con docker sin opciones

Dos flags controlan la parte interactiva de una sesión. -i mantiene stdin abierto, por lo que lo que escriba llega al proceso. -t asigna un terminal pseudo, llamado TTY, para que el shell muestre un indicador y gestione las teclas de dirección. docker exec deja ambos desactivados de forma predeterminada, por eso todos los ejemplos anteriores escriben docker exec -it. docker compose exec activa ambos por usted, de modo que docker compose exec -it web bash y docker compose exec web bash hacen lo mismo. Compose también acepta -it para mantener la compatibilidad con el uso habitual de comandos antiguos.

La ausencia de un TTY se detecta en segundos. El shell se ejecuta, pero no muestra ningún indicador y Ctrl-C nunca llega al proceso. El caso contrario, en el que debe pedir a Compose que no asigne un TTY, tiene su propio flag y su propia sección más adelante.

Qué hacer cuando la imagen no tiene bash

Solicite bash a una imagen basada en Alpine y exec fallará así:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Ese mensaje no indica un problema de exec. Indica que el binario solicitado no está en la imagen. Alpine incluye BusyBox, que proporciona ash como /bin/sh, pero no incluye bash. Por tanto, solicite sh:

docker compose exec web sh

Las imágenes basadas en Debian y Ubuntu, incluidas las etiquetas -slim, sí incluyen bash. bash proporciona historial de comandos y una finalización más completa. Por eso, pruebe primero con bash y use sh como alternativa. sh existe en casi todas las imágenes de propósito general.

Algunas imágenes no tienen ningún shell. Las imágenes Distroless y las imágenes compiladas FROM scratch contienen el binario de la aplicación y sus bibliotecas, y nada más. Esto es intencionado, porque un shell que no existe no puede utilizarse contra usted. En estas imágenes, sh falla con el mismo mensaje y no queda ninguna otra opción. Hay dos métodos que funcionan. Las imágenes Distroless de Google publican etiquetas :debug que añaden un shell de BusyBox. Cambiar temporalmente a esa etiqueta permite acceder al contenedor. También puede iniciar un contenedor independiente dentro de los espacios de nombres del contenedor de destino:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

Ahora las herramientas de netshoot apuntan a la red de la aplicación. Por tanto, curl localhost:8080 y ss -lntp se comportan como si estuviera dentro de ella. El sistema de archivos que ve pertenece a netshoot, no a la aplicación. Como el espacio de nombres de procesos se comparte, ls /proc/1/root/ permite acceder a los archivos propios del destino cuando se ejecuta como root.

Cuando el servicio no está en ejecución, use docker compose run --rm

exec necesita un contenedor en ejecución. Si lo apunta a un servicio detenido, rechaza la operación:

service "web" is not running

No iniciará nada por usted. docker compose run sí lo hará:

docker compose run --rm web bash

run crea un contenedor nuevo a partir de la definición del servicio web, con la misma imagen, el mismo entorno, los mismos volúmenes y las mismas redes, y reemplaza el comando del servicio por el que haya escrito. --rm elimina ese contenedor cuando sale. Si omite --rm, los contenedores sobrantes se acumulan con nombres como myproject-web-run-4f1c2b. docker compose ps -a los mostrará, pero ningún otro proceso los eliminará.

Hay dos comportamientos de run que suelen sorprender. No publica los puertos del servicio a menos que añada --service-ports. Es intencionado: un segundo contenedor que intente enlazar el puerto 8080 del host mientras el primero todavía lo utiliza fallaría con bind: address already in use. run también inicia todo lo que el servicio enumera en depends_on antes de mostrar el shell. Por tanto, una consulta rápida dentro del contenedor puede iniciar una base de datos y una caché. --no-deps omite ese comportamiento.

run pasa por el ENTRYPOINT de la imagen; exec no. exec inicia el comando directamente en el contenedor existente, por lo que el script de entrada nunca lo recibe. Con run, su bash llega como argumentos para ese script. Muchas imágenes oficiales terminan su entrypoint con exec "$@", de modo que los argumentos pasan directamente y se obtiene el shell. Un script que interprete sus propios argumentos hará otra cosa con ellos. En ese caso, puede reemplazar el entrypoint para esa ejecución:

docker compose run --rm --entrypoint sh web

Esta es la razón más habitual por la que un comando que funciona con exec se comporta de forma distinta con run. La diferencia entre command y entrypoint explica qué parte de la configuración de la imagen reemplaza en cada caso.

exec o run: cómo elegir

  • exec necesita un contenedor en ejecución. run no. Además, run puede iniciar dependencias.
  • exec ve la lista de procesos activa y los archivos tal como están en ese momento, incluidos los cambios que la aplicación haya escrito desde que se inició. run obtiene una copia limpia de la imagen, por lo que no contiene esos cambios.
  • exec omite el entrypoint. run lo ejecuta.
  • run deja un contenedor creado, salvo que se pase `--rm`.

Use exec para comprobar qué está ocurriendo realmente. Use `run --rm para crear una copia temporal del mismo entorno, ejecutar una migración puntual o cuando el servicio real no permanezca activo el tiempo suficiente para ejecutar exec` en él.

Indicadores útiles de exec: usuario, directorio de trabajo y réplicas

La mayoría de las imágenes cambia a un usuario que no es root, por lo que la instalación de una herramienta de diagnóstico dentro del shell de exec se detiene aquí:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u rootproporciona un shell de root en el mismo contenedor:

docker compose exec -u root web sh

-w /srv/appestablece el directorio de trabajo sólo para ese comando. -e KEY=valueañade una variable de entorno a la sesión, pero no al servicio. Cuando un servicio ejecuta más de una réplica, --index 2decide en qué contenedor se inicia la sesión. Si está buscando la causa de los permisos de propietario en un directorio montado, PUID y PGID en imágenes de contenedor explica por qué los identificadores numéricos, y no los nombres de usuario, determinan quién puede escribir allí.

Obtener un shell de psql o mysql dentro del contenedor de la base de datos

El cliente ya está incluido en la imagen de la base de datos. Por tanto, no necesita instalarlo en el host ni publicar el puerto:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Las imágenes de Postgres incluyen psql, las imágenes de MySQL incluyen mysql y las imágenes de MariaDB incluyen mariadb. La conexión se realiza desde dentro del contenedor, por lo que funciona aunque el archivo de Compose no publique ningún puerto de la base de datos. Esta es la configuración más segura: nada en Internet puede acceder a un puerto que no se haya publicado.

Un error habitual puede costar una tarde de trabajo. El shell expande las variables en el host antes de que Docker vea el comando. Por eso, -U "$POSTGRES_USER" envía una cadena vacía cuando esa variable sólo existe dentro del contenedor. Las comillas simples y un shell dentro del contenedor hacen la expansión en el lugar correcto:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

No ejecute docker compose run --rm db sin un comando en este caso. Eso inicia un segundo servidor de Postgres con el mismo volumen de datos y no puede arrancar:

FATAL:  lock file "postmaster.pid" already exists

El archivo de bloqueo funciona como debe, porque dos servidores que escribieran en un mismo directorio de datos lo corromperían. Mientras la base de datos esté activa, use exec para acceder al contenedor en ejecución. Decidir si la base de datos debe estar en Compose es un asunto independiente, y ejecutar la base de datos en Docker o en el host expone las ventajas y desventajas.

Servicios que necesitan una consola al iniciar: stdin_open y tty

exec y run cubren los shells que abre manualmente. Un servicio cuyo proceso principal es interactivo por naturaleza necesita dos claves en el archivo compose:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true es docker run -i y tty: true es docker run -t. Sin ellas, el contenedor se inicia y termina inmediatamente con el código 0, y docker compose ps -a muestra Exited (0). No se ha producido ningún fallo. python, al no tener un terminal en stdin, lee inmediatamente el fin de archivo y termina con normalidad. Este es el comportamiento correcto para un programa al que nadie introduce datos.

Con ambas claves configuradas, conéctese al proceso en ejecución:

docker attach $(docker compose ps -q console)

Desconéctese con Ctrl-P y después Ctrl-Q. El proceso seguirá en ejecución. Esta secuencia sólo funciona cuando el contenedor tiene un TTY y stdin abierto. Ctrl-C envía una interrupción a PID 1 y detiene el servicio.

Deje ambas claves desactivadas para los servicios habituales. Un servidor web nunca lee stdin, y tty: true hace que muchos programas cambien a la salida en color y al almacenamiento en búfer por líneas porque creen que una persona los está observando. Esto llena docker compose logs de códigos de escape.

Por qué exec mediante scripts falla en cron y CI: la opción -T

Un comando exec que funciona en el terminal falla dentro de un trabajo de cron o de un ejecutor de integración continua (CI):

the input device is not a TTY

Compose solicita un terminal pseudo-TTY de forma predeterminada, pero cron no asigna ningún terminal al trabajo. Por eso, la solicitud falla antes de ejecutar el comando. -T desactiva esta solicitud:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T es importante por otro motivo. Un TTY modifica el flujo de bytes al enviarlo, por lo que un volcado comprimido que pasa por él llega dañado. Cualquier salida redirigida o canalizada necesita -T.

Hay otros dos detalles de cron. Pase -f con una ruta absoluta, porque cron ejecuta el trabajo desde el directorio personal, donde no hay ningún archivo de Compose. Entonces Compose se detiene con no configuration file provided: not found. Además, exec devuelve el código de salida del comando que ejecuta. Por eso, un pg_dump fallido hace que el script falle con set -e, en lugar de escribir una copia de seguridad vacía e informar de que la operación terminó correctamente. El resto de los comandos habituales se recopila en una guía rápida de comandos de Compose que conviene tener junto a esos scripts.

Por qué desaparecen los cambios que realiza dentro de un contenedor

Instala una herramienta con exec, edita un archivo de configuración, corrige el problema y, una semana después, la corrección ha desaparecido. Esto ocurre porque la capa escribible del contenedor funciona así. docker compose up -d destruye el contenedor antiguo y crea uno nuevo a partir de la imagen después de cualquier cambio en la etiqueta de la imagen o en la definición del servicio. Todas las ediciones manuales desaparecen con el contenedor antiguo.

docker compose restart funciona de otra forma. Detiene e inicia el mismo contenedor, por lo que las ediciones manuales se conservan. Por eso una corrección manual puede parecer permanente durante semanas y desaparecer después durante una actualización no relacionada. Los volúmenes con nombre y los bind mounts sobreviven a ambas operaciones porque sus datos se almacenan fuera del contenedor. bind mounts y volúmenes con nombre explica cuál debe elegir para los datos que desea conservar.

Por tanto, use un shell de exec como lugar para leer y probar. Cuando conozca la corrección, escríbala en un lugar donde se conserve: añada el paquete al Dockerfile y la configuración al archivo compose. Después, docker compose up -d para aplicarla y confirme con otro exec que el contenedor nuevo realmente la contiene.

FAQ

¿Cuál es la diferencia entre docker compose exec y docker compose run?

exec ejecuta un comando dentro de un contenedor que ya está en ejecución, junto al proceso principal, y omite el entrypoint de la imagen. run crea un contenedor nuevo a partir de la misma definición de servicio, con la misma imagen, el mismo entorno, los mismos volúmenes y las mismas redes, pasa el comando por el entrypoint e inicia primero los servicios depends_on. Además, run deja sin publicar los puertos del servicio, a menos que añada --service-ports. Use exec para inspeccionar el servicio activo. Use run --rm cuando el servicio esté detenido o cuando no quiera afectarlo.

¿Por qué docker compose exec indica que el servicio no está en ejecución?

exec se conecta a un contenedor existente y no puede crear uno. Por eso, si el servicio está detenido o se ha bloqueado, muestra service "web" is not running. Compruebe docker compose ps -a, que muestra los contenedores detenidos con un estado como Exited (1), y lea docker compose logs web para conocer el motivo de la detención. Para obtener un shell de todos modos, ejecute docker compose run --rm --entrypoint sh web. Esto crea un contenedor nuevo a partir de la misma definición de servicio sin ejecutar el comando de inicio defectuoso.

¿Cómo abro un shell cuando la imagen no incluye bash?

Si docker compose exec web bash falla con exec: "bash": executable file not found in $PATH, significa que bash no está presente en la imagen. Esto es normal en cualquier imagen basada en Alpine. Use docker compose exec web sh, porque BusyBox proporciona /bin/sh. Las imágenes Distroless y scratch no contienen ningún shell, por lo que ningún comando exec funcionará. Cambie a la etiqueta :debug de la imagen si el publicador ofrece una, o inicie un contenedor de depuración en los espacios de nombres del destino con docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, donde $CID procede de docker compose ps -q web.

¿Por qué mi comando exec falla con «the input device is not a TTY» en cron?

docker compose exec solicita un terminal pseudo-TTY de forma predeterminada y cron no proporciona ninguno. Por eso, la solicitud falla antes de que se ejecute el comando. Añada -T para desactivarlo: docker compose exec -T db pg_dump -U postgres app. Use -T también para cualquier salida redirigida o canalizada, porque un TTY modifica el flujo de bytes y daña un volcado binario. En cron, pase también -f con la ruta absoluta al archivo de Compose. De lo contrario, Compose termina con no configuration file provided: not found.

¿Los cambios que hago dentro de un contenedor con exec sobreviven a un reinicio?

Sobreviven a docker compose restart, que reutiliza el mismo contenedor. Se pierden con docker compose up -d después de cualquier cambio en la imagen o en la configuración, porque vuelve a crear el contenedor a partir de la imagen y descarta su capa escribible. Los datos escritos en volúmenes con nombre o montajes bind sobreviven a ambos procesos, porque se almacenan fuera del contenedor. Haga los cambios de diagnóstico con exec y después incluya la versión permanente en el Dockerfile o en el archivo de Compose.