Docker Entrypoint Patterns and Init Process Handling
Master ENTRYPOINT vs CMD, entrypoint scripts, signal handling, and init processes to build containers that start and stop reliably.
What you'll learn
- ✓The difference between ENTRYPOINT and CMD and how they combine
- ✓Writing entrypoint scripts for runtime configuration
- ✓Signal handling and graceful shutdown in containers
- ✓Using tini or docker-init to fix PID 1 zombie problems
- ✓Exec form vs shell form and why it matters
Prerequisites
None — this post is self-contained.
How a container starts and stops matters more than most teams realize. A misconfigured entrypoint can ignore shutdown signals, leak zombie processes, or prevent runtime configuration. This guide covers the patterns that make containers start correctly and stop gracefully.
ENTRYPOINT vs CMD
Both instructions set what runs when a container starts, but they serve different roles:
- ENTRYPOINT defines the executable. It is hard to override at runtime.
- CMD provides default arguments to the entrypoint. It is easy to override at runtime.
When both are present, Docker concatenates them:
ENTRYPOINT ["python"]
CMD ["app.py"]
# Runs: python app.py
If a user runs docker run myimage test.py, CMD is replaced:
# Runs: python test.py
docker run myimage test.py
The entrypoint stays; only the CMD portion changes. This pattern is useful for containers that always run the same binary but accept different arguments.
Exec Form vs Shell Form
This distinction causes more production issues than any other Dockerfile concept.
Exec form (recommended):
ENTRYPOINT ["python", "app.py"]
Docker runs the process directly as PID 1. The process receives signals (SIGTERM, SIGINT) and can shut down gracefully.
Shell form:
ENTRYPOINT python app.py
Docker wraps this in /bin/sh -c python app.py. The shell becomes PID 1, and your Python process is a child. When Docker sends SIGTERM to stop the container, the shell receives it but does not forward it to the child. After a 10-second timeout, Docker sends SIGKILL, force-killing the process without graceful shutdown.
Always use exec form for ENTRYPOINT and CMD. Shell form is acceptable only in RUN instructions where signal handling is irrelevant.
The Entrypoint Script Pattern
Many containers need runtime configuration: waiting for a database, running migrations, substituting environment variables into config files. An entrypoint script handles this setup before handing off to the main process.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "app.py"]
The entrypoint script:
#!/bin/sh
set -e
# Wait for the database to be reachable
echo "Waiting for database..."
until pg_isready -h "$DB_HOST" -p "${DB_PORT:-5432}" -q; do
sleep 1
done
echo "Database is ready."
# Run migrations if requested
if [ "$RUN_MIGRATIONS" = "true" ]; then
echo "Running database migrations..."
python manage.py migrate --no-input
fi
# Hand off to CMD
exec "$@"
The critical line is exec "$@". This replaces the shell process with whatever CMD specifies. After exec, the main application becomes PID 1 and receives signals directly. Without exec, the shell remains PID 1 and signal handling breaks.
Signal Handling and Graceful Shutdown
When you run docker stop, Docker sends SIGTERM to PID 1 and waits 10 seconds (configurable with --stop-timeout). If the process has not exited, Docker sends SIGKILL.
Your application must handle SIGTERM. In Python:
import signal
import sys
def handle_sigterm(signum, frame):
print("Received SIGTERM, shutting down gracefully...")
# Close database connections, finish in-flight requests, etc.
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)
In Node.js:
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
server.close(() => {
process.exit(0);
});
});
Without signal handling, your application is force-killed after the timeout, which can corrupt in-flight transactions, leave connections dangling, or cause data loss.
The PID 1 Problem and Init Systems
PID 1 in Linux has a special responsibility: it must reap zombie (defunct) child processes. Normal applications do not do this. If your application spawns child processes and does not wait for them, zombies accumulate.
Docker provides a built-in init process to solve this:
docker run --init myapp:latest
The --init flag injects tini, a minimal init process, as PID 1. Tini forwards signals to your application and reaps zombie children. Your application runs as PID 2.
In Docker Compose:
services:
app:
image: myapp:latest
init: true
You can also install tini directly in your image for environments where --init is not available:
FROM python:3.12-slim
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tini", "--"]
CMD ["python", "app.py"]
Use --init or tini when your application spawns subprocesses. If your application is a single process with no children, it is optional but still a good safety net.
Combining Entrypoint Scripts with Init
When you need both an entrypoint script and an init process:
FROM python:3.12-slim
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
CMD ["python", "app.py"]
The process tree is:
- tini (PID 1) - reaps zombies, forwards signals
- docker-entrypoint.sh - runs setup, then
exec "$@" - python app.py - replaces the shell via exec, becomes direct child of tini
Common Anti-Patterns
Starting multiple processes without supervision. If your entrypoint starts a background worker and a web server, use a process manager (supervisord) or split them into separate containers. Do not use & to background processes in an entrypoint script.
Using shell form for ENTRYPOINT. This wraps your process in a shell that swallows signals. Always use exec form.
Forgetting exec "$@" in entrypoint scripts. Without exec, the shell stays as the parent process, preventing signal forwarding and wasting a process slot.
Hardcoding the application command in ENTRYPOINT. Put the command in CMD so users can override it for debugging:
# Enter a shell for debugging instead of running the app
docker run -it myapp sh
Health Checks and Startup
Combine entrypoint configuration with health checks to prevent traffic from reaching a container before it is ready:
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
The --start-period gives the entrypoint script time to run migrations or wait for dependencies before Docker starts counting health check failures.
Practical Recommendations
Use exec form for all ENTRYPOINT and CMD instructions. Write an entrypoint script when you need runtime setup, and always end it with exec "$@". Add --init or tini when your application spawns child processes. Handle SIGTERM in your application code for graceful shutdown. These patterns take minutes to implement and prevent entire categories of production incidents.
Related articles
- Docker Docker vs Podman: Which Container Runtime to Use
Compare Docker and Podman on architecture, security, compatibility, and developer experience. Learn which container runtime fits your workflow in 2026.
- Docker Docker Compose Watch: Hot Reload for Containers
Learn how to use Docker Compose Watch to automatically sync file changes and rebuild containers during development. Faster feedback loops without manual restarts.
- Docker Docker Init: Generate Dockerfiles Automatically
Learn how to use docker init to scaffold Dockerfiles, Compose files, and .dockerignore for your projects in seconds. Supports Node.js, Python, Go, Rust, and more.
- Docker Docker Scout: Find and Fix Image Vulnerabilities
Learn how to use Docker Scout to scan container images for CVEs, analyze software dependencies, and get actionable remediation advice directly from the CLI.