Blog · Infrastruktur

Docker, WireGuard, Traefik & SMB – unsere kleine Corporate-Umgebung

Wie wir uns eine überschaubare, aber erstaunlich flexible Firmenumgebung gebaut haben – aus Bausteinen, die wir wirklich verstehen.

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_share und dns_config fü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.