Splitting Docker Across Mac + Synology NAS Without Changing DNS
Split Docker across a Mac and Synology NAS without touching DNS or ports. Migrate six services using Cloudflare tunnel ingress-flip and the AVX2 trap.
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 Concept: Ingress-Flip
Cloudflare tunnels do not 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.
Here is the architecture before:
User -> cloudflare.com -> tunnel daemon (Mac Mini) -> localhost:5678 -> n8n containerHere is the architecture after:
User -> cloudflare.com -> tunnel daemon (Mac Mini AND Synology) -> 192.168.1.50:5678 -> n8n container on NASThe public URL never changes. DNS never changes. The only thing that changes is the IP in the ingress rule.
The Migration Pattern
The process for each service was identical:
- Spin up a tunnel connector on the Synology NAS
- Move the Docker stack to the NAS
- Update the ingress rule from
localhost:8080to192.168.1.50:8080 - Confirm the service loads from an external network
- Stop the original container on the Mac Mini
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.
Setting Up the Second Tunnel Connector
On the Synology, I installed cloudflared through the Docker package manager. The container setup is straightforward:
version: '3.8'
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- shared_networkThe trick: use the same tunnel token on both machines. Cloudflare allows multiple connectors for a single tunnel. Each connector maintains its own outbound WebSocket connection to Cloudflare's edge. When traffic arrives, Cloudflare routes to any healthy connector.
The Service That Almost Killed the Migration
The Synology DS920+ uses an Intel Celeron J4125 - Apollo Lake architecture. Documenso's Docker image ships binaries compiled with AVX2 instructions. Apollo Lake does not support AVX2. The container started, then immediately segfaulted with no useful error.
This is the kind of problem self-hosters hit constantly and nobody writes about because it is boring to debug. The symptom: the container exits with code 139 (segfault) or just hangs. Docker logs show nothing. strace on the process reveals an illegal instruction signal.
The fix: rebuild the binary from source targeting x86-64-v2, package it as a custom Docker layer, and swap it in. Here is the Dockerfile I used:
FROM node:20-bookworm-slim AS builder
WORKDIR /app
RUN git clone https://github.com/documenso/documenso.git .
RUN npm ci
RUN npm run build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 3000
CMD ["node", "apps/web/server.js"]The key is building on a compatible architecture or using -march=x86-64-v2 to strip AVX2 instructions. I cross-compiled on the Mac Mini and copied the image to the Synology via docker save and docker load.
Took 90 minutes to figure out what was wrong and build the fix. The migration itself took 20.
The Traffic Flow
Here is the complete path a request takes after the split:
User browser -> Cloudflare edge (closest PoP) -> Cloudflare tunnel (any healthy connector)
-> ingress rule matching hostname -> 192.168.1.50:8080 -> container on Synology
-> response back through the same tunnelThe ingress rules in the Cloudflare Zero Trust dashboard look like this:
| Subdomain | Service | Target Machine |
|---|---|---|
| n8n | http://192.168.1.50:5678 | Synology |
| documenso | http://192.168.1.50:3001 | Synology |
| invoice | http://192.168.1.50:3002 | Synology |
| listmonk | http://192.168.1.50:9000 | Synology |
| twenty | http://localhost:3000 | Mac Mini |
| outline | http://localhost:3001 | Mac Mini |
Services still on the Mac Mini use localhost. Services migrated to the Synology use its LAN IP. Cloudflare does not care which machine the ingress target points to - it just proxies traffic.
What I Learned About Memory Pressure
The Mac Mini was under memory pressure even with swap enabled. At 14.5 GB used, the system was compressing memory pages and occasionally freezing for a second when opening a new application. After moving six services to the Synology, the Mac sits at 9.1 GB used with no swap activity.
The Synology DS920+ has 12 GB of RAM. After taking on the migrated services, it is at 5.2 GB used. The NAS was already running 24/7 for file storage - adding Docker services cost nothing extra in power or uptime.
If you are running a self-hosted productivity stack on a single machine, memory is the thing you will hit before CPU every time. Split early.
The One Thing I Would Change
I should have started with Docker Swarm or a lightweight orchestrator. The manual approach works for six services. At 15, you want declarative placement rules so you can say "n8n workers run on the Synology, Twenty stays on the Mac" without updating ingress rules by hand.
Docker Swarm handles this natively with node labels and constraints. The learning curve is shallow - it is just Docker Compose with a deploy block. If you are planning a multi-host setup from scratch, start with Swarm.
Summary
Splitting services across hosts is straightforward on paper and full of invisible architecture traps in practice. The Cloudflare tunnel ingress-flip pattern works without DNS changes, without port forwarding, and without downtime. The hard part is the stuff the docs never warn you about - like Celeron J4125 CPUs not supporting AVX2 instructions in an otherwise standard Docker image.
If you are running self-hosted services on a Synology or an old mini PC, you are going to hit the AVX2 wall eventually. Build from source and target your actual CPU architecture. It is the only reliable way.
Most infrastructure content tells you how things are supposed to work. The useful stuff tells you what broke and how you fixed it.
This is the sequel to How to Expose Your Docker Apps with Cloudflare Tunnels. Related: 7 AI Agents Running on a Mac Mini and The Productivity Stack Nobody Is Selling You.
Also relevant: n8n Silent Failure - Docker Networking covers another Docker gotcha, and Running llama.cpp on a 2013 Mac Pro shows what happens when you push old hardware past its limits.
Built with Docker infrastructure support from Whtnxt and MCP development services. Full service offering here.
Hero image: Photo by panumas nikhomkhai / Pexels.