Warum wir überhaupt eine „Mini-Corporate-Umgebung“ wollten
Wir sind ein kleines Team mit ziemlich klassischen Anforderungen: interne Tools, Admin‑Zugänge, ein paar Dienste, die nur im VPN sichtbar sein sollen, und ein Fileshare, der einfach funktioniert. Ein kompletter Enterprise‑Stack mit SD‑WAN, Firewall‑Appliances und proprietärem VPN war für uns aber eindeutig zu groß – finanziell und von der Komplexität her.
Stattdessen wollten wir eine Umgebung, die wir im Zweifel mit ein paar Compose‑Files, ein paar iptables‑Regeln und einem Blockdiagramm erklären können. Nichts, wofür man eine einwöchige Zertifizierung braucht, um einen neuen Dienst ans Netz zu bringen.
Schritt 1: Netzwerke & Volumes anlegen
Bevor wir Services starten, legen wir Netzwerke und Volumes an – einmalig in Portainer:
- Netzwerk
proxy(extern), in dem Traefik später alle HTTP/HTTPS‑Dienste sieht - Netzwerk
internal_net(extern), in dem Traefik, WireGuard, DNS und SMB miteinander sprechen - Externe Volumes für
letsencrypt,wireguard_config,wireguard_db,smb_shareunddns_configfür Zertifikate und Konfigurationen
Der Vorteil: Wenn wir später Container neu starten oder Images austauschen, bleiben Zertifikate und Konfigurationen erhalten. Portainer dient uns hier als „kleines Control‑Panel“ für die Infrastruktur – die eigentliche Logik steckt aber weiter in überschaubaren YAML‑Snippets.
Schritt 2: Traefik als Edge-Router aufsetzen
Traefik hängt in beiden Netzen und kümmert sich um TLS‑Terminierung und Routing via Hostnames:
services:
traefik:
image: traefik:latest
restart: always
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.main.acme.tlschallenge=true"
- "--certificatesresolvers.main.acme.email=admin@example.com"
- "--certificatesresolvers.main.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt/"
networks:
proxy:
internal_net:
ipv4_address: 172.31.99.254
Später versehen wir einzelne Dienste mit Traefik‑Labels (Host‑Rules, TLS‑Konfiguration). Wichtig ist nur, dass Traefik in beiden Netzen hängt und eine feste IP im internen Netz hat.
Schritt 3: WireGuard-VPN anbinden
Im nächsten Schritt bringen wir den WireGuard‑Server ins gleiche interne Netz:
wireguard:
image: linuxserver/wireguard:latest
cap_add:
- NET_ADMIN
volumes:
- wireguard_config:/config
ports:
- "51820:51820/udp"
restart: always
networks:
proxy:
internal_net:
ipv4_address: 172.31.99.2
Die Peers bekommen als AllowedIPs u.a. das interne Netz (z.B. 172.31.99.0/24). Damit
ist der Pfad VPN → WireGuard → interne Dienste grundsätzlich da – aber noch nicht ganz stabil.
Für das Management der Peers nutzen wir eine kleine WireGuard‑UI, die im gleichen Container‑Netzwerk läuft und die Konfiguration in das Volume von WireGuard schreibt:
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
depends_on:
- wireguard
cap_add:
- NET_ADMIN
network_mode: service:wireguard
environment:
- WGUI_USERNAME=admin
- WGUI_PASSWORD=CHANGE_ME_STRONG
restart: always
volumes:
- wireguard_db:/app/db
- wireguard_config:/etc/wireguard
Schritt 4: „Warum sehe ich den SMB-Share nicht?“ – NAT fixen
Unser erster Versuch war naiv: WireGuard‑Clients bekamen ein eigenes Netz (z.B. 10.252.2.0/24)
und wir haben dieses Netz einfach in das Docker‑Netz geroutet. Ergebnis: Pings gingen, aber der SMB‑Share
war aus dem VPN heraus nicht zuverlässig erreichbar. Einige Protokolle mögen es gar nicht, wenn Source‑IPs
„falsch“ aussehen.
Die Lösung war, den Verkehr aus dem VPN in Richtung Docker‑Netz zu SNAT‑ten. In der WireGuard‑UI tragen
wir dazu eine einfache NAT‑Regel als PostUp am Server ein (sinngemäß so, wie man es auch
direkt in iptables setzen würde):
iptables -t nat -A POSTROUTING \
-s 10.252.2.0/24 \
-d 172.31.99.0/24 \
-j SNAT --to-source 172.31.99.2
Damit sieht für die Container im internen Netz jeder VPN‑Client so aus, als käme er von der
WireGuard‑IP 172.31.99.2. Für unsere Zwecke reicht das völlig – wir wollen ja kein
identitätsbasiertes Zero‑Trust‑Netz, sondern ein kleines, gut überschaubares Admin‑VPN.
Schritt 5: Namensauflösung mit Technitium DNS
Der zweite Stolperstein war DNS. Wir wollten Hostnames wie vpn-ui.internal.example oder
files.internal.example, die intern auf Traefik bzw. den SMB‑Container zeigen. Gleichzeitig
wollten wir TLS‑Zertifikate über Let’s Encrypt, also musste Traefik diese Namen auch „von außen“ sehen
können.
Am Ende haben wir uns für einen einfachen Ansatz entschieden:
- Öffentliche DNS‑Einträge zeigen auf unsere Traefik‑Instanz (z.B. über eine feste IP oder einen DynDNS‑Record).
- Traefik holt per ACME/Let’s Encrypt Zertifikate für diese Hostnames.
- Technitium DNS läuft intern und beantwortet Anfragen aus dem VPN für dieselben Namen – nur eben mit internen Routen.
So bekommen wir auch auf rein internen Diensten gültige TLS‑Zertifikate, obwohl sie nur aus dem VPN erreichbar sind. Traefik steht vorn, kümmert sich um Zertifikate und leitet Anfragen dann ins interne Netz weiter.
Schritt 6: SMB-Share und DNS-Records ergänzen
Jetzt können wir den SMB‑Container und die DNS‑Records hinzufügen (siehe Compose‑Skizze). In Technitium
legen wir z.B. files.internal.example auf die SMB‑IP und vpn-ui.internal.example
auf Traefik.
Schritt 7: Alles zusammenstecken – Compose-Skizze
Im Alltag liegt das Ganze bei uns als Portainer‑Stack vor. Die folgende Skizze fasst die Bausteine zusammen:
Compose-Skizze anzeigen
version: "3.8"
networks:
proxy:
external: true
internal_net:
external: true
volumes:
letsencrypt:
external: true
wireguard_config:
external: true
wireguard_db:
external: true
smb_share:
external: true
dns_config:
external: true
services:
traefik:
image: traefik:latest
restart: always
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.main.acme.tlschallenge=true"
- "--certificatesresolvers.main.acme.email=admin@example.com"
- "--certificatesresolvers.main.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt/"
networks:
proxy:
internal_net:
ipv4_address: 172.31.99.254
wireguard:
image: linuxserver/wireguard:latest
cap_add:
- NET_ADMIN
volumes:
- wireguard_config:/config
ports:
- "51820:51820/udp"
restart: always
networks:
proxy:
internal_net:
ipv4_address: 172.31.99.2
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
depends_on:
- wireguard
cap_add:
- NET_ADMIN
network_mode: service:wireguard
environment:
- WGUI_USERNAME=admin
- WGUI_PASSWORD=CHANGE_ME_STRONG
restart: always
volumes:
- wireguard_db:/app/db
- wireguard_config:/etc/wireguard
smb:
image: dperson/samba
environment:
- USERID=1000
- GROUPID=1000
- TZ=Europe/Berlin
volumes:
- smb_share:/mount
command: >
samba.sh -s "files.internal.example;/mount;yes;no;no;fileshare" -u "fileshare;CHANGE_ME"
cap_add:
- NET_ADMIN
networks:
internal_net:
ipv4_address: 172.31.99.3
dns:
image: technitium/dns-server:latest
environment:
- DNS_SERVER_NAME=LABDNS
volumes:
- dns_config:/etc/dns/
networks:
proxy:
internal_net:
ipv4_address: 172.31.99.20
Was wir gelernt haben
Die Umgebung ist keine Raketenwissenschaft, aber sie hat uns ein paar Dinge gelehrt: Kleine, verständliche Bausteine schlagen große, undurchsichtige Plattformen. Man muss nicht jeden Use Case mit einem eigenen SaaS‑Produkt erschlagen – manchmal reicht ein sauber konfigurierter VPN‑Tunnel plus ein paar Docker‑Netzwerke, um 90 % der Anforderungen eines kleinen Teams abzudecken.
