At some point, your setup stops being “a server” and starts being a system.
That’s usually when things get weird.
- Docker networking starts fighting you
- Reverse proxies turn into glue instead of structure
- Half your services live on the host, the other half in containers
- Everything works, but only if you don’t touch it
I hit that point running a hybrid Swizzin + Docker stack.
So I stopped trying to fix Docker.
And replaced the network instead.
The problem nobody talks about
Most people build stacks like this:
internet → nginx → apps → storage
Then spend the rest of their time:
- hardening NGINX
- blocking bots
- tuning fail2ban
- pretending that’s normal
It works.
But it’s backwards.
You’re building a public service… just to use it privately.
Move the boundary
Instead of defending the edge, move it.
Everything sits behind Tailscale.
tailscale → nginx → local services
No public ingress. No exposed ports. No background noise.
You don’t protect your server from the internet.
You just don’t publish it.
The hybrid setup that actually works
This is not a “Docker everything” post.
Some workloads want the metal:
- torrent clients
- disk-heavy operations
- anything that hammers IO
That stays on Swizzin.
Everything else goes into containers:
- Sonarr / Radarr (plus 4K splits)
- Prowlarr
- Jellyseerr
- PostgreSQL
- NZBGet
- automation glue
The split is simple:
Metal for throughput. Containers for control.
Where it usually falls apart
Hybrid setups break at the network layer.
- Docker uses bridges
- Host services expect direct access
- Reverse proxies sit awkwardly in between
You end up debugging things like:
“Why can this container not reach that service unless I do something cursed?”
And yeah — you can make it work.
But it’s fragile.
Tailscale becomes the network
Instead of fixing Docker networking, I stopped relying on it.
Everything binds to Tailscale.
- host services
- reverse proxy
- remote access
- internal routing
Now everything lives on one flat network.
No NAT. No guessing. No weird interfaces.
If it’s on the network, it works.
NGINX is not public anymore
This is the part that actually changes things.
NGINX does not listen on 0.0.0.0.
It binds only to the private interface.
# only reachable over tailscale
listen 100.x.x.x:443 ssl;
listen [fd7a:...]:443 ssl;
server_name media.internal;
That means:
- no public 80/443
- no scanners
- no bot traffic
- no constant garbage
NGINX becomes a private ingress router, not a public web server.
The clean service model
Everything behind NGINX is local-only.
From the Docker side:
services:
sonarr:
ports:
- "127.0.0.1:8989:8989"
radarr:
ports:
- "127.0.0.1:7878:7878"
And from the routing side:
location /sonarr {
proxy_pass http://127.0.0.1:8989;
}
location /radarr {
proxy_pass http://127.0.0.1:7878;
}
So the flow becomes:
tailscale → nginx → 127.0.0.1 → container
Apps don’t know the internet exists.
They don’t need to.
This is where it clicks
tailscale
│
▼
nginx
│
┌───────────────┼───────────────┐
│ │ │
swizzin docker database
(bare metal) stack (shared)
│ │
└─────── shared storage ───────┘
(/data)
This is the first time the system actually feels stable.
This isn’t just access. It’s an edge.
Most people use Tailscale for access.
This uses it as a boundary.
Tailscale controls who connects
NGINX controls how things are served
And NGINX is not stock.
Treating NGINX like a real edge
NGINX is built properly:
zstd on;
brotli on;
gzip on;
Along with:
- HTTP/2 + HTTP/3 + QUIC
- async IO
- proper buffer tuning
Compression isn’t an afterthought.
It’s layered:
- prefer zstd
- fall back to brotli
- fall back to gzip
- disable for bad clients
This is closer to CDN behavior than most homelabs ever get.
Caching where it matters
proxy_cache_path /var/cache/nginx/sonarr keys_zone=sonarr_cache:10m;
location /sonarr/MediaCover/ {
proxy_cache sonarr_cache;
proxy_cache_valid 200 30d;
}
You cache:
- media assets
- UI bundles
- static resources
With:
- cache locking
- stale updates
- auth-aware bypass
Not “turn on cache and hope.”
Deliberate.
IO tuned like it matters
Under the hood:
- async file reads
- direct IO for large files
- open file caching
- tuned buffers
The goal:
Serve fast without blocking.
Even internally, this matters more than people think.
You accidentally build a private CDN
client → tailscale → nginx (edge)
├─ compression
├─ caching
├─ routing
▼
services
No Cloudflare. No exposure.
Still fast.
Still clean.
The part most people miss
Tailscale isn’t just networking.
It’s policy.
Access isn’t:
“you’re on the network, good luck”
It’s defined.
Here’s a simplified version:
{
"acls": [
{
"action": "accept",
"src": ["iphone"],
"dst": ["autogroup:internet:*"]
},
{
"action": "accept",
"src": ["automation"],
"dst": ["media:443", "media:22"]
}
]
}
Different devices get different access.
Your phone is not your server.
Automation nodes don’t get full control.
It gets better: posture checks
You can validate what the device is:
"postures": {
"default": [
"node:os in ['macos','ios','windows']",
"node:tsReleaseTrack == 'stable'"
]
}
Now access depends on:
- OS
- client version
- device type
Not just “is it connected?”
SSH without the nonsense
"ssh": [
{
"action": "check",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["root", "user"],
"checkPeriod": "48h"
}
]
No key distribution.
No drift.
Identity-based SSH, built into the network.
Mental model
infra:
swizzin + docker + storage
routing:
nginx
control:
tailscale
Each layer has a job.
Nothing overlaps.
Nothing fights.
And this is why Swizzin adding Tailscale matters
There’s active work bringing native Tailscale support into Swizzin.
This is the part people are sleeping on.
Right now, this setup takes intent:
- install tailscale manually
- bind interfaces correctly
- structure nginx properly
When this lands?
This becomes the default sane way to run a box.
- private by default
- clean networking
- hybrid setups just work
- no public exposure required
This is a big shift.
What you actually get
Not features.
Outcomes.
Performance where it matters
Bare metal where it counts.
Flexibility where it matters
Containers for everything else.
Security by default
Nothing exposed unless you choose it.
Simplicity under pressure
One network. One model.
No weirdness.
Final thought
Most people scale by adding layers.
More proxies. More rules. More complexity.
This goes the other way.
Remove the internet.
Flatten the network.
Treat your server like a private edge node.
Let:
- Swizzin handle the metal
- Docker handle the apps
- Tailscale handle the network
And everything gets quieter.
