Resource Repository / Host Access Link
Primary Forge Self-Hosted Forgejo v2 mentalnet.xyz/forgejo-v2/.../tuxdock
Public Mirror GitHub github.com/MARKMENTAL/tuxdock
Architecture Write-Up Part I Retrospective pro.mentalnet.xyz/tuxdockwriteup
Architecture Write-Up Part II Retrospective pro.mentalnet.xyz/tuxdockwriteup-pt2

Part I: The Premise — Calling the Bluff on sleep infinity

When I published Part I of the Tux-Dock retrospective, the core mechanism for keeping stateful, persistent containers alive was simple: anchor the container with sleep infinity as PID 1, drop into an interactive shell, install developer toolchains, and let the environment persist like a micro-VM.

It worked for interactive sessions, but anyone with deep Linux systems programming experience knows the dirty mechanical truth: sleep infinity makes an awful PID 1.

The Mechanical Reality of the Zombie Apocalypse

In a Linux PID namespace, PID 1 inherits a mandatory kernel contract: adopt all orphaned child processes and reap them upon termination.

When running long-lived development sandboxes—compiling software with gcc, running package managers (apt, apk), spawning background shells, or triggering detached maintenance scripts—child processes inevitably fork and exit before their sub-children do. The Linux kernel reparents these abandoned processes directly to PID 1.

  • The Catatonic Parent: sleep infinity sits in a blocking nanosleep() loop. It registers zero signal handlers for SIGCHLD and never calls wait() or waitpid().
  • Kernel Table Exhaustion: When an orphaned process dies, the kernel transitions its task descriptor to EXIT_ZOMBIE (<defunct>) awaiting harvest. Because sleep never reaps it, the dead struct remains stuck in the kernel process table indefinitely.
  • The Signal Deadlock: Forwarding termination signals like SIGTERM fails to propagate cleanly to underlying workloads, forcing Docker to wait out its 10-second timeout before dropping the uncatchable SIGKILL hammer.
tuxreaperd Grim Reaper Tux Promo Card

Processes don't fear tuxreaperd!

Part II: Architecture — Freestanding Minimalism vs. Container Inits

Existing container init supervisors like tini or Yelp’s dumb-init solve basic child reaping, but they introduce notable baggage and operational flaws for web development:

  • Static C Runtime Overhead: Static builds pull in standard C libraries (musl/glibc), bloating binaries anywhere from 20 KB to 750 KB+.
  • Blind Signal Blasting: Traditional inits treat every workload as a generic black box, broadcasting raw SIGTERM down the process tree. This breaks production web servers like Apache and PHP-FPM, which depend on non-standard signals (SIGWINCH and SIGQUIT) to finish in-flight FastCGI and HTTP requests gracefully.

I built tuxreaperd: a freestanding micro-init written in pure C and inline assembly with -nostdlib, dropping total on-disk size to under 5 KB with an idle footprint of just 16 KB RSS (~2 MMU pages).

Stripped Binary Size Comparison (KB on Disk)

Click bars or hover for exact footprints. Note: dumb-init (glibc static) weighs ~700+ KB and is omitted for scale.

Architectural & Feature Comparison Matrix

Feature / Scenario tini dumb-init mini-init-asm tuxreaperd (~4.7 KB)
Automatic per-binary signal translation No Manual --rewrite only No Yes (Apache / nginx / PHP-FPM)
Zero-config web graceful stops No No No Yes
Post-exit descendant process draining No No No Yes (60s bounded drain)
Multi-pass /proc signal sweeps No No No Two-pass sweep
Freestanding / no libc No No (unless musl) Yes Yes (-nostdlib + raw syscalls)
Restart-on-crash No No Yes (EP_RESTART_ENABLED) No
Configurable grace → SIGKILL timeout No No Yes (EP_GRACE_SECONDS) No (fixed 60s drain)

The Web-Aware Signal Remapping Pipeline

