Splitting Docker Across Mac + Synology NAS Without Changing DNS
How I moved six production services from a RAM-constrained Mac Mini to a Synology NAS using Cloudflare tunnel ingress-flip, zero DNS changes, and one AVX2 segfault that almost killed the migration.
Last week I posted about running production Docker apps through Cloudflare Tunnels with no open ports and no static IP. That setup works great until you run out of RAM on the Mac hosting everything.
My Mac Mini was sitting at 14.5 GB of 16 GB used. Twenty, n8n, Outline, Listmonk, Documenso, InvoiceNinja — every service fighting for the same memory. I needed to split the stack across two machines without touching DNS, without changing ports, and without my wife asking why the internet was down.
The answer: Cloudflare tunnel ingress-flip.
The Pattern: Ingress-Flip Migration
Cloudflare tunnels don't bind to a specific machine. They bind to tunnel connectors — lightweight daemons that can run anywhere. When you define an ingress rule connecting a public hostname to a local service, that localhost resolves on whatever machine the connector is running on.
So the migration pattern is:
- Spin up a tunnel connector on the Synology NAS
- Move a Docker stack to the NAS
- Change the ingress rule from
localhost:8080to192.168.1.50:8080(or wherever the service now lives) - Done. Zero DNS changes. Zero port forwarding. The public URL never moves.
The Migration in Practice
In one session I moved six services — Documenso, InvoiceNinja, Outline's Redis, Listmonk, and two n8n workers — from the Mac Mini to the Synology. The Mac dropped from 14.5 GB to 9.1 GB. No downtime anyone would notice.
The docker-compose stacks look nearly identical on both machines. The only real difference is the cloudflared tunnel connector running on each host, pointing at the same tunnel UUID but resolving local services differently based on which machine they live on.
Here's the core of the Synology stack:
version: '3.8'
services:
documenso:
image: docker.io/documenso/documenso:latest
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@postgres:5432/documenso?schema=public
depends_on:
- postgres
invoice-ninja:
image: invoicekit/invoice-ninja:latest
ports:
- "8080:80"
environment:
- DB_HOST=postgres
- DB_DATABASE=ninja
depends_on:
- postgres
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --hostname app.mydomain.com --url http://localhost:3000 run my-tunnel-uuidThe Cloudflare tunnel connector on the NAS picks up the new ingress rules and routes traffic to the services now running locally. The public hostname never changes — it was always pointing at the tunnel, not at any specific machine.
The Real Story: The AVX2 Segfault
Most infrastructure content tells you how things are supposed to work. The useful stuff tells you what broke and how you fixed it. Splitting services across hosts is straightforward on paper and full of invisible architecture traps in practice.
The Synology DS920+ uses an Intel Celeron J4125 — Apollo Lake architecture. Documenso's Docker image ships binaries compiled with AVX2 instructions. Apollo Lake doesn't support AVX2. The container started, then immediately segfaulted with no useful error.
$ docker logs documenso
/app/dist/index.js: line 1: illegal instruction (core dumped)This is the kind of problem self-hosters hit constantly and nobody writes about because it's boring to debug.
The Fix: Build From Source Targeting x86-64-v2
The solution required rebuilding the binary from source targeting x86-64-v2, packaging it as a custom Docker layer, and swapping it in.
Here's the Dockerfile override I used:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Rebuild native modules targeting x86-64-v2
ENV CFLAGS="-march=x86-64-v2"
ENV CXXFLAGS="-march=x86-64-v2"
RUN npm rebuild
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "dist/index.js"]The key is the CFLAGS and CXXFLAGS environment variables. Setting -march=x86-64-v2 ensures the compiler generates instructions compatible with Apollo Lake's limited instruction set, avoiding AVX2 entirely while still using newer instruction sets that Celeron J4125 does support (SSE4.2, POPCNT, etc.).
Took 90 minutes to debug and fix. The migration itself took 20.
Why This Matters for Self-Hosters
If you're running self-hosted services on a Synology or an old mini PC, you're going to hit the AVX2 wall eventually. Build from source and target your actual CPU architecture. It is the only reliable way.
The broader lesson: when you split services across hosts, the failure modes aren't in the networking — they're in the binaries. Docker images are almost always built for generic x86-64 or newer architectures. Your hardware may not match. Always check CPU instruction support before assuming a container will just work.
The Memory Trade-off
After the migration, the Mac Mini dropped from 14.5 GB to 9.1 GB usage. The Synology DS920+ (with 8 GB RAM) handles the lighter services comfortably. The key insight: not all services need the same resources. Database-heavy workloads like PostgreSQL benefit from being on the machine with more RAM, while stateless services can live anywhere.
I left n8n and TwentyCRM on the Mac Mini since they're memory-intensive during workflow execution. Outline, Listmonk, Documenso, and InvoiceNinja moved to the NAS — they run fine with less RAM and were the biggest consumers of the Mac's 16 GB.
This post is part of my Systems Architecture pillar. I write about self-hosting, automation, and infrastructure that actually works in practice.