Skip to main content
CleanStart

Knowledge Hub

cleanimg-init: Memory-Safe PID 1 Process Manager

A complete guide to cleanimg-init, the Rust-based PID 1 process manager for distroless containers.

Section 1: What Is cleanimg-init

The Problem with Traditional Approaches

Running your application directly as PID 1 in a container creates critical signal handling issues. When a container receives SIGTERM (the standard shutdown signal), an unprepared application may ignore the signal entirely forcing Kubernetes to escalate to SIGKILL, leave zombie processes running (processes in a terminated state that haven't been reaped), fail to close file descriptors properly preventing graceful shutdown, or leak resources during the termination sequence.

Shells like /bin/sh and /bin/bash weren't designed for PID 1 and don't reap child processes correctly. They don't forward signals reliably and introduce unnecessary overhead. Every shell adds a layer of indirection and potential failure points.

Traditional init managers like tini and dumb-init are written in C. While widely used, C programs face several memory safety challenges including buffer overflow vulnerabilities in signal handlers, use-after-free bugs in process management, memory leaks in long-running PID 1 processes, undefined behavior in edge cases (signal races, zombie cleanup), and C's manual memory management can fail silently. A PID 1 process must run indefinitely without crashes. Memory safety violations in C can cause entire container infrastructure to destabilize.

Why cleanimg-init

cleanimg-init is a 1MB static Rust binary that serves as a production-grade PID 1 replacement. It provides memory safety through Rust plus borrow checker, configurable signal handling timeout (unlike tini's fixed 30s), guaranteed zombie reaping, 8 native commands, approximately 1MB static size, container startup time under 50ms, self-contained operation without external shell dependencies, and compatibility with distroless containers running in scratch. cleanimg-init eliminates the need for a shell while providing memory safety guarantees that C-based alternatives cannot offer.

The comparison to tini and dumb-init shows key differences. Memory safety is guaranteed in cleanimg-init through Rust but requires manual memory management in tini and dumb-init, both written in C. Signal handling is configurable with timeout in cleanimg-init but fixed at 30 seconds in tini and unavailable in dumb-init. Zombie reaping is guaranteed in cleanimg-init, yes in tini, and yes in dumb-init. Init builtins total 8 native commands in cleanimg-init but none in both tini and dumb-init which are shell-only. Binary size is 1MB static in cleanimg-init, 100KB static in tini, and 100KB static in dumb-init. Container startup time is under 50ms in cleanimg-init and tini but exceeds 200ms in shells. External shell dependencies are eliminated in cleanimg-init and present in both tini and dumb-init. Distroless compatibility works in cleanimg-init, yes in tini, yes in dumb-init, but not in shells.

Section 2: The 8 Native Builtins — Complete Reference

cleanimg-init includes 8 native commands that perform common initialization tasks without requiring a shell or external binaries. Each command is memory-safe and optimized for container startup.

1. cp — Copy Files and Directories

The cp command recursively copies files and directories, with optional ownership and permission preservation. The syntax is cp [flags] <source> <destination> with flags including -r or --recursive to copy directories recursively, -p or --preserve to preserve file permissions and timestamps, and -d or --dereference to follow symbolic links (copy target, not link).

Example 1: Copy application config file

FROM distroless.dev/cc-debian12 COPY --from=builder /app/my-app /app/my-appCOPY ./config.yaml /tmp/config.template ENTRYPOINT ["/sbin/cleanimg-init", \  "cp", "/tmp/config.template", "/etc/myapp/config.yaml", \  "exec", "/app/my-app"]

Example 2: Copy entire directory with permissions preserved

FROM distroless.dev/cc-debian12 COPY --from=builder /app/data /app/data ENTRYPOINT ["/sbin/cleanimg-init", \  "cp", "-r", "-p", "/app/data/templates", "/var/lib/app/templates", \  "exec", "/app/my-app"]

Example 3: Copy certificate files with dereferencing

FROM distroless.dev/cc-debian12 COPY ./certs /tmp/certs ENTRYPOINT ["/sbin/cleanimg-init", \  "cp", "-d", "/tmp/certs/server.crt", "/etc/ssl/certs/server.crt", \  "mkdir", "-p", "/etc/ssl/certs", \  "exec", "/app/web-server"]

2. mkdir — Create Directories with Permissions

The mkdir command creates directories with specified permissions and ownership (when running as root). The syntax is mkdir [flags] <path> [<path> ...] with flags -p or --parents to create parent directories as needed, and -m or --mode=MODE to set directory permissions (octal, e.g., 0755).

Example 1: Create working directories with specific permissions

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app/data", \  "mkdir", "-p", "-m", "0700", "/var/lib/app/secrets", \  "mkdir", "-p", "-m", "1777", "/tmp/app-tmp", \  "exec", "/app/my-app"]

Example 2: Create Redis persistence directory

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/data", \  "mkdir", "-p", "-m", "0755", "/logs", \  "exec", "/redis-server", "--dir", "/data", "--logfile", "/logs/redis.log"]

Example 3: Create multi-level application structure

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/app/cache", \  "mkdir", "-p", "-m", "0755", "/app/logs", \  "mkdir", "-p", "-m", "0700", "/app/private", \  "chmod", "0755", "/app", \  "exec", "/app/server"]

3. mv — Move and Rename Files

The mv command moves or renames files and directories, optionally overwriting existing targets. The syntax is mv [flags] <source> <destination> with the flag -f or --force to overwrite destination without prompting.

Example 1: Rename configuration file

FROM distroless.dev/cc-debian12 COPY ./config.prod.yaml /tmp/config.yaml ENTRYPOINT ["/sbin/cleanimg-init", \  "mv", "-f", "/tmp/config.yaml", "/etc/myapp/config.yaml", \  "exec", "/app/my-app"]

Example 2: Move PID file to expected location

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/run/app", \  "mv", "-f", "/tmp/app.pid.template", "/var/run/app/app.pid", \  "exec", "/app/daemon"]

Example 3: Reorganize log directories at startup

FROM distroless.dev/cc-debian12 COPY --from=builder /app/logs /tmp/logs ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/log/app", \  "mv", "-f", "/tmp/logs/*", "/var/log/app/", \  "exec", "/app/worker"]

4. rm — Remove Files and Directories

The rm command removes files and directories with optional recursive deletion and force mode. The syntax is rm [flags] <path> [<path> ...] with flags -r or --recursive to remove directories recursively, and -f or --force to force removal without prompting.

Example 1: Clean stale lock files

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "rm", "-f", "/var/lib/app/lock", \  "rm", "-f", "/tmp/app.sock", \  "exec", "/app/server"]

Example 2: Remove outdated cache directory

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "rm", "-r", "-f", "/var/cache/old-data", \  "mkdir", "-p", "-m", "0755", "/var/cache/app", \  "exec", "/app/worker"]

Example 3: Clean temporary files before startup

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "rm", "-f", "/tmp/app-*.log", \  "rm", "-r", "-f", "/tmp/app-cache", \  "exec", "/app/service"]

5. chmod — Change File Permissions

The chmod command modifies file and directory permissions in octal or symbolic notation. The syntax is chmod [flags] <mode> <path> [<path> ...] with the flag -r or --recursive to apply permissions recursively.

Example 1: Set restrictive permissions on private keys

FROM distroless.dev/cc-debian12 COPY ./private.key /tmp/private.key ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0700", "/etc/app", \  "cp", "/tmp/private.key", "/etc/app/private.key", \  "chmod", "0600", "/etc/app/private.key", \  "exec", "/app/server"]

Example 2: Make scripts executable

FROM distroless.dev/cc-debian12 COPY ./entrypoint.sh /app/entrypoint.sh ENTRYPOINT ["/sbin/cleanimg-init", \  "chmod", "0755", "/app/entrypoint.sh", \  "exec", "/app/my-app"]

Example 3: Fix directory permissions recursively

FROM distroless.dev/cc-debian12 COPY --from=builder /app /app ENTRYPOINT ["/sbin/cleanimg-init", \  "chmod", "-r", "0755", "/app", \  "chmod", "0644", "/app/config.yaml", \  "exec", "/app/server"]

6. ln — Create Symbolic Links

The ln command creates symbolic (soft) links to files and directories, with optional force overwrite. The syntax is ln [flags] <target> <link_name> with flags -s or --symbolic to create symbolic link (required, always used), and -f or --force to force creation even if link exists.

Example 1: Create compatibility symlinks

FROM distroless.dev/cc-debian12 COPY ./app /app/binary ENTRYPOINT ["/sbin/cleanimg-init", \  "ln", "-s", "-f", "/app/binary", "/usr/local/bin/myapp", \  "exec", "/app/binary"]

Example 2: Link config files to standard locations

FROM distroless.dev/cc-debian12 COPY ./config.yaml /app/config.yaml ENTRYPOINT ["/sbin/cleanimg-init", \  "ln", "-s", "-f", "/app/config.yaml", "/etc/app/config.yaml", \  "exec", "/app/server"]

Example 3: Create versioned library links

FROM distroless.dev/cc-debian12 COPY --from=builder /usr/lib/libapp.so.1.0 /usr/lib/ ENTRYPOINT ["/sbin/cleanimg-init", \  "ln", "-s", "-f", "/usr/lib/libapp.so.1.0", "/usr/lib/libapp.so", \  "ln", "-s", "-f", "/usr/lib/libapp.so.1.0", "/usr/lib/libapp.so.1", \  "exec", "/app/binary"]

7. touch — Create Empty Files with Timestamps

The touch command creates empty files or updates file modification times. The syntax is touch [flags] <path> [<path> ...] with flags -a or --atime to change only access time, -m or --mtime to change only modification time, and -t to set specific timestamp (format: [[CC]YY]MMDDhhmm[.ss]).

Example 1: Create placeholder files

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app", \  "touch", "/var/lib/app/initialized", \  "touch", "/var/lib/app/startup.log", \  "exec", "/app/server"]

Example 2: Initialize empty database placeholder

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/data", \  "touch", "/data/db.sqlite", \  "chmod", "0600", "/data/db.sqlite", \  "exec", "/app/app-with-db"]

Example 3: Create marker file for health checks

FROM distroless.dev/cc-debian12 ENTRYPOINT ["/sbin/cleanimg-init", \  "touch", "/tmp/ready", \  "exec", "/app/worker"]

8. cat — Concatenate and Display Files

The cat command reads file contents and writes to stdout or another file, useful for initializing config files from templates. The syntax is cat <source_file> [<source_file> ...].

Example 1: Output configuration for validation

FROM distroless.dev/cc-debian12 COPY ./config.yaml /tmp/config.yaml ENTRYPOINT ["/sbin/cleanimg-init", \  "cat", "/tmp/config.yaml", \  "mkdir", "-p", "-m", "0755", "/etc/app", \  "cp", "/tmp/config.yaml", "/etc/app/config.yaml", \  "exec", "/app/server"]

Example 2: Merge multiple configuration files

FROM distroless.dev/cc-debian12 COPY ./defaults.conf /tmp/defaults.confCOPY ./overrides.conf /tmp/overrides.conf ENTRYPOINT ["/sbin/cleanimg-init", \  "cat", "/tmp/defaults.conf", \  "cat", "/tmp/overrides.conf", \  "exec", "/app/server"]

Example 3: Verify environment-specific config

FROM distroless.dev/cc-debian12 COPY ./config.prod.yaml /tmp/config.yaml ENTRYPOINT ["/sbin/cleanimg-init", \  "cat", "/tmp/config.yaml", \  "cp", "/tmp/config.yaml", "/etc/app/config.yaml", \  "exec", "/app/server"]

Execution Modes

cleanimg-init supports two execution paradigms that serve different purposes in container initialization.

init-then-exec Mode (Container ENTRYPOINT)

This mode runs initialization builtins, then executes the application as PID 1.

ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app/data", \  "chmod", "0755", "/app", \  "exec", "/app/server", "--port", "8080"]

Behavior: All builtins run sequentially. On first failure, the process stops and exits with error. The final command executes as PID 1 replacement. SIGTERM is forwarded to the application. Zombies are reaped while the application runs.

run-and-exit Mode (K8s initContainer)

This mode runs initialization builtins, then exits cleanly without executing another command.

initContainers:- name: init  image: distroless.dev/cc-debian12  entrypoint: ["/sbin/cleanimg-init"]  args:  - "mkdir"  - "-p"  - "-m"  - "0755"  - "/data"  - "chmod"  - "0755"  - "/data"

Behavior: All builtins run sequentially. The process exits with status code (0 on success, non-zero on error). No command is executed. The container terminates normally. The main container then runs the application.

Section 3: Signal Handling and Zombie Reaping

How SIGTERM Forwarding Works

When Kubernetes terminates a Pod, it sends SIGTERM to the main process (PID 1). cleanimg-init intercepts this signal and forwards it to your application, allowing graceful shutdown.

Signal handling sequence: The container receives SIGTERM from the orchestrator (Kubernetes, Docker, etc.). cleanimg-init catches SIGTERM in its signal handler. The signal is forwarded to the application (child process). The application receives SIGTERM and begins graceful shutdown. cleanimg-init waits for application exit (default timeout: 30 seconds). The application exits normally or the timeout expires. cleanimg-init reaps the child and exits with the same status code.

Configurable Timeout

The grace period for application shutdown is configurable via environment variable:

ENV CLEANIMG_SHUTDOWN_TIMEOUT=60 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/data", \  "exec", "/app/server"]

Default timeout: 30 seconds

Timeout behavior: After SIGTERM is received, the application has CLEANIMG_SHUTDOWN_TIMEOUT seconds to exit. If the timeout expires, cleanimg-init sends SIGKILL to force termination. SIGKILL cannot be caught or ignored (guaranteed process death). The exit code reflects the application's status.

Zombie Process Reaping

Zombie processes occur when a child process exits but its parent hasn't called wait() to collect its exit status. These "dead" processes consume PID slots and cannot be garbage collected.

How cleanimg-init prevents zombies:

// Simplified logic:signal(SIGCHLD, reap_handler);  // Register zombie reaper fn reap_handler() {    loop {        match waitpid(-1, WNOHANG) {  // Non-blocking wait            Ok((pid, status)) => {                eprintln!("Reaped child {}: {:?}", pid, status);            }            Err(_) => break,  // No more children to reap        }    }}

Result: Every child process that exits is immediately reaped, preventing zombie accumulation.

Exit Code Preservation

cleanimg-init ensures your application's exit code is preserved:

ENTRYPOINT ["/sbin/cleanimg-init", \  "exec", "/app/server"]

Exit code mapping: Exit code 0 means application exited normally. Exit codes 1-127 represent the application's exit code (preserved exactly). Exit code 128+N indicates application terminated by signal N (e.g., 128+15 = 143 for SIGTERM). Exit code 255 means cleanimg-init encountered a fatal error (rare).

Graceful Shutdown Sequence Diagram

                Container Termination                        |                        v                  Container receives SIGTERM                        |                        v                cleanimg-init signal handler triggered                        |                        v                Forward SIGTERM to application                        |                        v            +-----------+---------------+            |                           |            v                           v     Application handles SIGTERM   Start countdown            |                           |            v                           v      Clean up resources          (30s default)            |                           |            v                           v    Application exits normally    Timeout expires?            |                           |            +--------+---------+--------+                     |                     v        cleanimg-init reaps child                     |                     v      cleanimg-init exits with same         exit code as application                     |                     v            Container terminated

Key properties: Signal forwarding is synchronous (application sees SIGTERM immediately). Timeout is enforced by cleanimg-init (not orchestrator). Exit code preservation ensures monitoring tools see correct status. Zombie reaping prevents resource leaks during entire container lifetime.

Section 4: Per-Application Examples

PostgreSQL

Problem: PostgreSQL requires initialization scripts to set up data directory ownership, permissions, and base tables. The official docker-entrypoint.sh is a complex shell script requiring /bin/sh.

Solution: Use cleanimg-init to create directories, set permissions, then exec postgres.

FROM postgres:16-bookworm # Install cleanimg-initCOPY --from=ghcr.io/example/cleanimg-init:latest /sbin/cleanimg-init /sbin/cleanimg-init RUN chmod +x /sbin/cleanimg-init && \    rm -rf /var/lib/postgresql ENV PGDATA=/var/lib/postgresql/dataENV CLEANIMG_SHUTDOWN_TIMEOUT=60 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0700", "/var/lib/postgresql", \  "mkdir", "-p", "-m", "0700", "/var/lib/postgresql/data", \  "mkdir", "-p", "-m", "0755", "/var/run/postgresql", \  "chmod", "0755", "/var/run/postgresql", \  "exec", "postgres", \    "-c", "shared_buffers=256MB", \    "-c", "effective_cache_size=1GB"]

Kubernetes manifest:

apiVersion: v1kind: Podmetadata:  name: postgres-podspec:  containers:  - name: postgres    image: my-registry.io/postgres:16-distroless    ports:    - containerPort: 5432    env:    - name: POSTGRES_PASSWORD      valueFrom:        secretKeyRef:          name: pg-secret          key: password    - name: CLEANIMG_SHUTDOWN_TIMEOUT      value: "60"    livenessProbe:      exec:        command:        - /usr/lib/postgresql/16/bin/pg_isready        - -U        - postgres      initialDelaySeconds: 30      periodSeconds: 10    volumeMounts:    - name: data      mountPath: /var/lib/postgresql  volumes:  - name: data    emptyDir: {}

Redis

Problem: Redis needs persistent storage directory, optional sentinel configuration, and socket permissions. Standard approach uses shell startup scripts.

Solution: cleanimg-init creates directories, optional config generation, then execs redis-server.

FROM redis:7-alpine # Install cleanimg-initCOPY --from=ghcr.io/example/cleanimg-init:latest /sbin/cleanimg-init /sbin/cleanimg-init # Copy optional default configCOPY redis.conf /tmp/redis.conf.default ENV CLEANIMG_SHUTDOWN_TIMEOUT=30 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/data", \  "mkdir", "-p", "-m", "0755", "/var/run/redis", \  "mkdir", "-p", "-m", "0755", "/var/log/redis", \  "cp", "/tmp/redis.conf.default", "/etc/redis/redis.conf", \  "chmod", "0644", "/etc/redis/redis.conf", \  "exec", "redis-server", \    "/etc/redis/redis.conf", \    "--dir", "/data", \    "--appendonly", "yes", \    "--appenddir", "/data"]

Kubernetes manifest with persistence:

apiVersion: apps/v1kind: Deploymentmetadata:  name: redisspec:  replicas: 1  selector:    matchLabels:      app: redis  template:    metadata:      labels:        app: redis    spec:      containers:      - name: redis        image: my-registry.io/redis:7-distroless        ports:        - containerPort: 6379        env:        - name: CLEANIMG_SHUTDOWN_TIMEOUT          value: "30"        resources:          requests:            memory: "512Mi"            cpu: "500m"          limits:            memory: "1Gi"            cpu: "1000m"        livenessProbe:          exec:            command:            - redis-cli            - ping          initialDelaySeconds: 5          periodSeconds: 10        readinessProbe:          exec:            command:            - redis-cli            - ping          initialDelaySeconds: 2          periodSeconds: 5        volumeMounts:        - name: data          mountPath: /data        - name: config          mountPath: /etc/redis      volumes:      - name: data        persistentVolumeClaim:          claimName: redis-pvc      - name: config        configMap:          name: redis-config---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: redis-pvcspec:  accessModes:  - ReadWriteOnce  resources:    requests:      storage: 10Gi

Kafka

Problem: Kafka in KRaft mode (no Zookeeper) requires specific directory structure, proper cluster ID formatting, and log directory initialization. Manual setup is error-prone.

Solution: cleanimg-init creates KRaft-compatible structure, then execs kafka.

FROM confluentinc/cp-kafka:7.5 # Install cleanimg-initCOPY --from=ghcr.io/example/cleanimg-init:latest /sbin/cleanimg-init /sbin/cleanimg-init RUN chmod +x /sbin/cleanimg-init ENV KAFKA_CLUSTER_ID=MkQkTr7dT6KUUVVZewKAmgENV KAFKA_NODE_ID=1ENV KAFKA_PROCESS_ROLES=broker,controllerENV KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093ENV KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092ENV KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka:9093ENV CLEANIMG_SHUTDOWN_TIMEOUT=30 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/kafka/data", \  "mkdir", "-p", "-m", "0755", "/var/lib/kafka/secrets", \  "mkdir", "-p", "-m", "0755", "/var/log/kafka", \  "touch", "/var/lib/kafka/data/.kafka_initialized", \  "exec", "/usr/local/bin/kafka-server-start.sh", \    "/etc/kafka/server.properties"]

Kubernetes StatefulSet:

apiVersion: apps/v1kind: StatefulSetmetadata:  name: kafkaspec:  serviceName: kafka  replicas: 3  selector:    matchLabels:      app: kafka  template:    metadata:      labels:        app: kafka    spec:      containers:      - name: kafka        image: my-registry.io/kafka:7.5-distroless        ports:        - containerPort: 9092          name: plaintext        - containerPort: 9093          name: controller        env:        - name: KAFKA_CLUSTER_ID          value: MkQkTr7dT6KUUVVZewKAmg        - name: KAFKA_NODE_ID          valueFrom:            fieldRef:              fieldPath: metadata.ordinalSubdomainName        - name: KAFKA_ADVERTISED_LISTENERS          value: PLAINTEXT://$(HOSTNAME).kafka:9092        - name: CLEANIMG_SHUTDOWN_TIMEOUT          value: "30"        resources:          requests:            memory: "1Gi"            cpu: "1000m"          limits:            memory: "2Gi"            cpu: "2000m"        livenessProbe:          tcpSocket:            port: 9092          initialDelaySeconds: 30          periodSeconds: 10        readinessProbe:          exec:            command:            - kafka-broker-api-versions.sh            - --bootstrap-server            - localhost:9092          initialDelaySeconds: 10          periodSeconds: 5        volumeMounts:        - name: data          mountPath: /var/lib/kafka/data  volumeClaimTemplates:  - metadata:      name: data    spec:      accessModes: [ "ReadWriteOnce" ]      resources:        requests:          storage: 50Gi---apiVersion: v1kind: Servicemetadata:  name: kafkaspec:  clusterIP: None  selector:    app: kafka  ports:  - port: 9092    targetPort: 9092  - port: 9093    targetPort: 9093

Nginx

Problem: Nginx requires cache directories, temporary directories, and proper ownership before startup. The nginx user must own these directories for security.

Solution: cleanimg-init creates complete directory structure with correct permissions.

FROM nginx:1.25-alpine # Install cleanimg-initCOPY --from=ghcr.io/example/cleanimg-init:latest /sbin/cleanimg-init /sbin/cleanimg-init COPY nginx.conf /etc/nginx/nginx.conf RUN chmod +x /sbin/cleanimg-init && \    addgroup -g 101 -S nginx && \    adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx ENV CLEANIMG_SHUTDOWN_TIMEOUT=15 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/cache/nginx", \  "mkdir", "-p", "-m", "0755", "/var/cache/nginx/client_temp", \  "mkdir", "-p", "-m", "0755", "/var/cache/nginx/proxy_temp", \  "mkdir", "-p", "-m", "0755", "/var/cache/nginx/uwsgi_temp", \  "mkdir", "-p", "-m", "0755", "/var/run/nginx", \  "mkdir", "-p", "-m", "0755", "/var/log/nginx", \  "chmod", "-r", "0755", "/var/cache/nginx", \  "chmod", "0644", "/var/cache/nginx/client_temp", \  "exec", "nginx", "-g", "daemon off;"]

Custom Python Application

Problem: Python applications often need temporary directories, log directories, and cache locations created before startup. Managing this with shell adds complexity and overhead.

Solution: cleanimg-init creates standard application directory structure, then execs Python.

FROM python:3.12-slim # Install cleanimg-initCOPY --from=ghcr.io/example/cleanimg-init:latest /sbin/cleanimg-init /sbin/cleanimg-init WORKDIR /app COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt COPY . . ENV PYTHONUNBUFFERED=1ENV APP_LOGLEVEL=INFOENV CLEANIMG_SHUTDOWN_TIMEOUT=30 ENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app", \  "mkdir", "-p", "-m", "0755", "/var/log/app", \  "mkdir", "-p", "-m", "0755", "/tmp/app-cache", \  "touch", "/var/lib/app/.initialized", \  "exec", "python", "-u", "app.py"]

Example application with environment-based config:

# app.pyimport osimport signalimport sysfrom pathlib import Pathimport logging # Setup logginglog_dir = Path("/var/log/app")logging.basicConfig(    filename=log_dir / "app.log",    level=os.getenv("APP_LOGLEVEL", "INFO"),    format="%(asctime)s - %(levelname)s - %(message)s")logger = logging.getLogger(__name__) class AppServer:    def __init__(self):        self.running = True        signal.signal(signal.SIGTERM, self.handle_sigterm)     def handle_sigterm(self, signum, frame):        logger.info("SIGTERM received, initiating graceful shutdown")        self.running = False     def run(self):        logger.info("Application started")        try:            while self.running:                # Application logic                pass        except KeyboardInterrupt:            logger.info("Interrupted")        finally:            logger.info("Application shutdown complete") if __name__ == "__main__":    app = AppServer()    app.run()

Custom Go Application

Problem: Simplest case—Go static binary doesn't need runtime or shell, but PID 1 still needs proper signal forwarding and zombie reaping.

Solution: Use cleanimg-init in pure exec mode for signal handling.

FROM ghcr.io/example/cleanimg-init:latest as initFROM distroless/cc-debian12 COPY --from=builder /app/server /app/serverCOPY --from=init /sbin/cleanimg-init /sbin/cleanimg-init ENV CLEANIMG_SHUTDOWN_TIMEOUT=30 ENTRYPOINT ["/sbin/cleanimg-init", \  "exec", "/app/server", "-port", "8080"]

Example Go application with graceful shutdown:

package main import (    "context"    "log"    "net/http"    "os"    "os/signal"    "syscall"    "time") func main() {    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {        w.WriteHeader(http.StatusOK)    })     server := &http.Server{        Addr:    ":8080",        Handler: http.DefaultServeMux,    }     // Start server in goroutine    go func() {        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {            log.Fatalf("Server error: %v", err)        }    }()     // Wait for SIGTERM    sigterm := make(chan os.Signal, 1)    signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT)    <-sigterm     log.Println("SIGTERM received, shutting down...")     // Graceful shutdown with timeout    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)    defer cancel()     if err := server.Shutdown(ctx); err != nil {        log.Printf("Shutdown error: %v", err)        os.Exit(1)    }     log.Println("Server shutdown complete")}

Section 5: Kubernetes Integration

Using cleanimg-init as initContainer (run-and-exit mode)

For initialization tasks that must complete before the main application starts, cleanimg-init can run as an initContainer. Initialization happens once before application starts. Failures in init prevent application from starting. Clear separation of concerns is maintained. Easier to debug init issues independently.

apiVersion: v1kind: Podmetadata:  name: app-with-initspec:  initContainers:  - name: init-directories    image: my-registry.io/cleanimg-init:latest    args:    - "mkdir"    - "-p"    - "-m"    - "0755"    - "/data/app"    - "mkdir"    - "-p"    - "-m"    - "0700"    - "/data/secrets"    - "chmod"    - "0755"    - "/data"    volumeMounts:    - name: data      mountPath: /data   - name: init-config    image: my-registry.io/cleanimg-init:latest    args:    - "cp"    - "/config/defaults.yaml"    - "/data/app/config.yaml"    - "chmod"    - "0644"    - "/data/app/config.yaml"    volumeMounts:    - name: data      mountPath: /data    - name: config      mountPath: /config   containers:  - name: app    image: my-registry.io/my-app:latest    volumeMounts:    - name: data      mountPath: /data   volumes:  - name: data    emptyDir: {}  - name: config    configMap:      name: app-config

Using cleanimg-init as ENTRYPOINT (init-then-exec mode)

For runtime setup that should happen every container start, cleanimg-init can serve as the ENTRYPOINT:

apiVersion: apps/v1kind: Deploymentmetadata:  name: stateful-appspec:  replicas: 3  selector:    matchLabels:      app: stateful-app  template:    metadata:      labels:        app: stateful-app    spec:      containers:      - name: app        image: my-registry.io/stateful-app:latest        imagePullPolicy: IfNotPresent        ports:        - name: http          containerPort: 8080          protocol: TCP        - name: metrics          containerPort: 9090          protocol: TCP         env:        - name: CLEANIMG_SHUTDOWN_TIMEOUT          value: "30"        - name: LOG_LEVEL          value: "info"        - name: DATA_DIR          value: "/data"         resources:          requests:            memory: "512Mi"            cpu: "500m"          limits:            memory: "1Gi"            cpu: "1000m"         livenessProbe:          httpGet:            path: /health/live            port: http          initialDelaySeconds: 30          periodSeconds: 10          timeoutSeconds: 5         readinessProbe:          httpGet:            path: /health/ready            port: http          initialDelaySeconds: 10          periodSeconds: 5          timeoutSeconds: 3         lifecycle:          preStop:            exec:              command:              - /bin/sh              - -c              - sleep 5  # Allow time for load balancer to deregister         volumeMounts:        - name: data          mountPath: /data        - name: cache          mountPath: /tmp/cache        - name: logs          mountPath: /var/log/app       terminationGracePeriodSeconds: 45       volumes:      - name: data        persistentVolumeClaim:          claimName: app-data      - name: cache        emptyDir:          sizeLimit: 1Gi      - name: logs        emptyDir:          sizeLimit: 500Mi

Liveness and Readiness Probes Without Shell

cleanimg-init eliminates the need for shell-based health checks through TCP checks (simplest, no shell needed) and HTTP checks (no shell needed). Startup probes are useful for slow-starting apps:

apiVersion: v1kind: Podmetadata:  name: app-with-probesspec:  containers:  - name: app    image: my-registry.io/app:latest     # TCP check (simplest, no shell needed)    livenessProbe:      tcpSocket:        port: 8080      initialDelaySeconds: 10      periodSeconds: 10     # HTTP check (no shell needed)    readinessProbe:      httpGet:        path: /ready        port: 8080        scheme: HTTP      initialDelaySeconds: 5      periodSeconds: 5     # Startup probe for slow-starting apps    startupProbe:      httpGet:        path: /health        port: 8080      failureThreshold: 30      periodSeconds: 2

Complete Deployment Manifest with cleanimg-init

A production-ready example combining all elements demonstrates proper integration:

apiVersion: v1kind: Namespacemetadata:  name: production ---apiVersion: v1kind: ConfigMapmetadata:  name: app-config  namespace: productiondata:  app.yaml: |    server:      port: 8080      graceful_shutdown: 30s    logging:      level: info      format: json    database:      pool_size: 10 ---apiVersion: apps/v1kind: Deploymentmetadata:  name: production-app  namespace: productionspec:  replicas: 3  strategy:    type: RollingUpdate    rollingUpdate:      maxSurge: 1      maxUnavailable: 0   selector:    matchLabels:      app: production-app   template:    metadata:      labels:        app: production-app        version: v1.0.0     spec:      securityContext:        runAsNonRoot: true        runAsUser: 65534        fsGroup: 65534       initContainers:      - name: init-db        image: my-registry.io/cleanimg-init:latest        args:        - "mkdir"        - "-p"        - "-m"        - "0755"        - "/data"        volumeMounts:        - name: data          mountPath: /data       containers:      - name: app        image: my-registry.io/production-app:v1.0.0        imagePullPolicy: IfNotPresent         securityContext:          allowPrivilegeEscalation: false          readOnlyRootFilesystem: true          capabilities:            drop:            - ALL         ports:        - name: http          containerPort: 8080        - name: metrics          containerPort: 9090         env:        - name: CLEANIMG_SHUTDOWN_TIMEOUT          value: "30"        - name: LOG_LEVEL          value: "info"        - name: ENVIRONMENT          value: "production"        - name: CONFIG_PATH          value: "/etc/app/config.yaml"         resources:          requests:            memory: "512Mi"            cpu: "500m"          limits:            memory: "1Gi"            cpu: "1000m"         livenessProbe:          httpGet:            path: /health/live            port: http          initialDelaySeconds: 30          periodSeconds: 10          timeoutSeconds: 5          failureThreshold: 3         readinessProbe:          httpGet:            path: /health/ready            port: http          initialDelaySeconds: 5          periodSeconds: 5          timeoutSeconds: 3          failureThreshold: 2         lifecycle:          preStop:            exec:              command:              - sleep              - "5"         volumeMounts:        - name: config          mountPath: /etc/app          readOnly: true        - name: data          mountPath: /data        - name: tmp          mountPath: /tmp        - name: cache          mountPath: /var/cache       affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - weight: 100            podAffinityTerm:              labelSelector:                matchExpressions:                - key: app                  operator: In                  values:                  - production-app              topologyKey: kubernetes.io/hostname       terminationGracePeriodSeconds: 45       volumes:      - name: config        configMap:          name: app-config      - name: data        persistentVolumeClaim:          claimName: app-data      - name: tmp        emptyDir:          sizeLimit: 1Gi      - name: cache        emptyDir:          sizeLimit: 500Mi ---apiVersion: v1kind: Servicemetadata:  name: production-app  namespace: productionspec:  type: ClusterIP  selector:    app: production-app  ports:  - name: http    port: 80    targetPort: http  - name: metrics    port: 9090    targetPort: metrics ---apiVersion: policy/v1kind: PodDisruptionBudgetmetadata:  name: production-app  namespace: productionspec:  minAvailable: 2  selector:    matchLabels:      app: production-app

Section 6: Troubleshooting

Container Keeps Restarting — Signal Handling Misconfiguration

Symptoms: Container terminates immediately after startup. No error logs are visible. kubectl logs shows nothing or partial output. Restart count increases rapidly.

Root causes: The exec command might not be found (binary path is incorrect or doesn't exist), the exec command might exit immediately (application crashed on startup), or SIGTERM might not be forwarded (timeout expires, SIGKILL forces restart).

Solutions:

# WRONG: Typo in executable pathENTRYPOINT ["/sbin/cleanimg-init", "exec", "/app/serer"] # CORRECT: Verify path exists in buildENTRYPOINT ["/sbin/cleanimg-init", \  "exec", "/app/server"] # Test in separate RUN commandRUN ls -la /app/server

Debugging: Check actual container error with kubectl logs <pod-name>. Inspect previous container logs with kubectl logs <pod-name> --previous. Check pod events with kubectl describe pod <pod-name>. Test locally with docker: docker run --rm my-image /app/server --help.

Permission Denied — chmod/chown Sequence Issues

Symptoms: "Permission denied" errors appear in application logs. File operations fail with EACCES. Configuration files cannot be read. Log files cannot be written.

Root causes: Wrong permissions on parent directory preventing child access, permissions set after copy (COPY command happens during build, chmod happens at runtime), or running as non-root (file is owned by root, app runs as unprivileged user).

Solutions:

# WRONG: chmod after exec happens too lateENTRYPOINT ["/sbin/cleanimg-init", \  "exec", "/app/server"] # CORRECT: chmod before execENTRYPOINT ["/sbin/cleanimg-init", \  "chmod", "0755", "/app", \  "chmod", "0644", "/app/config.yaml", \  "exec", "/app/server"] # CORRECT: Set permissions during buildRUN chmod 0755 /app && chmod 0644 /app/config.yamlENTRYPOINT ["/sbin/cleanimg-init", "exec", "/app/server"] # CORRECT: Create directory with correct permissionsENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app", \  "chmod", "0755", "/var/lib/app", \  "exec", "/app/server"]

Directory Not Found — mkdir Ordering

Symptoms: "No such file or directory" occurs during cp or touch operations. Parent directory is missing when child is created. Configuration files cannot be copied to target location.

Root causes: Parent directory doesn't exist, creating /a/b/c when /a/b is missing. Child might be created before parent. Typos in paths cause case-sensitive filesystem issues.

Solutions:

# WRONG: Parent doesn't existENTRYPOINT ["/sbin/cleanimg-init", \  "touch", "/var/lib/app/initialized"] # CORRECT: Use -p flag to create parentsENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var/lib/app", \  "touch", "/var/lib/app/initialized"] # CORRECT: Explicit parent creationENTRYPOINT ["/sbin/cleanimg-init", \  "mkdir", "-p", "-m", "0755", "/var", \  "mkdir", "-p", "-m", "0755", "/var/lib", \  "mkdir", "-p", "-m", "0755", "/var/lib/app", \  "touch", "/var/lib/app/initialized"] # CORRECT: Verify paths before copyRUN mkdir -p /var/lib/appENTRYPOINT ["/sbin/cleanimg-init", \  "cp", "/config.yaml", "/var/lib/app/config.yaml"]

Zombie Processes Accumulating — PID 1 Not Reaping

Symptoms: ps aux shows <defunct> processes. PID exhaustion errors appear (too many processes). Process table fills up over time. Application subprocess termination is not logged.

Root causes: Not using cleanimg-init as PID 1 (running shell instead), application spawning children without waiting (children become zombies), or resource limits hit (no PID slots available for new processes).

Solutions:

# WRONG: Shell as PID 1 (doesn't reap zombies)ENTRYPOINT /bin/sh -c "/app/server" # CORRECT: cleanimg-init as PID 1ENTRYPOINT ["/sbin/cleanimg-init", "exec", "/app/server"] # CORRECT: Application handles child processes// Go example with proper wait()cmd := exec.Command("child-process")cmd.Run()  // Wait for child to complete # Check for zombies (debugging)kubectl exec <pod> -- ps aux | grep defunct

Reference: Environment Variables

Variable

Default

Purpose

CLEANIMG_SHUTDOWN_TIMEOUT

30

Seconds to wait for app shutdown before SIGKILL

Reference: Exit Codes

Code

Meaning

0

Application exited successfully

1-127

Application's exit code (preserved)

128+N

Application killed by signal N (e.g., 143 = 128+15 = SIGTERM)

255

cleanimg-init fatal error

Getting Help

For issues, feature requests, or security concerns, refer to the project documentation or contact the maintainers.