Files
selfhosted2/23may2026-Q-server-prototype.md
T

7.3 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/32 through 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:44310.0.0.2:8080 (Nextcloud)
  • git.home:44310.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:543210.0.0.2:5432 (PostgreSQL)
  • git.home:222210.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.187
  • agge: 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/.env on 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:

  1. Generate: openssl rand -base64 18
  2. Update .env and sync to agge
  3. Run ALTER USER <user> WITH PASSWORD '<new>'; in the appropriate PostgreSQL container
  4. If rotating the Nextcloud app user, also update config.php via sed
  5. 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.

Session history

Session ses_21ab382baffej8OpnTkrmaG5UA — initial prototype deployment: Docker files written, containers configured, applications deployed and verified.

Post-migration recovery (this session):

  1. Hector was rebuilt from Omarchy Linux to Linux Mint Mate, losing the local work directory and self-signed certs.
  2. The remote Gitea repo on agge was intact; the local repo was recovered via GIT_SSL_NO_VERIFY=1 git fetch over HTTPS (self-signed cert).
  3. /etc/hosts was corrected — all domains now point to raspen (192.168.1.187) as the single entry point.
  4. A WireGuard VPN was established between raspen (Docker sidecar) and agge (native systemd service).
  5. Nginx upstream targets were changed from 192.168.1.188 to 10.0.0.2.
  6. Docker port bindings on agge were restricted to 10.0.0.2 to enforce tunnel-only access.
  7. UFW on agge was configured to allow WireGuard UDP 51820.
  8. Gitea SSH port 2222 was added to nginx's TCP stream proxies on raspen, enabling git push over SSH through the VPN tunnel.
  9. PostgreSQL passwords were rotated to strong values; .env replaced with .env.example pattern and gitignored; password management documented.

All services confirmed operational: Gitea (HTTP 200), Nextcloud (HTTP 302), PostgreSQL (port open), and static page (HTTP 200).