Network Segmentation in Homelab Environments: VLANs and Docker Security
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
When I first set up my homelab, I ran everything on the same network and told myself "it's just home servers, what's the worst that could happen?" Then a malicious container nearly compromised my NAS. That day taught me that network segmentation isn't optional—it's the difference between a playground and a liability.
Network segmentation using VLANs (Virtual Local Area Networks) and Docker network isolation prevents a single compromised service from accessing your entire infrastructure. In this guide, I'll walk you through practical VLAN configuration on a budget-friendly setup and show you exactly how to isolate Docker containers so they can't talk to each other unless you explicitly allow it.
Why Network Segmentation Matters in a Homelab
Think of network segmentation as building internal firewalls. Without it, if your media server is compromised, an attacker gets the same network access as your Nextcloud instance, your password manager, and your home automation devices. With segmentation, each service lives on its own island.
VLANs let you create multiple logical networks on a single physical switch. Docker network isolation lets you create separate networks for different containers. Combined, they create defense in depth: even if someone breaks into your Jellyfin container, they can't reach your database container on a different Docker network.
I segment my homelab into four tiers:
- Management VLAN (10): Proxmox, router admin panel, backup server
- Services VLAN (20): Docker containers, self-hosted apps, databases
- Media VLAN (30): Jellyfin, Immich, download services
- IoT VLAN (40): Smart home devices, separate from everything else
This setup cost me under $100 total—you don't need enterprise gear. A $40–50/year VPS from RackNerd (around $3.33/month) makes a perfect segmented management node if you want to access your homelab remotely without exposing internal VLANs directly to the internet.
VLAN Configuration: The Hardware Setup
You need a managed switch that supports VLANs—unmanaged switches won't do it. I use a TP-Link T1600G-28TS (about $80 used), which handles 24 ports and VLAN tagging. Budget alternatives include the Ubiquiti EdgeSwitch or Cisco Catalyst 2960, both widely available secondhand.
Here's my physical setup:
- Router (port 1, untagged VLAN 1 + access to trunk)
- Proxmox host (port 2, tagged trunk)
- Docker host (port 3, tagged trunk)
- NAS (port 4, untagged VLAN 10 for management)
- Remaining ports for devices
Start by accessing your switch's web interface. For the TP-Link, it's usually 192.168.0.1. You'll create VLANs and assign ports.
Configuring the Router for VLAN Routing
Your router needs to act as a "gateway" between VLANs. I prefer OpenWrt or pfSense for this because they give you granular control. If you're using stock firmware, you may be limited.
On pfSense, creating a VLAN is straightforward. Here's the process:
# SSH into your pfSense router
ssh [email protected]
# Create VLAN interfaces in the GUI or config:
# Interfaces > VLANs > New VLAN
# Parent: em0 (or your WAN interface)
# VLAN Tag: 10 (for Management)
# Description: Management
# Then assign IP addresses:
# Interfaces > Assignments > New
# Select VLAN 10
# Configure IPv4: Static
# Address: 10.0.10.1 / Subnet: 24
# Create firewall rules to control traffic between VLANs:
# Firewall > Rules > [VLAN interface]
# Rule: Allow Management VLAN to Services VLAN (port 5432 for PostgreSQL)
# Rule: Block Services VLAN from Management VLAN (inbound)
The key insight: by default, VLANs can't talk to each other. You must explicitly create firewall rules to allow inter-VLAN communication. I restrict most communication and only open ports when necessary.
Docker Network Isolation: The Container Layer
Even if your Jellyfin container gets root access, it shouldn't be able to reach your MariaDB container unless they're on the same Docker network. I create separate networks for different service tiers.
# Create Docker networks for isolation
docker network create --driver bridge --subnet=172.20.0.0/16 services-internal
docker network create --driver bridge --subnet=172.21.0.0/16 media-internal
docker network create --driver bridge --subnet=172.22.0.0/16 db-internal
# Create a read-only network for services that should have zero egress
docker network create --driver bridge --internal --subnet=172.23.0.0/16 isolated-services
# Example: Run a database on the isolated network
docker run -d \
--name mariadb \
--network db-internal \
-e MYSQL_ROOT_PASSWORD=SecurePassword123 \
-e MYSQL_DATABASE=nextcloud \
-v /data/mariadb:/var/lib/mysql \
mariadb:11
# Example: Run Nextcloud on services network but connect it to DB network
docker run -d \
--name nextcloud \
--network services-internal \
-p 8080:80 \
-e MYSQL_HOST=mariadb \
-e MYSQL_DATABASE=nextcloud \
-v /data/nextcloud:/var/www/html \
nextcloud:latest
# Connect Nextcloud to the database network so it can reach MariaDB
docker network connect db-internal nextcloud
This setup ensures that Nextcloud (on services-internal) can reach MariaDB (on db-internal) because we explicitly connected them, but Jellyfin (on media-internal) cannot reach the database at all unless we add it to that network too.
-p 8080:80), any network on your machine can reach it via the host IP. Use 127.0.0.1:8080:80 to bind only to localhost if you want strict isolation. Better yet, use a reverse proxy (Caddy or Traefik) on a single container that bridges networks.
Practical Example: A Secure Multi-Tier Docker Compose Setup
Here's a real docker-compose.yml that implements segmentation with Caddy as the reverse proxy:
version: '3.8'
services:
caddy:
image: caddy:latest
container_name: caddy-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- services-internal
- media-internal
depends_on:
- nextcloud
- jellyfin
restart: unless-stopped
nextcloud:
image: nextcloud:latest
container_name: nextcloud
environment:
MYSQL_HOST: mariadb
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud_user
MYSQL_PASSWORD: SecureDBPass123
volumes:
- nextcloud_data:/var/www/html
networks:
- services-internal
- db-internal
depends_on:
- mariadb
restart: unless-stopped
mariadb:
image: mariadb:11
container_name: mariadb
environment:
MYSQL_ROOT_PASSWORD: RootPass123
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud_user
MYSQL_PASSWORD: SecureDBPass123
volumes:
- mariadb_data:/var/lib/mysql
networks:
- db-internal
restart: unless-stopped
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
ports:
- "8920:8920"
volumes:
- jellyfin_data:/var/lib/jellyfin
- /media/movies:/movies:ro
- /media/tv:/tv:ro
networks:
- media-internal
restart: unless-stopped
networks:
services-internal:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
media-internal:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
db-internal:
driver: bridge
ipam:
config:
- subnet: 172.22.0.0/16
volumes:
caddy_data:
caddy_config:
nextcloud_data:
mariadb_data:
jellyfin_data:
In this setup, Caddy bridges the services and media networks, so it can proxy requests to both Nextcloud and Jellyfin. MariaDB lives on a separate network that only Nextcloud can access. Jellyfin has no access to Nextcloud or the database—perfect isolation.
Testing Your Segmentation
After deployment, verify that isolation actually works:
# Test: Can Jellyfin reach MariaDB?
docker exec jellyfin ping mariadb
# Should timeout or return "Name or service not known"
# Test: Can Nextcloud reach MariaDB?
docker exec nextcloud mysql -h mariadb -u nextcloud_user -p -e "SELECT 1"
# Should work (enter password)
# Test VLAN routing (from a device on Management VLAN):
ping 10.0.20.1 # Services gateway - should work if rule allows
ping 10.0.30.1 # Media gateway - should NOT work if rule denies
If these tests fail as expected (Jellyfin can't reach the database), your segmentation is working.
Monitoring and Maintenance
Document your firewall rules and review them quarterly. I keep a simple Markdown file listing:
- Which VLAN can reach which
- Which ports are open
- Why each rule exists
Use tools like docker network inspect to audit container connectivity, and keep your switch firmware updated (usually a few times a year for managed switches).
Going Remote: Adding a VPS to Your Setup
If you want remote access to your homelab without exposing internal VLANs, a low-cost VPS ($40/year with RackNerd) makes a perfect bastion host. Run WireGuard or Tailscale on it to tunnel into your homelab management VLAN. Your Docker services never touch the internet directly—traffic goes through the VPS gateway instead.
Conclusion
Network segmentation is not a "nice to have"—it's foundational security. Start small: if you only run Docker, create three networks (database, services, media). Add VLANs when you're ready to extend to physical devices. The investment of a few hours now saves you from a breach that could expose your personal data or compromise your entire infrastructure.
Next step: Buy or borrow a managed switch, configure one test VLAN, and spin up a test docker-compose with network isolation. You'll gain confidence quickly.
Discussion