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 terminatedKey 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: 10GiKafka
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: 9093Nginx
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-configUsing 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: 500MiLiveness 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: 2Complete 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-appSection 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/serverDebugging: 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 defunctReference: Environment Variables
Variable | Default | Purpose |
|---|---|---|
|
| Seconds to wait for app shutdown before SIGKILL |
Reference: Exit Codes
Code | Meaning |
|---|---|
| Application exited successfully |
| Application's exit code (preserved) |
| Application killed by signal N (e.g., 143 = 128+15 = SIGTERM) |
| cleanimg-init fatal error |
Getting Help
For issues, feature requests, or security concerns, refer to the project documentation or contact the maintainers.
