511 lines
12 KiB
Markdown
511 lines
12 KiB
Markdown
# VHostLoom
|
||
|
||
A Docker Hosting Framework: Traefik + Authelia + Modular Sites
|
||
|
||
This repository provides a reusable framework for running multiple
|
||
virtual-hosted web services on a single server using:
|
||
|
||
- [Traefik](https://traefik.io/) as a reverse proxy and TLS terminator
|
||
- [Authelia](https://www.authelia.com/) as an SSO / authentication gateway
|
||
- Docker Compose for container orchestration
|
||
- Modular per-site stacks with bind-mounted volumes
|
||
|
||
The design goals are:
|
||
|
||
> **Add or change sites without constantly editing core proxy config**
|
||
> and keep application data available natively on the host filesystem.
|
||
|
||
Under the view that hacking is inevitable, it seemed to me that making
|
||
it so a hacker has to hack each container separately makes the system
|
||
less brittle and less exposed to threats like ransomware. With modest
|
||
backup discipline, this system should permit porting or getting back
|
||
up expeditiously.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- **Core proxy stack** (`core-proxy/`) with Traefik and Authelia
|
||
- Let’s Encrypt HTTP-01 certificates
|
||
- Traefik dashboard at `https://traefik.example.com`, protected by Authelia
|
||
- Authelia at `https://auth.example.com` for login
|
||
|
||
- **Modular site stacks** under `sites/`:
|
||
- `static-site`: static file hosting at `https://example.com`
|
||
- `wordpress-site`: WordPress at `https://example.com/wp`
|
||
- `forgejo`: Forgejo (Git hosting) + CI runner at `https://git.example.com`
|
||
- `nextcloud`: Nextcloud cloud storage at `https://cloud.example.com`
|
||
|
||
- **Security model**
|
||
- Default-deny firewall template (`firewall/nftables.conf.example`)
|
||
- Only 80/443 exposed publicly (and optionally a few others)
|
||
- Authelia used to put an extra auth layer in front of sensitive apps
|
||
- Easy integration with ZeroTier or other VPNs for “VPN-only” services
|
||
|
||
- **Host-friendly data layout**
|
||
- Each site’s data lives in bind-mounted directories under `sites/`
|
||
- Easy to back up, rsync, or inspect without entering containers
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
- Linux host (e.g., Ubuntu Server 22.04+)
|
||
- Docker and Docker Compose plugin installed
|
||
- A public IP address (static or effectively static)
|
||
- Control over DNS records for your domains
|
||
|
||
For HTTPS, you’ll need DNS `A` (and/or `AAAA`) records pointing to your server:
|
||
|
||
- `example.com` → server IP
|
||
- `auth.example.com` → server IP
|
||
- `traefik.example.com` → server IP
|
||
- `git.example.com` → server IP
|
||
- `cloud.example.com` → server IP
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
1. **Clone the repo**
|
||
|
||
```bash
|
||
git clone https://example.com/your/hosting-framework.git
|
||
cd hosting-framework
|
||
|
||
|
||
2. **Create shared Docker network**
|
||
|
||
```bash
|
||
docker network create traefik_proxy
|
||
```
|
||
|
||
3. **Prepare Traefik ACME storage**
|
||
|
||
```bash
|
||
cd core-proxy
|
||
mkdir -p traefik/dynamic
|
||
touch traefik/acme.json
|
||
chmod 600 traefik/acme.json
|
||
```
|
||
|
||
4. **Edit core config**
|
||
|
||
* `core-proxy/docker-compose.yml`
|
||
|
||
* Change `admin@example.com` to a real email
|
||
* Adjust domains in Traefik labels for your use case
|
||
* `core-proxy/traefik/dynamic/authelia.yml`
|
||
|
||
* Set `auth.example.com` (or equivalent)
|
||
* `core-proxy/authelia/configuration.yml`
|
||
|
||
* Change `example.com`, secrets, etc.
|
||
* `core-proxy/authelia/users_database.yml`
|
||
|
||
* Generate a password hash:
|
||
|
||
```bash
|
||
docker run --rm authelia/authelia:latest authelia hash-password 'yourpassword'
|
||
```
|
||
|
||
Paste the hash into `password:`.
|
||
|
||
5. **Start the core stack**
|
||
|
||
```bash
|
||
cd core-proxy
|
||
docker compose up -d
|
||
```
|
||
|
||
6. **Configure DNS**
|
||
|
||
Create DNS records pointing your domains to the server’s public IP.
|
||
Let’s Encrypt will fail if DNS is wrong or not propagated.
|
||
|
||
7. **Bring up example sites**
|
||
|
||
```bash
|
||
# Static site
|
||
cd ../sites/static-site
|
||
docker compose up -d
|
||
|
||
# WordPress
|
||
cd ../wordpress-site
|
||
docker compose up -d
|
||
|
||
# Forgejo
|
||
cd ../forgejo
|
||
docker compose up -d
|
||
|
||
# Nextcloud
|
||
cd ../nextcloud
|
||
docker compose up -d
|
||
```
|
||
|
||
8. **Test**
|
||
|
||
* `https://traefik.<your-domain>` → Authelia login → Traefik dashboard
|
||
* `https://auth.<your-domain>` → Authelia portal
|
||
* `https://example.com` → static site
|
||
* `https://example.com/wp` → WordPress installer
|
||
* `https://git.<your-domain>` → Authelia login → Forgejo setup
|
||
* `https://cloud.<your-domain>` → Authelia login → Nextcloud setup
|
||
|
||
---
|
||
|
||
## Rationale & Design
|
||
|
||
### Separation of concerns
|
||
|
||
* **Core proxy** (Traefik + Authelia) is stable and rarely changed.
|
||
* **Sites** live in separate directories with their own `docker-compose.yml`.
|
||
* **Firewall** is independent of Docker and enforces network boundaries.
|
||
|
||
This makes it easy to:
|
||
|
||
* Add new virtual hosts (just add a new `sites/<name>/docker-compose.yml`)
|
||
* Share the framework without embedding secrets
|
||
* Back up only what matters (`sites/**`, Authelia DB, maybe Traefik `acme.json`)
|
||
|
||
### Minimal coupling
|
||
|
||
The only shared assumptions between stacks:
|
||
|
||
* A Docker network named `traefik_proxy`
|
||
* Authelia’s forward-auth middleware named `authelia-auth@file`
|
||
* Traefik’s Let’s Encrypt resolver named `letsencrypt`
|
||
|
||
Everything else is per-site.
|
||
|
||
---
|
||
|
||
## Configuration Details
|
||
|
||
### Traefik labels
|
||
|
||
Each service defines its routing behavior entirely via labels:
|
||
|
||
* Match host and path:
|
||
`traefik.http.routers.<name>.rule=Host(`example.com`) && PathPrefix(`/wp`)`
|
||
* Bind to entrypoint:
|
||
`...entrypoints=web` or `websecure`
|
||
* Enable HTTPS / certificates:
|
||
`...tls.certresolver=letsencrypt`
|
||
* HTTP→HTTPS redirect using a middleware:
|
||
|
||
* Define middleware:
|
||
`traefik.http.middlewares.foo-https-redirect.redirectscheme.scheme=https`
|
||
* Attach it to an HTTP router:
|
||
`traefik.http.routers.foo-http.middlewares=foo-https-redirect`
|
||
|
||
### Authelia protection
|
||
|
||
To require login via Authelia before an app:
|
||
|
||
```yaml
|
||
labels:
|
||
- "traefik.http.routers.<name>-https.middlewares=authelia-auth@file"
|
||
```
|
||
|
||
Remove or comment this label to make a site public.
|
||
|
||
### Authelia users
|
||
|
||
Defined in `core-proxy/authelia/users_database.yml`:
|
||
|
||
```yaml
|
||
users:
|
||
admin:
|
||
displayname: "Admin User"
|
||
email: "admin@example.com"
|
||
groups: [admins]
|
||
password: "<argon2id hash>"
|
||
```
|
||
|
||
You can use groups and more complex access_control rules if desired; the default config in this repo simply treats any authenticated user as allowed.
|
||
|
||
---
|
||
|
||
## Security & Firewall
|
||
|
||
The `firewall/nftables.conf.example` file contains a default-deny firewall with:
|
||
|
||
* Loopback and established connections allowed
|
||
* ICMP allowed (optional but recommended)
|
||
* SSH allowed only over a VPN interface (e.g., ZeroTier)
|
||
* 80/443 open for Traefik
|
||
* Example ports restricted to the VPN interface
|
||
|
||
You can adapt it and enable with:
|
||
|
||
```bash
|
||
sudo cp firewall/nftables.conf.example /etc/nftables.conf
|
||
sudo nft -f /etc/nftables.conf
|
||
sudo systemctl enable nftables
|
||
```
|
||
|
||
Always test SSH access before locking down too far.
|
||
|
||
---
|
||
|
||
## Maintenance
|
||
|
||
### Updating containers
|
||
|
||
For each stack:
|
||
|
||
```bash
|
||
cd core-proxy
|
||
docker compose pull
|
||
docker compose up -d
|
||
|
||
cd ../sites/<site-name>
|
||
docker compose pull
|
||
docker compose up -d
|
||
```
|
||
|
||
### Backups
|
||
|
||
At minimum, back up:
|
||
|
||
* `core-proxy/traefik/acme.json` (certificates)
|
||
* `core-proxy/authelia/` (configuration + DB)
|
||
* `sites/**/` data directories:
|
||
|
||
* `sites/static-site/html/`
|
||
* `sites/wordpress-site/db/` and `wp/`
|
||
* `sites/forgejo/data/`, `db/`, `runner/`
|
||
* `sites/nextcloud/nextcloud/`, `db/`, `redis/`
|
||
|
||
Use `rsync`, `borg`, `restic`, or your favorite solution.
|
||
|
||
### Logs
|
||
|
||
* Traefik logs: stdout (use `docker logs traefik` or a log collector)
|
||
* Authelia logs: stdout + `notification.log` (if configured)
|
||
* Site logs: as exposed via each container
|
||
|
||
---
|
||
|
||
## Extending the Framework
|
||
|
||
To add a new site:
|
||
|
||
1. Create `sites/<new-site>/docker-compose.yml`.
|
||
2. Attach it to `traefik_proxy`.
|
||
3. Add Traefik labels for hostnames, HTTPS, redirect, and optionally Authelia.
|
||
4. Bind volumes for data so they live on the host.
|
||
5. `docker compose up -d` in that directory.
|
||
6. Add DNS records for the new hostname(s).
|
||
|
||
You never need to edit the core proxy configuration for new sites.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
This software is licensed under the MIT license.
|
||
|
||
## Disclaimer
|
||
|
||
Beyond the MIT license statement, this software was produced by
|
||
iterative prompting of OpenAI's GPT 5.1 LLM.
|
||
|
||
## Notes
|
||
|
||
For myself, I only want the core-proxy items on the boot drive.
|
||
The 'sites' directory I am sym-linking from a mount that has
|
||
much more disk space.
|
||
|
||
Also, Docker containers can be large. Consider sym-linking the
|
||
Docker image directory to a larger disk.
|
||
|
||
## Secure Private Access (WireGuard Module)
|
||
|
||
Some services in VHostLoom are **not public-facing** by design:
|
||
|
||
* Stable Diffusion interfaces
|
||
* Llamafile demos & research endpoints
|
||
* Ollama/vLLM
|
||
* Internal dashboards
|
||
* Forgejo SSH
|
||
* Anything experimental or non-web
|
||
|
||
To protect these services, VHostLoom supports an optional **WireGuard VPN module** that restricts private-service ports so they are reachable **only from authenticated VPN clients**, and *never* from the public Internet.
|
||
|
||
WireGuard may be used instead of, or alongside, ZeroTier.
|
||
|
||
---
|
||
|
||
### Why WireGuard?
|
||
|
||
WireGuard is:
|
||
|
||
* extremely fast (kernel-level cryptography)
|
||
* small and security-audited
|
||
* widely supported across platforms
|
||
* perfect for “private access only” services
|
||
|
||
VHostLoom’s WireGuard module:
|
||
|
||
* Exposes WireGuard only on WAN (`udp/51820`)
|
||
* Creates a private VPN subnet (`10.20.0.0/24`)
|
||
* Restricts critical ports to the WireGuard interface
|
||
* Leaves Traefik-managed public services untouched
|
||
|
||
---
|
||
|
||
## Installing WireGuard
|
||
|
||
Install on the server:
|
||
|
||
```bash
|
||
sudo apt install wireguard wireguard-tools
|
||
```
|
||
|
||
Copy in the example config:
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/wireguard
|
||
sudo cp wireguard/wg0.conf.example /etc/wireguard/wg0.conf
|
||
sudo chmod 600 /etc/wireguard/wg0.conf
|
||
```
|
||
|
||
Generate server keypair:
|
||
|
||
```bash
|
||
wg genkey | tee server.key | wg pubkey > server.pub
|
||
```
|
||
|
||
Add the server private key to:
|
||
|
||
```ini
|
||
PrivateKey = <SERVER_PRIVATE_KEY>
|
||
```
|
||
|
||
---
|
||
|
||
## Starting WireGuard
|
||
|
||
```bash
|
||
sudo systemctl enable wg-quick@wg0
|
||
sudo systemctl start wg-quick@wg0
|
||
```
|
||
|
||
Verify:
|
||
|
||
```bash
|
||
ip addr show wg0
|
||
wg show
|
||
```
|
||
|
||
---
|
||
|
||
## Firewall Configuration
|
||
|
||
Choose one:
|
||
|
||
* **`firewall/nftables-wireguard.conf.example`** — basic WireGuard + Traefik + private-services setup
|
||
* **`firewall/nftables-wireguard-zt.conf.example`** — *combined* WireGuard + ZeroTier rules
|
||
|
||
Install a firewall:
|
||
|
||
```bash
|
||
sudo cp firewall/nftables-wireguard.conf.example /etc/nftables.conf
|
||
sudo nft -f /etc/nftables.conf
|
||
sudo systemctl enable nftables
|
||
```
|
||
|
||
This:
|
||
|
||
* Allows public web (80/443)
|
||
* Accepts WireGuard on WAN (UDP 51820)
|
||
* Allows private services (**only** via wg0)
|
||
* Default-denies everything else
|
||
|
||
---
|
||
|
||
## Adding a New WireGuard Client
|
||
|
||
Use the helper script:
|
||
|
||
```bash
|
||
wireguard/gen-wg-peer.sh clientname
|
||
```
|
||
|
||
It will generate:
|
||
|
||
* `clientname.key`
|
||
* `clientname.pub`
|
||
* `clientname.conf` (the client config)
|
||
|
||
Add the peer entry to `/etc/wireguard/wg0.conf` **automatically**.
|
||
|
||
Then send the generated `.conf` file to your device.
|
||
|
||
---
|
||
|
||
## Client Template
|
||
|
||
Clients use a simple config:
|
||
|
||
```ini
|
||
[Interface]
|
||
PrivateKey = <CLIENT_PRIVATE_KEY>
|
||
Address = 10.20.0.X/32
|
||
DNS = 1.1.1.1
|
||
|
||
[Peer]
|
||
PublicKey = <SERVER_PUBLIC_KEY>
|
||
Endpoint = <PUBLIC_IP>:51820
|
||
AllowedIPs = 10.20.0.0/24
|
||
PersistentKeepalive = 25
|
||
```
|
||
|
||
Import into:
|
||
|
||
* WireGuard app (iOS/Android)
|
||
* `wg-quick`
|
||
* Desktop clients
|
||
|
||
---
|
||
|
||
## Coexisting with ZeroTier
|
||
|
||
If you want both overlay networks:
|
||
|
||
* ZeroTier mesh connections
|
||
* WireGuard direct VPN
|
||
* Shared access control for private ports
|
||
|
||
Use:
|
||
|
||
```
|
||
firewall/nftables-wireguard-zt.conf.example
|
||
```
|
||
|
||
It grants private-port access to **either**:
|
||
|
||
* WireGuard (`wg0`)
|
||
* ZeroTier (`zt*`)
|
||
|
||
You can also restrict different services to different VPNs.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
WireGuard gives VHostLoom:
|
||
|
||
* Strong isolation of private services
|
||
* Minimal attack surface
|
||
* Predictable firewalling
|
||
* Fast, encrypted access
|
||
|
||
It integrates fully with the project’s model:
|
||
|
||
* **Traefik/Authelia** for public-facing authenticated web
|
||
* **WireGuard** (and/or ZeroTier) for *non-web private services*
|
||
|