5 min read

From 32-Bit Hacks to 64-Bit Native: Auditing & Hardening a 20-Container Raspberry Pi 5 Homelab

Every self-hosted server accumulates quiet 'temporary' workarounds in its docker-compose.yml. Here is what happened when I migrated my 20-container homelab from a 32-bit userland to a native 64-bit Raspberry Pi 5—and why removing legacy hacks is the most satisfying kind of engineering.

Every self-hosted server has two versions of its architecture: the clean diagram in your head, and the actual docker-compose.yml that evolved at 1:00 AM on a Sunday when a container refused to boot.

When I first wrote My Cloud, at Home, I talked about the philosophy of self-hosting—taking back ownership of your data, ditching endless SaaS subscriptions, and running your own digital island on a Raspberry Pi.

Today, I want to talk about the grittier engineering side of that island: what happens when you migrate a 20-container homelab from a legacy 32-bit userland (ARMv7) onto a native 64-bit Raspberry Pi 5 (aarch64, Debian 13 Trixie) with NVMe storage—and the surprising amount of hidden technical debt you uncover when you finally audit your own stack.


The Ghost of 32-Bit Workarounds Past

If you ran Docker on a Raspberry Pi a few years ago, you probably remember the awkward transition era: a 64-bit kernel running underneath a 32-bit OS userland.

Back then, modern container images (especially heavy Node.js, MySQL, and machine-learning workloads) started using newer 64-bit Linux system calls like clone3, clock_gettime64, and fstatat64. On a 32-bit system with an older libseccomp, Docker's default syscall filter didn't recognize them. The result? Containers would mysteriously crash on startup with cryptic permission errors, threads failing to spawn, or clocks refusing to read.

And what did every homelabber do at 1:00 AM when a container wouldn't start? You googled the error, found a GitHub issue from 2021, and pasted these two lines into your docker-compose.yml:

platform: linux/arm64/v8
security_opt:
  - seccomp=unconfined

It worked. The container booted. You moved on with your life.

The problem is that temporary workarounds have a way of becoming permanent infrastructure. When I copied my compose files over to a fresh, native 64-bit Debian 13 install on a Raspberry Pi 5, those lines came along for the ride across half my stack—my analytics engine, my Git server, my database, my media server, and my photo library.


Why seccomp=unconfined Is a Quiet Time Bomb

On a modern 64-bit OS with a current Docker engine, seccomp=unconfined isn't a compatibility fix anymore—it's just turning off the seatbelt.

Docker's default seccomp profile blocks roughly 44 dangerous Linux syscalls out of 300+ (things like kexec_load, mount, reboot, swapon, and kernel keyring manipulation) that a normal web app has no business touching. Disabling seccomp means that if an attacker ever lands a Remote Code Execution exploit inside a container, the kernel's syscall sandbox is wide open.

Auditing every compose file on my Pi 5 and stripping out every single seccomp=unconfined and hardcoded platform: linux/arm64/v8 override—plus dropping unnecessary capabilities like SYS_TIME from my DNS container (since the host already manages NTP via systemd-timesyncd)—was step one. Every service booted cleanly on native aarch64 with full kernel sandboxing restored.


Killing the Post-Reboot Crash Loop (depends_on Is Not Enough)

The second piece of technical debt was subtle: whenever my Raspberry Pi rebooted after a kernel or firmware update, this very blog would occasionally enter a 60-second crash loop.

Why? Because in Docker Compose, writing this:

depends_on:
  - ghost-db

only waits for the database container to start—it does not wait for the database engine inside it to finish initializing InnoDB and open port 3306. On a cold boot when 20 containers wake up at once, the web app would beat MySQL by three seconds, fail to connect, and crash.

Replacing bare depends_on lists with explicit readiness probes across both PostgreSQL and MySQL eliminated cold-boot race conditions completely:

  ghost:
    depends_on:
      ghost-db:
        condition: service_healthy

  ghost-db:
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

Now, dependent services wait patiently until the database actually answers a ping before attempting migrations or opening HTTP ports.


