Homelab: exposer ses services homelab avec WireGuard, Traefik et Let’s Encrypt
Dans mon homelab, je fais tourner plusieurs applications (comme Immich) sur un simple PC derrière une connexion FAI résidentielle avec IP dynamique. Je ne veux pas exposer mes services directement sur Internet, mais je souhaite quand même y accéder de manière sécurisée depuis mon téléphone ou en déplacement.
Ma solution repose sur un principe simple : un seul port ouvert sur mon routeur, celui du VPN WireGuard.
Prérequis
- Un nom de domaine
- Un registrar DNS avec API (OVH dans mon cas, mais n’importe lequel peut fonctionner)
- Le routeur internet configuré pour autoriser le forwarding du traffic VPN. Une seule règle suffit :
- Protocole : UDP
- Port externe : 52420
- IP interne : Votre serveur homelab
- Port interne : 51820
- et docker compose pour faire simple
Principe de base
Depuis l’extérieur (via mobile):
- Client se connecte au VPN (vpn.myhomelab.tld:58237)
- Client obtient une IP 10.13.13.X et utilise DNS 172.16.0.253
- Client demande par exemple immich.myhomelab.tld
- dnsdist résout vers l’IP locale de Traefik
- Connexion HTTPS vers Traefik (certificat Let’s Encrypt)
- Traefik proxifie vers réseau interne docker (immich:2283)
Internet
│
│ Port UDP de WireGuard uniquement
▼
┌─────────────────────────────────────┐
│ Router (NAT) │
└─────────────────────────────────────┘
│
│ Réseau local 172.16.0.0/12
▼
┌──────────────────────────────────────────────────────────────────┐
│ Homelab Server (172.16.0.0/12) │
│ │
│ ┌────────────────┐ ┌────────────────────┐ ┌─────────────────┐ │
│ │ publicaddr │ │ WireGuard │ │ dnsdist │ │
│ └────────────────┘ └────────────────────┘ └─────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Traefik (reverse proxy) │ │
│ │ │ │
│ │ • Écoute sur ports 80/443 (local uniquement) │ │
│ │ • Génère certificats Let's Encrypt via DNS-01 │ │
│ │ • Auto-découverte via labels Docker │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Services (Immich, Vault,etc...) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Les composants clés
WireGuard
Dans mon setup, WireGuard :
- Crée un réseau privé 10.13.13.0/24 pour les clients
- Pousse le DNS 172.16.0.253 vers les clients
- Route uniquement le trafic vers 172.16.0.0/12 (split-tunneling)
- Maintient les connexions avec keepalive
L’image Docker linuxserver/wireguard génère automatiquement les configurations pour chaque peer (device).
PublicADDR
Ne disposant pas d’un IP publique stable, j’utilise publicaddr-ovhcloud pour que vpn.myhomelab.tld pointe toujours vers la bonne adresse,
Ce container :
- Détecte l’IP publique toutes les 5 minutes
- Compare avec l’IP enregistrée chez OVH
- Met à jour les enregistrements DNS si changement
- Supporte IPv4 et IPv6
- Gère plusieurs sous-domaines
C’est essentiellement un client DynDNS qui utilise l’API OVH.
https://github.com/dmachard/docker-publicaddr-ovhcloud https://github.com/dmachard/python-publicaddr
DnsDIST
Les clients VPN doivent résoudre immich.monhomelab.tld vers une IP locale (172.16.0.X) et non vers l’IP publique. C’est le principe du split-brain DNS.
dnsdist (port 53) :
- Point d’entrée pour toutes les requêtes DNS
- Filtrage de publicités via blocklists sur mobile
- Logging vers dnscollector
- Forwarding vers divers DNS (google, quaddns, etc…)
dnscollector : https://github.com/dmachard/DNS-collector
- Collecte et agrège les logs DNS
- Statistiques et monitoring
Traefik
Traefik sert de point d’entrée unique pour tous mes services web.
Son rôle :
- Routage : Dirige les requêtes vers le bon service selon le domaine
- TLS termination : Gère le HTTPS et les certificats
- Auto-configuration : Détecte les nouveaux services Docker via labels
- ACME automatique : Génère et renouvelle les certificats Let’s Encrypt
Utilisation du challenge DNS-01
Le challenge HTTP-01 classique nécessite que Let’s Encrypt puisse accéder à votre serveur sur le port 80 depuis Internet. Ça impliquerait d’exposer Traefik publiquement.
Le challenge DNS-01 fonctionne différemment :
- Traefik demande un certificat pour
immich.monhomelab.tld - Let’s Encrypt demande de prouver le contrôle du domaine
- Traefik crée un enregistrement TXT via l’API OVH :
_acme-challenge.monhomelab.tld - Let’s Encrypt vérifie l’enregistrement DNS
- Certificat émis !
Mise en oeuvre avec docker
Arborescence
homelab/ ├── corelab/ │ ├── docker-compose.yml │ └── .env │ ├── observe/ │ ├── docker-compose.yml │ └── .env │ ├── apps/ │ ├── docker-compose.yml │ └── .env
docker network create --driver bridge --subnet 172.25.0.0/16 traefik_proxy
docker network create vpn_network
docker compose
Tout est défini dans un seul fichier docker-compose.yml et un fichier .env:
networks:
vpn:
external: true
traefik:
external: true
services:
# ============================================================
# publicaddr : DynDNS automatique pour OVH
# ============================================================
publicaddr_ovhcloud:
image: dmachard/publicaddr-ovhcloud:${PUBLICADDR_OVHCLOUD_VERSION}
container_name: publicaddr
env_file:
- .env
environment:
PUBLICADDR_OVHCLOUD_UPDATE: ${PUBLICADDR_REFRESH}
PUBLICADDR_OVHCLOUD_DEBUG: ${PUBLICADDR_DEBUG}
PUBLICADDR_OVHCLOUD_HAS_IPV6: ${PUBLICADDR_HAS_IPV6}
PUBLICADDR_OVHCLOUD_ZONE: ${PUBLICADDR_ZONE}
PUBLICADDR_OVHCLOUD_SUBDOMAINS: ${PUBLICADDR_SUBDOMAINS}
PUBLICADDR_OVHCLOUD_ENDPOINT: ${OVH_EP}
PUBLICADDR_OVHCLOUD_APPLICATION_KEY: ${OVH_AK}
PUBLICADDR_OVHCLOUD_APPLICATION_SECRET: ${OVH_AS}
PUBLICADDR_OVHCLOUD_CONSUMER_KEY: ${OVH_CK}
restart: unless-stopped
# ============================================================
# dnscollector : Collecte des logs DNS (optionnel)
# ============================================================
dnscollector:
image: dmachard/dnscollector:${DNSCOLLECTOR_VERSION}
restart: unless-stopped
container_name: dnscollector
network_mode: host
volumes:
- /etc/localtime:/etc/localtime:ro
- ./dnscollector/config.yml:/etc/dnscollector/config.yml
- ./dnstap:/var/dnscollector/
# ============================================================
# blocklist_updater : Télécharge les listes de domaines à bloquer
# ============================================================
blocklist_updater:
image: alpine:latest
container_name: blocklist_updater
volumes:
- ./dnsdist/:/config/
restart: always
command: >
sh -c "
apk add --no-cache curl &&
echo '0 1 * * * curl -fsSL -o /config/blocklist.cdb https://raw.githubusercontent.com/dmachard/blocklist-domains/data/blocklist.cdb' > /etc/crontabs/root &&
curl -fsSL -o /config/blocklist.cdb https://raw.githubusercontent.com/dmachard/blocklist-domains/data/blocklist.cdb &&
crond -f
"
# ============================================================
# dnsdist : Proxy DNS avec filtrage
# ============================================================
dnsdist:
image: powerdns/${DNSDIST_VERSION}
restart: unless-stopped
container_name: dnsdist
volumes:
- ./dnsdist/:/etc/dnsdist/conf.d/
network_mode: host
depends_on:
- dnscollector
- blocklist_updater
# ============================================================
# wireguard : Serveur VPN
# ============================================================
wireguard:
image: lscr.io/linuxserver/wireguard:${WIREGUARD_VERSION}
container_name: wireguard
cap_add:
- NET_ADMIN
env_file:
- .env
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- SERVERURL=${WIREGUARD_SERVERURL}
- SERVERPORT=${WIREGUARD_SERVERPORT}
- PEERS=${WIREGUARD_PEERS}
- PEERDNS=${WIREGUARD_PEERDNS}
- INTERNAL_SUBNET=${WIREGUARD_INTERNAL_SUBNET}
- ALLOWEDIPS=${WIREGUARD_ALLOWEDIPS}
- PERSISTENTKEEPALIVE_PEERS=all
- LOG_CONFS=true
volumes:
- ./wireguard:/config
- /lib/modules:/lib/modules
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
networks:
- vpn
restart: unless-stopped
# ============================================================
# Traefik : Reverse proxy
# ============================================================
traefik:
image: traefik:${TRAEFIK_VERSION}
container_name: traefik
ports:
- 443:443/tcp
networks:
- traefik
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.traefik-dashboard.entrypoints=websecure
- traefik.http.routers.traefik-dashboard.rule=Host(`traefik.myhomelab.tld`)
- traefik.http.routers.traefik-dashboard.service=api@internal
- traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/data
env_file:
- .env
environment:
- "OVH_ENDPOINT="${OVH_EP}"
- "OVH_APPLICATION_KEY="${OVH_AK}"
- "OVH_APPLICATION_SECRET="${OVH_AS}"
- "OVH_CONSUMER_KEY="${OVH_CK}"
command:
# See https://doc.traefik.io/traefik/providers/docker/
- --providers.docker=true
- --providers.docker.network=traefik
- --providers.docker.watch=true
- --providers.docker.exposedbydefault=false
# See https://doc.traefik.io/traefik/observability/logs/
- --log.filePath=/data/traefik.log
- --log.level=DEBUG
- --accesslog
# See https://doc.traefik.io/traefik/operations/api/
- --api=true
- --api.dashboard=true
Env file
# Software version to use
DNSCOLLECTOR_VERSION="dnsdist-20:v2.0.0"
DNSDIST_VERSION="v2.0.0"
TRAEFIK_VERSION="v3.6.7"
WIREGUARD_VERSION="1.0.20250521-r1-ls101"
PUBLICADDR_OVHCLOUD_VERSION="v0.10.0"
# Fréquence de mise à jour (en secondes)
PUBLICADDR_UPDATE: 300
PUBLICADDR_DEBUG: 0
PUBLICADDR_HAS_IPV6: 0
# domaine dns à mettre à jour
PUBLICADDR_ZONE: myhomelab.tld
PUBLICADDR_SUBDOMAINS: *.myhomelab.tld
# credentials ovh https://eu.api.ovh.com/createToken/
OVH_EP: ovh-eu
OVH_AK: votre_application_key
OVH_AS: votre_application_secret
OVH_CK: votre_consumer_key
# domaine et port du vpn
WIREGUARD_SERVERURL=vpn.myhomelab.tld
WIREGUARD_SERVERPORT=51820
# Liste des clients qui auront accès au VPN
WIREGUARD_PEERS=denis_mobile
# dns à utiliser par les clients
WIREGUARD_PEERDNS=172.16.0.253
# Réseau privé du VPN (10.13.13.0/24)
WIREGUARD_INTERNAL_SUBNET=10.13.13.0
# Plages IP routées via le VPN (split-tunneling)
WIREGUARD_ALLOWEDIPS=172.16.0.0/12
Le script download_blocklist.sh télécharge des listes de domaines publicitaires.
#!/bin/sh
curl -o /config/blocklist.cdb https://raw.githubusercontent.com/dmachard/blocklist-domains/data/blocklist.cdb
Configuration de dnsdist (dnsdist/dnsdist.yml) :
****
Conclusion
Cette configuration tourne de manière stable depuis plusieurs anénes sans intervention. Les certificats se renouvellent automatiquement, le DNS se met à jour tout seul, et les containers redémarrent en cas de reboot. Mon téléphone garde le VPN WireGuard activé en permanence. J’accède à mes photos via Immich comme si c’était un service cloud, mais tout reste chez moi avec un contrôle total.
