Docker Compose: command frente a entrypoint
ENTRYPOINT define el programa y command sus argumentos. Consulta las 4 combinaciones de reemplazo en Compose y por qué entrypoint elimina el CMD de la imagen.
command de Docker Compose frente a entrypoint, en una sola regla
En Docker Compose, entrypoint: establece el programa que se ejecuta y command: establece los argumentos que se pasan a ese programa. El proceso del contenedor es la lista de entrypoint con la lista de command añadida al final. Todo el comportamiento restante de esta página se deriva de esa única frase.
Estas dos claves corresponden a dos instrucciones de Dockerfile. entrypoint: reemplaza el ENTRYPOINT de la imagen. command: reemplaza el CMD de la imagen. No son independientes, y ahí es donde surgen los problemas: establecer entrypoint: también descarta el CMD de la imagen. La especificación de Compose lo indica directamente. Si entrypoint no es nulo, Compose ignora cualquier comando predeterminado de la imagen.
Lee lo que ya declara la imagen
Antes de sobrescribir nada, revise lo que incluye la imagen.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16Obtiene ["docker-entrypoint.sh"] y ["postgres"], por lo que el contenedor ejecuta docker-entrypoint.sh postgres. Ese script crea el directorio de datos durante el primer arranque, lee las variables POSTGRES_*, abandona los privilegios y cambia al usuario postgres y, por último, ejecuta los argumentos que recibió. La decisión depende de qué parte quiera cambiar. Para pasar una opción a la base de datos, sustituya command:. Si sustituye entrypoint:, no se ejecuta ninguna de esas tareas de preparación.
Las cuatro combinaciones, mostradas en una imagen pequeña
Cree una imagen cuya única función sea mostrar la lista de argumentos con la que se inició.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemoEjecute docker compose up después de cada cambio y lea la única línea que registra.
- Ninguna clave configurada. El proceso es
/bin/echo ep cmdy el registro muestraep cmd. - Sólo
command: ["cmd2"]. El proceso es/bin/echo ep cmd2. El entrypoint no cambia y sólo se modifican los argumentos. - Sólo
entrypoint: ["/bin/echo", "ep2"]. El proceso es/bin/echo ep2y el registro muestraep2. Se elimina elcmdde la imagen y no aparece ningún aviso. - Ambas claves configuradas. El proceso es
/bin/echo ep2 cmd2. Este es el único caso en el que se controla la lista completa de argumentos.
Por qué establecer entrypoint elimina CMD de la imagen
El CMD de una imagen se escribe como la lista de argumentos predeterminada para el ENTRYPOINT de esa imagen. Si se reemplaza el entrypoint, esos argumentos pasan a pertenecer a un programa que ya no se ejecuta. Por eso Compose los descarta en lugar de construir una línea de comandos que el autor de la imagen nunca pretendió crear. docker run --entrypoint se comporta de la misma forma. Por tanto, es un comportamiento de Docker, no una particularidad de Compose.
La consecuencia es concreta. nginx:1.27 declara ENTRYPOINT ["/docker-entrypoint.sh"] y CMD ["nginx", "-g", "daemon off;"]. Establezca entrypoint: /custom-init.sh y el script se iniciará con una lista de argumentos vacía. Un script que termina con el exec "$@" habitual no tendrá nada que ejecutar. Por tanto, exec no hace nada, el script llega a su última línea y el contenedor termina con el código 0 sin mostrar ningún mensaje de error. Vuelva a especificar los argumentos manualmente:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]La regla que debe recordar es la siguiente: cada vez que establezca entrypoint:, decida qué debe contener command: en la misma modificación.
Sintaxis exec y sintaxis shell, y diferencias de Compose
Un Dockerfile acepta dos sintaxis. CMD ["nginx", "-g", "daemon off;"] es la sintaxis exec: el binario se ejecuta directamente, sin intervención de un shell. CMD nginx -g "daemon off;" es la sintaxis shell: Docker la reescribe como /bin/sh -c 'nginx -g "daemon off;"', por lo que primero se ejecuta un shell y el programa se convierte en su proceso hijo.
Compose no aplica esta regla, lo que suele causar confusión. Una cadena en command: se divide en argumentos y se ejecuta directamente, sin una envoltura /bin/sh -c. La referencia de Compose lo indica explícitamente: el campo command no se ejecuta dentro del contexto SHELL definido en la imagen. Si necesita funciones del shell, debe invocarlo explícitamente.
Por eso command: echo "hello $$HOSTNAME" imprime el texto literal hello $HOSTNAME. Ningún shell recibió la cadena, así que no se expandió. Solicite un shell cuando lo necesite:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Señales, PID 1 y un docker compose down limpio
docker compose stop y docker compose down envían SIGTERM a PID 1 dentro de cada contenedor, esperan stop_grace_period y después envían SIGKILL. El período de gracia predeterminado es de 10 segundos.
PID 1 es especial en Linux. El kernel no aplica la acción predeterminada de una señal a PID 1. Por eso, un proceso que no instala un controlador de SIGTERM simplemente ignora SIGTERM cuando se ejecuta como PID 1. Permanece activo durante todo el período de gracia y después se termina de forma inmediata. Esto interrumpe cualquier conexión abierta o transacción no confirmada.
Un shell delante del programa aumenta la probabilidad de que ocurra este problema, porque el shell es PID 1 y la mayoría de los shells no reenvían las señales a un proceso hijo. Algunos shells se reemplazan por el comando final de una cadena -c. Por eso, en ocasiones el programa sí llega a PID 1. Esto depende del shell y de la cadena exacta. No lo dé por supuesto. Compruébelo:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoSi PID 1 aparece como /bin/sh -c ... en lugar de su programa, hay dos soluciones. Use la forma exec en la imagen o conserve el shell y entregue el control del proceso con exec:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec reemplaza el proceso del shell por su programa en lugar de crear un proceso hijo. Así, el programa hereda PID 1 y recibe la señal.
Algunos programas crean procesos hijos y nunca los recolectan. Esto deja procesos zombis, porque PID 1 también actúa como recolector. Compose tiene una opción para resolverlo:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true ejecuta un proceso init pequeño como PID 1. Este reenvía las señales a su proceso y recolecta los procesos hijos. stop_grace_period da más tiempo a un apagado que realmente sea lento. Si su programa espera una señal diferente, stop_signal: SIGQUIT cambia la señal que envía Compose. Consulte lo que una imagen ya solicita con docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.
Si una pila tarda siempre diez segundos por servicio en docker compose down, eso indica que nada está gestionando SIGTERM. Corríjalo antes de atribuir el problema a las herramientas y consulte la diferencia entre docker compose down y stop para saber qué elimina cada subcomando.
La misma diferencia entre exec y shell aparece en otro lugar. Un healthcheck escrito como test: ["CMD", "curl", "-f", "http://localhost/"] ejecuta el binario directamente, mientras que test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] se ejecuta mediante un shell para que || tenga efecto. Cómo escribir healthchecks de Compose que fallen de forma fiable explica el resto de ese campo.
Añadir un indicador a una imagen oficial
Esto es lo que buscaban la mayoría de los lectores. Quiere añadir un indicador a postgres y no debe interferir con el script de inicialización.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:Sólo cambió command:, por lo que docker-entrypoint.sh todavía se ejecuta y sigue ejecutando lo que le indicó. Compruebe el resultado en lugar de darlo por supuesto:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'La salida debe mostrar 200. Si todavía muestra 100, ejecute docker compose config y confirme que command esperado aparece en la salida combinada. Compose combina los archivos de sustitución reemplazando command por completo, no añadiendo contenido, por lo que un segundo archivo que también establezca command: prevalece silenciosamente.
El ${POSTGRES_PASSWORD} anterior lo expande Compose en el host a partir de su archivo .env, antes de que exista el contenedor. Archivos de entorno y secretos en Compose explica dónde puede almacenar ese valor de forma segura.
Ejecutar una migración puntual con docker compose run
docker compose run crea un contenedor nuevo a partir de la misma definición de servicio y reemplaza el comando por lo que escriba después del nombre del servicio. El entrypoint de la imagen sigue ejecutándose, por lo que el contenedor se prepara exactamente igual que el contenedor de larga duración.
docker compose run --rm app python manage.py migrate--rmelimina el contenedor cuando termina el comando. Sin esta opción, cada ejecución deja un contenedor detenido, visible endocker compose ps -a.- Los puertos no se publican. Un contenedor
runignoraports:del servicio, a menos que añada--service-ports, por lo que no puede entrar en conflicto con el servicio que ya está activo. - Las dependencias se inician primero. Todo lo incluido en
depends_onse inicia antes que su comando, mientras que--no-depsomite ese comportamiento. - El contenedor recibe un nombre generado, como
myproject-app-run-9f2c1a, por lo que nunca entra en conflicto con el contenedor del servicio.
Para reemplazar también el entrypoint, existe una opción:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'La lista de argumentos resultante es /bin/sh -c 'python manage.py migrate', porque las palabras que aparecen después del nombre del servicio siguen siendo el comando. docker compose exec es la otra herramienta y funciona de otra manera: ejecuta un proceso dentro de un contenedor que ya está activo e ignora por completo entrypoint: y command:. Use run para una tarea que necesite un contenedor nuevo y exec para inspeccionar un contenedor activo. La chuleta de comandos de Compose muestra el resto de los subcomandos en paralelo.
¿Por qué mi contenedor se cierra inmediatamente?
Empiece por el código de salida, porque permite acotar rápidamente la causa.
docker compose ps -a
docker compose logs appCódigo de salida 0 y ninguna salida. El comando se ejecutó y terminó. La causa más habitual es una sustitución de entrypoint: que también eliminó el CMD de la imagen. Por eso el entrypoint se ejecutó con una lista de argumentos vacía y no tuvo nada que transferir.
Un error que termina en permission denied. El script no tiene el permiso de ejecución dentro de la imagen. Normalmente, el permiso nunca se estableció en el archivo del repositorio. Establézcalo durante la compilación con COPY --chmod=0755 entrypoint.sh /entrypoint.sh.
Un error que termina en no such file or directory para un archivo que puede ver claramente en la imagen. El script tiene finales de línea de Windows. Por eso, su primera línea se lee como #!/bin/sh más un byte de retorno de carro. El kernel busca un intérprete cuyo nombre incluya ese byte y no encuentra ninguno. Ejecute dos2unix entrypoint.sh y añada * text eol=lf a .gitattributes para evitar que vuelva a ocurrir.
executable file not found in $PATH. El binario indicado en command: no está en la imagen, o escribió un elemento integrado del shell como cd cuando se necesita un programa real.
Obtener un shell en una imagen cuyo entrypoint falla
Cuando el entrypoint termina antes de que pueda inspeccionar nada, reemplácelo:
docker compose run --rm --entrypoint /bin/sh appSi devuelve executable file not found in $PATH, la imagen no incluye ningún shell. Las imágenes basadas en Distroless y scratch suelen no incluirlo. Aun así, puede leer el sistema de archivos desde fuera sin iniciar el entrypoint:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeCuando necesite mantener el contenedor activo para poder conectarse a él varias veces, déjelo ejecutando un proceso que nunca termine. Coloque esto en un archivo de override que no vaya a confirmar en el repositorio:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []command: [] no es estrictamente necesario, porque establecer entrypoint: ya eliminó el CMD de la imagen, pero escribirlo deja constancia de la intención para quien lea el archivo después. Inícielo y acceda a él:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shAhora ejecute manualmente el entrypoint real y observe dónde se detiene. Así verá el mensaje de error en el terminal, en lugar de encontrarlo en un contenedor que terminó medio segundo antes. Si todavía está preparando su primer stack, una primera pila de Compose en un VPS explica la estructura de archivos que presupone todo lo anterior.
FAQ
¿Por qué mi contenedor se cierra inmediatamente después de ejecutar docker compose up?
Compruebe docker compose ps -a para ver el código de salida. La salida 0 sin ningún mensaje normalmente significa que configuró entrypoint: en el servicio. Esto también eliminó CMD de la imagen, por lo que el entrypoint se ejecutó con una lista de argumentos vacía y terminó. Vuelva a añadir los argumentos con command:. Un error que termina en permission denied significa que el script de entrypoint no tiene el bit de ejecución. Un error que termina en no such file or directory para un archivo existente significa que el script usa finales de línea de Windows. Por tanto, su línea shebang indica un intérprete que no existe.
¿Definir entrypoint en Compose elimina el CMD de la imagen?
Sí. Si entrypoint no es nulo, Compose ignora el comando predeterminado declarado por la imagen. Este comportamiento está documentado y coincide con docker run --entrypoint. La razón es que CMD de una imagen se escribe como argumentos para ENTRYPOINT de esa imagen. Al reemplazar el entrypoint, los argumentos anteriores dejan de corresponder a ningún elemento. Defina command: en el mismo servicio si el nuevo entrypoint todavía necesita argumentos.
¿Una cadena en el comando de Compose se ejecuta mediante un shell?
No. A diferencia de CMD de un Dockerfile, una cadena en command: de Compose se divide en argumentos y se ejecuta directamente, sin un contenedor /bin/sh -c. Por tanto, $VARIABLE nunca se expande mediante un shell dentro del contenedor. Llame usted mismo al shell cuando lo necesite, como en command: /bin/sh -c 'echo "hello $$HOSTNAME"'. El $$ duplicado escapa el signo de dólar para que Compose lo pase al contenedor en lugar de expandirlo en el host.
¿Por qué docker compose down tarda diez segundos con un solo contenedor?
Compose envía SIGTERM al PID 1, espera stop_grace_period (10 segundos de forma predeterminada) y después envía SIGKILL. El kernel no aplica las acciones de señal predeterminadas al PID 1. Por tanto, un programa sin un gestor SIGTERM ignora la señal y siempre espera durante todo el intervalo. Averigüe qué proceso es realmente el PID 1 con docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. Si es un shell, cambie la imagen a la forma exec o escriba exec dentro de la cadena del shell. Si el proceso crea procesos hijos que nunca recoge, defina init: true en el servicio.