In modern homelab and production environments, it’s often necessary to separate internal (private) and external (public) ingress traffic for security, compliance, and operational clarity. With Docker, Traefik, and VLANs, you can achieve this cleanly—without exposing sensitive services to the wrong network.
Below is a practical, production-grade approach using:
- Docker Compose
- Traefik (multiple instances)
- VLAN-backed Docker networks
- Provider constraints for service discovery
1. Network Topology: VLAN-backed Docker Networks
Create two Docker networks, each mapped to a different VLAN/subnet:
networks:
trf_ext:
driver: ipvlan
driver_opts:
parent: eth0.1125
ipvlan_mode: l2
ipam:
config:
- subnet: 10.2.125.0/24
gateway: 10.2.125.1
trf_int:
driver: ipvlan
driver_opts:
parent: eth0.1130
ipvlan_mode: l2
ipam:
config:
- subnet: 10.2.130.0/24
gateway: 10.2.130.1
trf_ext
is for external/public traffic (VLAN 1125).trf_int
is for internal/private traffic (VLAN 1130).
2. Multiple Traefik Instances
Run two Traefik containers, each on its own VLAN-backed network:
External Traefik (traefik-ext.yaml
)
services:
traefik-external:
image: traefik:latest
networks:
trf_ext:
ipv4_address: 10.2.125.254
socket_proxy:
command:
- --providers.docker=true
- --providers.docker.network=trf_ext
- --providers.docker.exposedByDefault=false
- --providers.docker.constraints=Label(`traefik.group`, `external`)
# ...other flags...
Internal Traefik (traefik-int.yaml
)
services:
traefik-internal:
image: traefik:latest
networks:
trf_int:
ipv4_address: 10.2.130.254
socket_proxy:
command:
- --providers.docker=true
- --providers.docker.network=trf_int
- --providers.docker.exposedByDefault=false
- --providers.docker.constraints=Label(`traefik.group`, `internal`)
# ...other flags...
3. Provider Constraints for Service Discovery
Provider constraints ensure each Traefik instance only discovers and routes the services you intend.
- Add a label to each service:
traefik.group=external
for public servicestraefik.group=internal
for private services
Example:
services:
ghost:
image: ghost
networks:
- trf_ext
labels:
- "traefik.enable=true"
- "traefik.group=external"
# ...other traefik labels...
portainer:
image: portainer/portainer-ce
networks:
- trf_int
labels:
- "traefik.enable=true"
- "traefik.group=internal"
# ...other traefik labels...
4. TLS and Certificate Resolvers
Each Traefik instance can use its own certificate resolver (e.g., Let’s Encrypt for external, ZeroSSL for internal):
# traefik-ext.yaml
- --entrypoints.websecure.http.tls.certresolver=dns-cloudflare
# traefik-int.yaml
- --entrypoints.websecure.http.tls.certresolver=zerossl
5. Result: Clean, Isolated Ingress
- External Traefik only sees and routes services labeled
traefik.group=external
and attached totrf_ext
. - Internal Traefik only sees and routes services labeled
traefik.group=internal
and attached totrf_int
. - No accidental exposure of internal services to the public.
- Each VLAN/subnet is isolated at L2, and only the intended services are reachable.
6. Extra: Fine-Grained Control
You can use more complex constraints for advanced scenarios:
- --providers.docker.constraints=Label(`traefik.group`, `internal`) && Label(`traefik.env`, `prod`)
Or use regex:
- --providers.docker.constraints=LabelRegex(`traefik.group`, `int.*`)
Conclusion
By combining Docker’s network isolation, Traefik’s multi-instance support, and provider constraints, you can build a robust, secure, and maintainable ingress architecture for any environment—homelab or production.
No sensitive data is exposed, and you have full control over what’s public and what’s private.
References:
Happy homelabbing!