5 min read

Standleitung - a tunnel for eternity

Expose private home services via a secure WireGuard tunnel using Socat and a Traefik reverse proxy on your VPS – without opening ports on your router. No Cloudflare, no magic. Just Docker, forwarding, HTTPS, and full control. My "Standleitung" setup is lean, fast, and fully self-managed.

Almost all of my applications and services are containerized and run either on my VPS or on my homelab servers. A key aspect of this setup is a clear and simple separation between public and private services. For example, my blog, websites, and some game servers? Public... My backups, smart home, cloud-stuff, and other sensitive services? Private, of course. But keeping these systems isolated while still allowing them to communicate seamlessly and securely is easier said than done.

That’s exactly why I decided to set up a WireGuard VPN between my VPS and my router/home network. You could also connect your VPS directly to one of your local devices – just go with whatever fits your setup best. There’s more info about this choice down below.


⚠️ Disclaimer

The approach described in this blog post – connecting your public server to your private home network – should be carefully considered. I'm only demonstrating how to implement this. With a setup of this kind, ensure that not only your home network is well-secured and possibly segmented, but that your VPS in particular is properly protected.


Requirements

Before diving into config files and Docker YAMLs, let’s quickly cover what you’ll need for this setup:

  • VPS with a public IP address
  • Home server with one or more services that should be exposed remotely
  • Public domain pointing to your VPS (e.g. A-Record)
  • Basic knowledge of how to use Docker Compose, WireGuard and Traefik

WireGuard VPN and Socat Forwarding

To create a VPN tunnel that connects the home network with the VPS in a secure and containerized way, I use the WireGuard container from Linuxserver. The alpine/socat container handles the TCP port forwarding. This combination gives me fine-grained control over traffic, avoids exposing ports unnecessarily, and makes debugging a lot easier.

💡 As I mentioned before – you could also connect your VPS directly to one of your local servers. In that case, you can probably leave out the socat part, but you’ll lose flexibility. I haven’t tested it that way myself.

Standleitung Compose

Let’s say you want to expose a service running on your homelab at 192.168.178.100:3000. Here’s how you can set it up using Docker Compose on your VPS:

##########
# Standleitung Docker Compose
# docker-compose.standleitung.yml
##########
---
services:
  wireguard:
    image: linuxserver/wireguard
    hostname: wireguard
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
    volumes:
      - ./wireguard:/config
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped
    networks:
      - standleitung

  socat_xyz:  # copy/paste/rename this definiton for more services
    image: alpine/socat
    depends_on:
      - wireguard
    command: >
      TCP-LISTEN:3000,fork,reuseaddr TCP:192.168.178.100:3000
    network_mode: "service:wireguard"
    restart: unless-stopped

networks:
  standleitung:

💡 Replace the listening port and private IP:port. Place your wg_config.conf under ./wireguard/.

docker compose up -d -f docker-compose.standleitung.yml

💡 Start your compose in background (detached, -d).

Once this is up and running, socat listens on port 3000 and forwards that traffic through the WireGuard tunnel to your private service. Or rather, WireGuard is technically the one listening, since socat uses WireGuard’s network interface via network_mode.

💡 Using network_mode: "service:wireguard" means the socat container shares the exact same network namespace as the WireGuard container – it’s as if they’re running inside the same network stack. Socat doesn’t get its own IP, it just piggybacks on WireGuard’s.

It’s basically magic. But slower. And real.


Traefik Reverse Proxy

Now that your VPS can talk to your private services, it’s time to expose them securely and publicly via HTTPS. That’s where Traefik comes in. It handles SSL via Let’s Encrypt, routes requests to your services, and looks cool while doing it. If you want, you can also enable the internal dashboard for a better overview of routers, middlewares, services, and all that stuff.

Traefik Compose

We’ll run Traefik in its own dedicated Docker network (because isolation is good), but also attach it to our standleitung VPN docker network so it can reach those tunneled services.

Related to the docker.sock-volume: RULE #1 - Do not expose the Docker daemon socket (even to the containers)

##########
# Traefik Docker Compose
# docker-compose.traefik.yml
##########
---
services:
  traefik:
    image: traefik
    restart: unless-stopped
    command:
      - "--configFile=/etc/traefik/traefik.yml"
    labels:
      - "traefik.enable=true"
    ports:
      - "80:80"
      - "443:443/tcp"
      - "443:443/udp"  # required by HTTP/3
    volumes:
      - "./traefik:/etc/traefik"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    networks:
      - traefik
      - standleitung

networks:
  traefik:
  standleitung:
    external: true

💡 Network standleitung is declared as an external network so that Traefik can communicate with the VPN-based services, like our wireguard container.

Traefik Configuration

No, I don’t use TOML.
I’ve intentionally left out things like security hardening, middlewares, and other extras, because that would quickly blow this post out of scope. The focus here is getting the tunnel and routing up and running – clean, simple, and functional.

##########
# Static Configuration
# ./traefik/traefik.yml
##########
---
entryPoints:
  web:
    address: ":80"
    http3: {}
    http:
      redirections:
        entryPoint:
          to: "websecure"
          scheme: "https"
          permanent: true
  websecure:
    address: ":443"
    http3: {}
    http:
      tls:
        certResolver: letsencrypt

providers:
  docker:
    exposedByDefault: false
  file:
    filename: "/etc/traefik/dynamic.yml"
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: "webmaster@mydomain.de"
      storage: "/etc/traefik/letsencrypt/acme.json"
      tlsChallenge: {}

log:
  level: INFO

💡 Replace webmaster@mydomain.de with your own webmaster mail address.

##########
# Dynamic Configuration
# ./traefik/dynamic.yml
##########
---
http:
  routers:
    myservice-https:
      entryPoints:
        - websecure
      rule: "Host(`mydomain.de`)"
      service: myservice
      tls: {}

  services:
    myservice:
      loadBalancer:
        servers:
          # name of the wireguard container and
          # the listening port you've configured in socat
          - url: "http://wireguard:3000"

💡 Replace mydomain.de with your own webmaster mail address. This configuration can be changed without restarting traefik.

docker compose up -d -f docker-compose.traefik.yml

💡 Start your compose in background (detached, -d).


Testing

Once everything is up and running, it’s time for a quick sanity check!

  1. Most obvious one: try to access your domain – you should be able to access your service. It works? Great, you are done, no need for further testing! 🎉
  2. Check container logs.
    1. List all containers: docker ps -a
    2. Show container logs: docker logs <container-name>
  3. Try to access yout service from inside the traefik container.
    1. docker exec -it <traefik-container-name> ash
    2. curl -ILkv http://wireguard:3000

Final Thoughts

This setup feels right and good for me, it allows me to expose private services on-demand through a trusted domain, without ever opening a single port on my home router. That said, it does come with responsibility:

  • Monitor your tunnel endpoints.
  • Limit which services you expose.
  • Keep WireGuard and Traefik updated.
  • Don’t forget to audit your DNS entries occasionally.

Some might ask, “Why not just use a Cloudflare Tunnel?”. And sure – Cloudflare Tunnel does offer convenient features like a web UI, DDoS protection and a lot more. But here’s the thing: it’s another dependency and not fully in my hands. I prefer to build and maintain my own stack – not just because it’s fun (well, most of the time), but because it forces me to understand my environment inside and out.

With just a bit of effort, you can create your own private expressway from your VPS to your LAN. I call mine "Standleitung" – and yes, it’s as fast as the name suggests.