Skip to content
Codeloom
Docker

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.

·6 min read · By Codeloom
Intermediate 10 min read

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:

  1. tini (PID 1) - reaps zombies, forwards signals
  2. docker-entrypoint.sh - runs setup, then exec "$@"
  3. 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.