Skip to content
← All posts

Why We Need a Real PID 1 for PHP Containers

Supervisord and shell scripts are not process managers. Here is why your PHP containers need proper signal handling, zombie reaping, and health-aware startup ordering.

· 8 min read · Sylvester Damgaard
Why We Need a Real PID 1 for PHP Containers

I've spent the last two years debugging container failures that all trace back to the same root cause: PHP-FPM isn't designed to be PID 1. Most teams either ignore this or paper over it with shell scripts and Supervisord. Both approaches break in production, and the failures are subtle enough that you might not notice until your deployment is handling real traffic.

The PID 1 Problem

In a Linux container, the first process (PID 1) has special responsibilities. The kernel sends signals like SIGTERM and SIGCHLD to PID 1 and expects it to handle them correctly. Specifically, PID 1 must:

  1. Forward signals to child processes. When Kubernetes sends SIGTERM to stop your pod, PID 1 must relay that signal to PHP-FPM so it can drain connections gracefully.

  2. Reap zombie processes. When a child process exits, it stays in the process table as a zombie until its parent calls wait(). If PID 1 doesn't reap zombies, they accumulate and eventually exhaust the PID space.

  3. Exit when children die. If PHP-FPM crashes, PID 1 should notice and either restart it or exit so the container orchestrator can restart the container.

PHP-FPM does none of these things when running as PID 1. It was designed to run under a traditional init system, not inside a container.

Why Shell Scripts Fail

The most common workaround is a shell entrypoint:

bash
#!/bin/sh
php-fpm -D
nginx -g 'daemon off;'

This starts PHP-FPM as a daemon and runs nginx in the foreground. The problems are immediate:

  • No signal forwarding. When the container receives SIGTERM, the shell might not forward it to either process. PHP-FPM keeps accepting requests while the container is being terminated.

  • No zombie reaping. Bash doesn't reap zombies from backgrounded processes. /bin/sh (often dash in Alpine) is even worse.

  • No health awareness. Nginx starts immediately, but PHP-FPM might not have its workers ready yet. The first requests return 502 errors.

  • No restart logic. If PHP-FPM crashes, the shell script has no idea. Nginx keeps running, returning 502s until the container health check finally fails.

Why Supervisord Is Not the Answer

Supervisord is the step most teams take after shell scripts fail. It handles process supervision and restarts, but it was built for long-running servers, not containers:

  • Signal handling is incomplete. Supervisord catches SIGTERM but its shutdown sequence isn't configurable enough for graceful PHP-FPM draining. You can't say "send SIGQUIT to FPM, wait 30 seconds, then SIGKILL."

  • No health checks. Supervisord monitors process existence (is the PID alive?) but not process health (is PHP-FPM actually accepting connections?). A process can be alive but completely broken.

  • No dependency ordering. You can't express "start nginx only after PHP-FPM's TCP socket is ready." Supervisord has priority for ordering, but no health-gate mechanism.

  • Heavy for containers. Supervisord pulls in Python and its standard library. In an Alpine container, that adds 40-50MB to your image.

What About tini and dumb-init?

Tools like tini and dumb-init solve exactly one problem: zombie reaping and signal forwarding for a single child process. They are the right answer if your container runs one process. But a typical PHP application container runs:

  • PHP-FPM (the application server)

  • Nginx (the web server, proxying to FPM over a unix socket)

  • Cron or a scheduler (for Laravel's schedule:run)

  • Queue workers (for background job processing)

With tini, you still need something to manage the other processes. You end up back at shell scripts or Supervisord, just with an extra binary in front.

A Purpose-Built PID 1

This is why I started building Cbox Init. It's a single Go binary designed specifically for container PID 1, with the features that PHP workloads actually need:

yaml
version: "1.0"

global:
  shutdown_timeout: 30

processes:
  php-fpm:
    command: ["php-fpm", "-F"]
    restart: always
    health_check:
      type: tcp
      address: "127.0.0.1:9000"
      interval: 2s

  nginx:
    command: ["nginx", "-g", "daemon off;"]
    depends_on: [php-fpm]
    restart: always

  scheduler:
    command: ["php", "artisan", "schedule:work"]
    depends_on: [php-fpm]
    restart: always

The key design decisions:

  • DAG-based startup ordering. Nginx doesn't start until PHP-FPM's TCP health check passes on port 9000. No more 502 errors during startup.

  • Proper signal cascade. On SIGTERM, Init sends SIGQUIT to PHP-FPM (graceful shutdown), waits for the configured timeout, and only then sends SIGKILL. Nginx gets SIGTERM. Each process can have its own signal configuration.

  • Zombie reaping. Init calls wait() in a loop, reaping any zombie processes from any child, including orphaned grandchild processes.

  • Health-aware restarts. If PHP-FPM's health check fails for more than the configured threshold, Init restarts it. Not just "is the PID alive" but "is the socket actually accepting connections."

  • Built-in cron. No need for a separate cron daemon. Init has a cron scheduler that runs commands on a schedule, with the same dependency and health awareness as managed processes.

The binary is 8MB, statically compiled, and has zero runtime dependencies. Compare that with Supervisord's Python stack.

The Multi-Process Container Debate

There's a school of thought that says containers should run one process each. Run PHP-FPM in one container, nginx in another, cron in a third. This is architecturally clean but operationally expensive:

  • You need a shared volume or TCP socket between the FPM and nginx containers

  • Startup ordering requires Kubernetes init containers or health check dependencies

  • Three containers means three sets of resource limits to tune

  • Log correlation across containers requires a centralized logging stack

For teams with mature Kubernetes infrastructure, the sidecar pattern works well. But for most PHP teams, a well-managed multi-process container is simpler, cheaper, and more reliable. The key is having a real PID 1, not pretending the problem doesn't exist.

Full documentation and configuration reference: Cbox Init docs.


// Sylvester Damgaard