When docker stop issues a SIGTERM, tuxreaperd does not blindly terminate child processes. Instead, it inspects running processes in the container:

  • Raw VFS Inspection: Reads /proc via direct sys_getdents64 and sys_readlink syscalls to identify active daemons without spawning external shell processes.
  • Signal Translation: Remaps SIGTERM to SIGWINCH for Apache HTTP Server and SIGQUIT for PHP-FPM master processes to stop accepting new requests and complete pending connections.
  • Bounded Descendant Draining: Runs a non-blocking monotonic timer loop (up to 60 seconds) letting worker pools flush I/O buffers before the container halts.

Part III: Low-Level Systems — ARM64 ABI Realities & Hardware Ground Truth

Eliminating standard C libraries and targeting the raw Linux kernel ABI requires absolute precision across CPU architectures.

The ARM64 O_DIRECTORY Header Trap

During cross-architecture verification on physical ARM64 hardware (a MediaTek Kompanio 500 Chromebook), the /proc scanner failed immediately on directory opens. Generative AI tools and search engines frequently claim that file flags are universal from generic headers. They are not:

/* The Linux kernel ABI specifies architecture-distinct octal values for openat(): */ /* x86_64 Architecture */ #define O_DIRECTORY 00200000 /* 0x10000 */ /* ARM64 Architecture (arch/arm64/include/uapi/asm/fcntl.h) */ #define O_DIRECTORY 00040000 /* 0x4000 */

Passing the x86_64 constant on ARM64 caused openat() to return -EINVAL directly from the kernel VFS layer. Verifying on actual hardware via a direct runtime test confirmed the ground truth:

$ python3 -c "import os; print(hex(os.O_DIRECTORY))" # On x86_64: 0x10000 # On ARM64: 0x4000

Part IV: Benchmarks & Execution Verification

Benchmarking tuxreaperd and custom freestanding diagnostic utilities against standard GNU/glibc toolchains demonstrates the performance advantage of bypassing dynamic linking and runtime allocation.

Diagnostic Execution: proc-top3 vs. ps aux

root@21b0616d7726:~# time ps aux && time proc-top3 USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 176 16 ? Ss 00:04 0:00 /usr/local/bin/tuxreaperd root 7 0.0 0.0 2600 1672 ? S 00:04 0:00 sleep infinity root 24 0.0 0.0 15008 2480 ? Ss 00:04 0:00 nginx: master process /usr/sbin/nginx www-data 25 0.0 0.0 15356 5088 ? S 00:04 0:00 nginx: worker process root 36 0.0 0.0 235968 7032 ? Ss 00:04 0:00 php-fpm: master process (/etc/php/8.4/fpm/php-fpm.conf) root 2662 0.0 0.0 7492 5340 pts/1 S 00:28 0:00 bash root 3421 0.0 0.0 9616 4844 pts/1 R+ 00:35 0:00 ps aux real 0m0.003s user 0m0.001s sys 0m0.002s PID NAME CPU (ticks) RAM (bytes) ------- -------------- ---------------- ---------------- 2662 bash 8 5468160 36 php-fpm8.4 6 7200768 1 tuxreaperd 2 16384 real 0m0.001s user 0m0.001s sys 0m0.000s

While standard ps spends 66% of its execution time in kernel space resolving shared objects (ld-linux.so, libc.so, libproc2.so), the freestanding utility reads /proc entries directly from the stack, executing in under 1 millisecond with zero measurable system time.

Part V: What's New in Tux-Dock 0.4.2-beta

The 0.4.2-beta release brings several functional and operational updates to the C++ TUI control center:

  • Direct Command Execution with Output Preview: Added an interactive "Run Command in Container (with output)" action to fire off one-shot maintenance scripts or service cycles and inspect immediate output directly in the dialog without needing to attach a full interactive shell.
  • Docker Background State Poller: Implemented a 5-second background poller alongside idempotent mutation logic (handling 304/404 API states cleanly) to eliminate unresolved state drift when managing containers across multiple instances of Tux-Dock on the same machine.
  • Cross-Architecture Support: Fully verified build pipeline for both x86_64 and arm64 hardware targets.

Getting Started with Tux-Dock 0.4.2

# Clone repository git clone https://mentalnet.xyz/forgejo-v2/markmental/tuxdock.git cd tuxdock # Build host C++ TUI and freestanding tuxreaperd binaries ./compile.sh # Launch the TUI sudo ./build/tux-dock