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

168 lines
8.1 KiB
Markdown

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: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.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.
### 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):
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).