| 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 infinitysits in a blockingnanosleep()loop. It registers zero signal handlers forSIGCHLDand never callswait()orwaitpid(). - Kernel Table Exhaustion: When an orphaned process dies, the kernel transitions its task descriptor to
EXIT_ZOMBIE(<defunct>) awaiting harvest. Becausesleepnever reaps it, the dead struct remains stuck in the kernel process table indefinitely. - The Signal Deadlock: Forwarding termination signals like
SIGTERMfails to propagate cleanly to underlying workloads, forcing Docker to wait out its 10-second timeout before dropping the uncatchableSIGKILLhammer.
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
SIGTERMdown the process tree. This breaks production web servers like Apache and PHP-FPM, which depend on non-standard signals (SIGWINCHandSIGQUIT) 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
/procvia directsys_getdents64andsys_readlinksyscalls to identify active daemons without spawning external shell processes. - Signal Translation: Remaps
SIGTERMtoSIGWINCHfor Apache HTTP Server andSIGQUITfor 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_64andarm64hardware 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