8.1 KiB
Project "selfhosted2", 23may2026
Technical environment
Cross-development environment for prototyping a residential application server behind a VPS-style reverse proxy.
hector: The development machine. ASUS laptop running Linux Mint Mate. The machine on which this LLM session takes place (Session ID: ses_21ab382baffej8OpnTkrmaG5UA).
agge: Target machine acting as the 'Q Server'. Dell Optiplex mini PC running a headless Ubuntu Server. This is where the production apps reside. LAN IP 192.168.1.188, WireGuard IP 10.0.0.2.
raspen: Target machine acting as a VPS, with a reverse proxy routing all external traffic to applications on agge over an encrypted WireGuard tunnel. Raspberry Pi 5, running a headless Raspberry Pi OS Lite (64-bit). LAN IP 192.168.1.187, WireGuard IP 10.0.0.1.
The machines are connected through a dedicated local wifi hotspot, SSID NR-24. In the production deployment, raspen will be replaced by a cloud VPS with a public IP; the Docker compose and nginx configuration are designed to be portable as-is.
Architecture
hector
│
└──→ raspen.home (192.168.1.187) ← nginx reverse proxy (Docker)
│ wireguard sidecar (Docker)
│ wg0: 10.0.0.1/30
│
│ ────────── WireGuard tunnel ──────────
│
│ wg0: 10.0.0.2/30
│ Docker: Gitea, Nextcloud, PostgreSQL
└──→ agge (192.168.1.188)
Raspen is the single entry point for all traffic. Nginx terminates SSL and proxies requests through the encrypted WireGuard tunnel to agge. No services are exposed on the plain LAN — Docker containers on agge bind exclusively to 10.0.0.2.
Network
Hosts file on hector (/etc/hosts):
192.168.1.187 raspen.home nc.home git.home pg.home
192.168.1.188 agge
Only raspen.home's IP is published to clients. agge has no direct domain mapping in the hosts file (it is addressed only by its WireGuard IP).
WireGuard VPN
| Peer | Interface | IP | Port |
|---|---|---|---|
| raspen | wg0 | 10.0.0.1/30 | ephemeral (outbound) |
| agge | wg0 | 10.0.0.2/30 | 51820/udp |
Raspen runs WireGuard inside a Docker sidecar container (linuxserver/wireguard). Agge runs WireGuard as a native systemd service (wg-quick@wg0). UFW on agge allows UDP 51820 inbound.
Raspen's config (repo: vps/wireguard/wg_confs/wg0.conf):
- Connects to
192.168.1.188:51820 - Keepalive 25s
- Only routes
10.0.0.2/32through the tunnel
Agge's config (/etc/wireguard/wg0.conf):
- Listens on port 51820
- Only accepts traffic from
10.0.0.1/32
Applications
VPS (raspen) — Docker stack
Location: vps/docker-compose.yml
Two containers sharing a network namespace:
- wireguard: Creates the VPN tunnel. Publishes ports 80, 443, 5432, 2222, and 51820/udp.
- nginx: Reverse proxy. Shares wireguard's network namespace (
network_mode: "service:wireguard"). Proxies to agge via WireGuard IPs.
Nginx config (vps/nginx/conf.d/default.conf):
nc.home:443→10.0.0.2:8080(Nextcloud)git.home:443→10.0.0.2:3000(Gitea)raspen.home:80/443→ static page- HTTP redirects to HTTPS
- Self-signed SSL certs at
vps/ssl/
TCP stream proxies (vps/nginx/stream.d/):
pg.home:5432→10.0.0.2:5432(PostgreSQL)git.home:2222→10.0.0.2:2222(Gitea SSH)
Backend (agge) — Docker stack
Location: backend/docker-compose.yml
Four containers on an internal backend bridge network:
| Container | Image | Ports (bound to 10.0.0.2) | Purpose |
|---|---|---|---|
| postgres | postgres:16-alpine | — (internal) | Database for Nextcloud |
| nextcloud | nextcloud:latest | 8080:80 | File sync & share |
| gitea | gitea/gitea:latest | 3000:3000 (HTTP), 2222:22 (SSH) | Git hosting |
| postgres_remote | postgres:16-alpine | 5432:5432 | Remote-access DB |
All published ports bind to 10.0.0.2 only, making them inaccessible over the plain LAN — traffic must arrive via the WireGuard tunnel.
Environment variables are loaded from .env (repo root).
Development
Working directory: /home/allan/Work/selfhosted2, a Git repo hosted on Gitea at agge. Two remote URLs are configured:
https://git.home/scoot/selfhosted2.git(via raspen proxy — useful for read-only with self-signed cert)ssh://git@git.home:2222/scoot/selfhosted2.git(via raspen stream proxy — used for push)
Remote access:
raspen:rasput@192.168.1.187agge:tebarbi@192.168.1.188
opencode configuration (opencode.json):
- Permission model asks before executing bash or accessing external directories.
Password management
Secrets are managed through a .env.example pattern:
.env.example(tracked in git) — template with placeholder values documenting all required variables..env(gitignored) — the actual secrets file on hector. A copy lives at~/selfhosted2/.envon agge.- Docker volumes — runtime copies of passwords baked into PostgreSQL and Nextcloud config on first deploy.
Password sources:
| Variable | User | Where it's used | Where it's stored |
|---|---|---|---|
POSTGRES_PASSWORD |
nextcloud |
Internal PostgreSQL (DB owner) | .env, Docker volume |
| — | oc_admin |
Nextcloud app DB user | Nextcloud config.php |
PG_PASSWORD |
remoteuser |
Remote PostgreSQL | .env, Docker volume |
NEXTCLOUD_ADMIN_PASSWORD |
admin |
Nextcloud web login | .env, personal password manager |
To rotate a password:
- Generate:
openssl rand -base64 18 - Update
.envand sync to agge - Run
ALTER USER <user> WITH PASSWORD '<new>';in the appropriate PostgreSQL container - If rotating the Nextcloud app user, also update
config.phpviased - Restart the service if needed
Do not commit .env to git. The .env.example template is the only password-related file that belongs in the repo.
Deploy-time only
The .env file is read only during docker compose up (container create/recreate). At runtime, secrets live in Docker volumes and container env vars. Options for handling .env on the target machine:
| Option | Security | Convenience |
|---|---|---|
Keep with chmod 600 (current) |
Root on agge can still read it — but root already owns the volumes | Compose works immediately |
| Remove after deploy | Minimal disk exposure | Must scp from hector before any docker compose down && up |
Deploy script (deploy.sh) |
.env lives only on hector, pushed transiently during deploy |
Single command, cleanest for production |
For the prototype, chmod 600 is fine. For a production VPS, a deploy script is better — the .env never sits on disk permanently.
Session history
Session ses_21ab382baffej8OpnTkrmaG5UA — initial prototype deployment: Docker files written, containers configured, applications deployed and verified.
Post-migration recovery (this session):
- Hector was rebuilt from Omarchy Linux to Linux Mint Mate, losing the local work directory and self-signed certs.
- The remote Gitea repo on agge was intact; the local repo was recovered via
GIT_SSL_NO_VERIFY=1 git fetchover HTTPS (self-signed cert). /etc/hostswas corrected — all domains now point to raspen (192.168.1.187) as the single entry point.- A WireGuard VPN was established between raspen (Docker sidecar) and agge (native systemd service).
- Nginx upstream targets were changed from
192.168.1.188to10.0.0.2. - Docker port bindings on agge were restricted to
10.0.0.2to enforce tunnel-only access. - UFW on agge was configured to allow WireGuard UDP 51820.
- Gitea SSH port 2222 was added to nginx's TCP stream proxies on raspen, enabling git push over SSH through the VPN tunnel.
- PostgreSQL passwords were rotated to strong values;
.envreplaced with.env.examplepattern and gitignored; password management documented.
All services confirmed operational: Gitea (HTTP 200), Nextcloud (HTTP 302), PostgreSQL (port open), and static page (HTTP 200).