Treating the Raspberry Pi 5 Like a Real Server

The jump from microSD cards and USB-SATA adapters to an NVMe SSD on the Raspberry Pi 5 changes what a homelab can do—especially when you're hosting 300+ GB of lossless FLAC music, movies, and a self-hosted photo library with local machine-learning inference.

During the audit, I made three hardware and host-level changes that made a massive difference in day-to-day responsiveness:

  1. Unlocking PCIe Gen 3 (dtparam=pciex1_gen=3): By default, the Pi 5's PCIe lane runs at Gen 2 speeds (~450 MB/s). Adding a single line to /boot/firmware/config.txt and staging the latest EEPROM bootloader firmware nearly doubled sequential NVMe throughput to ~850–900 MB/s—turning large database dumps and photo thumbnail bursts into instant operations.
  2. Global Docker Log Rotation (/etc/docker/daemon.json): Left unchecked, chatty containers will happily write gigabytes of json-file logs to your SSD over a year, burning through NVMe write endurance and disk space. Enforcing a global max-size: 20m and max-file: 3 policy caps every container's log footprint at 60 MB.
  3. Full Container + Hardware Telemetry: Pairing prometheus-node-exporter on the host with cAdvisor inside the monitoring network gave Grafana visibility not just into host CPU temperature and NVMe I/O, but per-container memory pressure and CPU throttling across all 20 services.

The Secret Weapon: Running an AI Engineer Inside the Pi (AGY + Gemini 4 Argon)

Doing a top-to-bottom infrastructure audit like this manually across five separate compose stacks, 20 containers, kernel boot configs, and database engines usually takes an entire weekend of squinting at logs and man pages.

Instead, I did the entire migration audit and hardening session right from the terminal—running AGY powered by Gemini 4 Argon (Medium) directly inside the Raspberry Pi with full local access to the system.

Having an agentic AI living on the host itself changes the workflow completely compared to copy-pasting snippets into a browser chat window:

  • Full Host & Container Visibility: Because AGY runs directly on the Pi, it could inspect uname -m, read /boot/firmware/config.txt, query docker inspect across all 20 running containers, check systemd unit statuses, and spot the leftover seccomp=unconfined and platform: linux/arm64/v8 overrides across every sub-project in seconds.
  • Safe, Backup-First Execution: Before touching a single line of YAML, it ran my transactional database backup scripts (pg_dumpall and mysqldump), snapshotted timestamped .bak copies of every compose file, applied the changes stack by stack, and verified that every healthcheck turned green before moving on.
  • Zero Context Switching (Even for This Post): Even this blog post was drafted and scheduled directly from the Pi terminal—AGY authenticated against my local Ghost Admin API over loopback and queued it up without me ever opening the browser editor.

The Payoff: 20 Services, One Calm Board

When you step back and look at what a single 8 GB Raspberry Pi 5 can comfortably run once you clean up the legacy cruft, it's honestly wild:

  • Public & Publishing: Ghost (this blog), Umami (privacy-first analytics), Gitea (private Git hosting), and Cloudflare Tunnel.
  • Media & Photos: Jellyfin, Navidrome (serving 100+ GB of lossless FLACs), and Immich (complete with local ML facial/object recognition, Valkey, and VectorChord Postgres).
  • Home & Network: Home Assistant, Pi-hole v6 DNS sinkhole, and MySpeed.
  • Observability & Side Projects: Prometheus, Grafana, cAdvisor, Uptime Kuma, and a handful of custom Python/Go microservices.

All 20 containers run concurrently with kernel sandboxing enabled, healthcheck-ordered startup, nightly transactional database backups, and automated CVE scanning—while still leaving plenty of RAM headroom.

Self-hosting isn't "set it and forget it." Like maintaining a mechanical watch or a classic car, the joy is in popping the hood every once in a while, clearing out the old workarounds you don't need anymore, and leaving the system cleaner, faster, and safer than you found it—especially when you have an AI pair-engineer riding shotgun right inside your terminal.