ServerlessBase Blog
  • What are Init Systems in Containers? (tini, dumb-init)

    Understanding init systems in containers, why they're needed, and how tini and dumb-init help manage processes in containerized environments.

    What are Init Systems in Containers? (tini, dumb-init)

    You've probably deployed a container and noticed it exits immediately after starting. You run docker run myimage, and it disappears. No error messages, no logs, just gone. This happens because containers don't have an init system by default, and the process you're running exits as soon as it finishes its work.

    Init systems solve this problem. They keep the container running, manage child processes, and handle signals properly. In this article, you'll learn why init systems matter, how they work, and which tools to use.

    The Problem: Containers Lack an Init System

    When you run a process in a container, it becomes PID 1—the first process in the container's process tree. PID 1 has special responsibilities in Linux:

    • Reaping zombie processes: When child processes terminate, their parent must call wait() to clean them up. If PID 1 doesn't do this, zombies accumulate.
    • Handling signals: Signals like SIGTERM and SIGINT need to be forwarded to child processes. PID 1 must implement signal handlers.
    • Running as root: By default, the first process runs as root, which is a security risk.
    • Starting multiple processes: You can't easily run multiple services in one container without an init system.

    Without an init system, these problems cause containers to exit unexpectedly or behave unpredictably.

    What Is an Init System?

    An init system is the first process started in a Linux system (PID 1). It's responsible for:

    1. Initializing the system: Mounting filesystems, setting up devices, starting services
    2. Managing processes: Starting, stopping, and supervising child processes
    3. Handling signals: Forwarding signals to child processes
    4. Reaping zombies: Cleaning up terminated child processes
    5. Logging: Managing system logs

    Traditional init systems like systemd, SysVinit, and Upstart run on physical servers and VMs. Containers need a simpler, more lightweight version.

    Why Containers Need a Minimal Init System

    Containers are ephemeral and resource-constrained. They need:

    • Minimal footprint: The init system should add as little overhead as possible
    • No dependencies: It should work in any Linux environment
    • Signal handling: Properly forward signals to child processes
    • Zombie reaping: Clean up terminated child processes
    • Process supervision: Keep the container running even if the main process exits

    tini: The Simplest Init System

    tini (Tiny Init) is a minimal init system designed specifically for containers. It's tiny (about 100KB), has no external dependencies, and handles the essential tasks.

    How tini Works

    tini runs as PID 1 and does three things:

    1. Reaps zombie processes: It calls wait() to clean up terminated child processes
    2. Forwards signals: It forwards SIGTERM, SIGINT, and other signals to child processes
    3. Starts the main process: It executes the command you specify

    Example: Using tini with Docker

    # Run a container with tini as the init system
    docker run --init -d myapp:latest
     
    # Or use the official tini image
    docker run -d --name myapp \
      -e DATABASE_URL=postgres://user:pass@host/db \
      -e REDIS_URL=redis://host:6379 \
      -e PORT=3000 \
      -p 3000:3000 \
      --init \
      myapp:latest

    The --init flag tells Docker to use tini as the init system.

    tini Signal Handling

    When you send a SIGTERM signal to a container, tini forwards it to the main process. This allows your application to shut down gracefully:

    # Send SIGTERM to the container
    docker kill --signal SIGTERM myapp
     
    # The main process receives SIGTERM and can clean up
    # tini then forwards SIGTERM to the main process

    tini Process Tree

    PID 1 (tini)
    └── PID 2 (your application)
        ├── PID 3 (child process A)
        └── PID 4 (child process B)

    tini manages all child processes, ensuring they're properly cleaned up.

    dumb-init: A More Robust Alternative

    dumb-init is another minimal init system designed for containers. It's more feature-rich than tini and handles edge cases better.

    Key Features of dumb-init

    • Signal handling: Forwards signals to child processes
    • Zombie reaping: Cleans up zombie processes
    • Process supervision: Keeps the container running
    • Environment preservation: Preserves environment variables
    • Non-root execution: Can run as a non-root user
    • Chroot support: Works in chroot environments

    Example: Using dumb-init

    # Install dumb-init in your Dockerfile
    FROM node:18-alpine
     
    # Install dumb-init
    RUN apk add --no-cache dumb-init
     
    # Start the application with dumb-init
    ENTRYPOINT ["dumb-init", "--"]
    CMD ["node", "server.js"]

    dumb-init vs tini

    Featuretinidumb-init
    Size~100KB~30KB
    Signal handlingBasicAdvanced
    Environment preservationYesYes
    Non-root executionNoYes
    Chroot supportNoYes
    Drop capabilitiesNoYes

    Both tools solve the same problem, but dumb-init has more features for complex scenarios.

    When to Use Each

    Use tini when:

    • You need the simplest possible init system
    • Your application doesn't have complex process requirements
    • You want minimal overhead
    • You're using Docker's --init flag

    Use dumb-init when:

    • You need advanced signal handling
    • You want to run as a non-root user
    • You need chroot support
    • You want to drop Linux capabilities
    • You're using Kubernetes or other orchestration systems

    Practical Example: Multi-Process Container

    Here's a complete example of running a Node.js application with a background worker using tini:

    # Dockerfile
    FROM node:18-alpine
     
    # Install tini
    RUN apk add --no-cache tini
     
    # Create app directory
    WORKDIR /app
     
    # Copy package files
    COPY package*.json ./
     
    # Install dependencies
    RUN npm ci --only=production
     
    # Copy application code
    COPY . .
     
    # Expose port
    EXPOSE 3000
     
    # Start the application with tini
    ENTRYPOINT ["/sbin/tini", "--"]
    CMD ["node", "server.js"]
    // server.js
    const http = require('http');
     
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Hello from Node.js!\n');
    });
     
    server.listen(3000, () => {
      console.log('Server running on port 3000');
     
      // Simulate a background worker
      setInterval(() => {
        console.log('Background worker running...');
      }, 5000);
    });

    When you run this container, tini ensures both the main server and the background worker are properly managed.

    Signal Handling Best Practices

    Graceful Shutdown

    Always implement graceful shutdown in your application:

    // server.js
    const http = require('http');
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Hello!\n');
    });
     
    let shutdownRequested = false;
     
    server.listen(3000, () => {
      console.log('Server running on port 3000');
    });
     
    // Handle SIGTERM from tini
    process.on('SIGTERM', () => {
      console.log('SIGTERM received, shutting down gracefully...');
      shutdownRequested = true;
     
      server.close(() => {
        console.log('HTTP server closed');
        process.exit(0);
      });
    });
     
    // Handle SIGINT (Ctrl+C)
    process.on('SIGINT', () => {
      console.log('SIGINT received, shutting down...');
      process.exit(0);
    });

    When you send docker kill --signal SIGTERM myapp, tini forwards SIGTERM to your process, which can shut down gracefully.

    Kubernetes Integration

    In Kubernetes, init systems are critical for proper signal handling:

    # deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: myapp
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: myapp
      template:
        metadata:
          labels:
            app: myapp
        spec:
          containers:
          - name: myapp
            image: myapp:latest
            ports:
            - containerPort: 3000
            lifecycle:
              preStop:
                exec:
                  command: ["/bin/sh", "-c", "sleep 5"]
            resources:
              limits:
                memory: "256Mi"
                cpu: "500m"
            livenessProbe:
              httpGet:
                path: /health
                port: 3000
              initialDelaySeconds: 30
              periodSeconds: 10
            readinessProbe:
              httpGet:
                path: /ready
                port: 3000
              initialDelaySeconds: 5
              periodSeconds: 5

    Without an init system, Kubernetes can't properly manage the container lifecycle.

    Common Issues and Solutions

    Issue 1: Container Exits Immediately

    Symptom: Container starts and exits right away.

    Cause: The main process exits, and no init system is running.

    Solution: Use an init system:

    docker run --init myapp:latest

    Issue 2: Zombie Processes

    Symptom: High zombie process count in the container.

    Cause: Child processes aren't being reaped.

    Solution: Use tini or dumb-init:

    FROM node:18-alpine
    RUN apk add --no-cache tini
    ENTRYPOINT ["/sbin/tini", "--"]
    CMD ["node", "server.js"]

    Issue 3: Signal Not Received

    Symptom: Container doesn't shut down when SIGTERM is sent.

    Cause: Signal handling isn't implemented in the application.

    Solution: Implement signal handlers:

    process.on('SIGTERM', () => {
      // Clean up and exit
      process.exit(0);
    });

    Security Considerations

    Running as Non-Root

    Both tini and dumb-init can run as non-root users:

    FROM node:18-alpine
     
    # Create a non-root user
    RUN addgroup -g 1001 -S nodejs && \
        adduser -S nodejs -u 1001
     
    # Switch to non-root user
    USER nodejs
     
    # Install tini
    RUN apk add --no-cache tini
     
    ENTRYPOINT ["/sbin/tini", "--"]
    CMD ["node", "server.js"]

    Dropping Capabilities

    dumb-init can drop Linux capabilities:

    ENTRYPOINT ["dumb-init", "--cap-drop=ALL", "--cap-add=NET_BIND_SERVICE"]

    This reduces the attack surface of your container.

    Performance Impact

    Both tini and dumb-init have minimal performance impact:

    • tini: ~100KB binary, negligible CPU/memory overhead
    • dumb-init: ~30KB binary, even lighter footprint

    For most applications, the overhead is insignificant compared to the benefits.

    Conclusion

    Init systems like tini and dumb-init are essential for containers. They handle zombie processes, forward signals, and keep containers running. Without them, your containers will behave unpredictably.

    Key takeaways:

    1. Containers need an init system to manage processes properly
    2. tini is the simplest option, perfect for basic use cases
    3. dumb-init offers more features for complex scenarios
    4. Always implement graceful shutdown in your applications
    5. Use init systems in Kubernetes and other orchestration systems

    If you're using Docker, the --init flag automatically adds tini. If you're using Kubernetes, consider using dumb-init for better signal handling and security.

    Platforms like ServerlessBase handle init system configuration automatically, so you can focus on your application code rather than container management details.

    Leave comment