Compare commits
8 Commits
01efd7188a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c13bcb3efa | |||
| 915cab05b9 | |||
| abcf231b91 | |||
| 55a8df8515 | |||
| 279aaf37d9 | |||
| a90002aefc | |||
| a84b1de3fe | |||
| 8b36c3c64b |
+3
-3
@@ -1,16 +1,16 @@
|
|||||||
# PostgreSQL (internal, for Nextcloud)
|
# PostgreSQL (internal, for Nextcloud)
|
||||||
POSTGRES_DB=nextcloud
|
POSTGRES_DB=nextcloud
|
||||||
POSTGRES_USER=nextcloud
|
POSTGRES_USER=nextcloud
|
||||||
POSTGRES_PASSWORD=CHANGE_THIS_PASSWORD
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
# Remote PostgreSQL (for external access)
|
# Remote PostgreSQL (for external access)
|
||||||
PG_DB=remotedb
|
PG_DB=remotedb
|
||||||
PG_USER=remoteuser
|
PG_USER=remoteuser
|
||||||
PG_PASSWORD=CHANGE_THIS_PASSWORD
|
PG_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
# Nextcloud
|
# Nextcloud
|
||||||
NEXTCLOUD_ADMIN_USER=admin
|
NEXTCLOUD_ADMIN_USER=admin
|
||||||
NEXTCLOUD_ADMIN_PASSWORD=CHANGE_THIS_PASSWORD
|
NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME
|
||||||
TRUSTED_DOMAINS=nc.home localhost 127.0.0.1
|
TRUSTED_DOMAINS=nc.home localhost 127.0.0.1
|
||||||
|
|
||||||
# Gitea
|
# Gitea
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
.session-*
|
||||||
|
session-*
|
||||||
|
*.swp
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/allan/Raspen/Raspxfer/AI-tx/23may2026-Q-server-prototype.md
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# selfhosted2
|
||||||
|
|
||||||
|
## Machines
|
||||||
|
|
||||||
|
| Name | Role | OS | LAN IP | WG IP | SSH user |
|
||||||
|
|------|------|----|--------|-------|----------|
|
||||||
|
| hector | Dev machine (this one) | Linux Mint Mate | — | — | — |
|
||||||
|
| agge | Backend server | Ubuntu Server | 192.168.1.188 | 10.0.0.2 | tebarbi |
|
||||||
|
| heron | VPS / reverse proxy | Ubuntu Server | 82.197.73.238 | 10.0.0.1 | qbert |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
All traffic goes through **heron** (single entry point, public IP `82.197.73.238`):
|
||||||
|
- nginx in Docker (`vps/docker-compose.yml`) terminates SSL (Let's Encrypt) and reverse proxies
|
||||||
|
- WireGuard sidecar container encrypts traffic to agge
|
||||||
|
- agge connects *out* to heron (agge is behind residential NAT, heron listens passively)
|
||||||
|
- All backend services bind to `10.0.0.2` only (tunnel-only access)
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Domain | Service | Backend | Via heron |
|
||||||
|
|--------|---------|---------|-----------|
|
||||||
|
| git.qmoln.se | Gitea | agge:10.0.0.2:3000 (HTTP), :2222 (SSH) | HTTPS :443, TCP :2222 |
|
||||||
|
| nc.qmoln.se | Nextcloud | agge:10.0.0.2:8080 | HTTPS :443 |
|
||||||
|
| pg.qmoln.se | PostgreSQL | agge:10.0.0.2:5432 | TCP :5432 |
|
||||||
|
| qmoln.se | Static page | Served directly by nginx | HTTP/HTTPS |
|
||||||
|
|
||||||
|
## Repo structure
|
||||||
|
|
||||||
|
- `backend/docker-compose.yml` — agge stack (postgres, nextcloud, gitea, postgres_remote)
|
||||||
|
- `vps/docker-compose.yml` — heron stack (wireguard sidecar + nginx)
|
||||||
|
- `vps/nginx/conf.d/` — HTTP/HTTPS proxy configs
|
||||||
|
- `vps/nginx/stream.d/` — TCP stream proxy configs (postgres, gitea ssh)
|
||||||
|
- `vps/wireguard/wg_confs/` — WireGuard tunnel config
|
||||||
|
- `.env.example` — template for secrets (real `.env` is gitignored)
|
||||||
|
|
||||||
|
## WireGuard keys
|
||||||
|
|
||||||
|
- agge public key: `02k4BaH3iZTQnPZe7zifcaS9n8xxrwCLyIOLTBWLdgk=`
|
||||||
|
- heron public key: `4BtJlUWOzBtvrRu3llQbD0GPvlXgTwLq79iBth3uOSo=`
|
||||||
|
- agge connects out to heron at `82.197.73.238:51820`
|
||||||
|
- heron's WG private key is in `vps/wireguard/wg_confs/wg0.conf`
|
||||||
|
|
||||||
|
## Git remote (via tunnel — only reachable when tunnel is up)
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh://git@git.qmoln.se:2222/scoot/selfhosted2.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- The machines `agge` and `heron` require `sudo` for most commands. Do **not** attempt to execute commands requiring sudo via SSH — present the command to the user and let them run it manually.
|
||||||
|
|
||||||
|
## Password management
|
||||||
|
|
||||||
|
See `.env.example` for required variables. Real `.env` lives on hector and a copy on agge (`chmod 600`). To rotate: generate with `openssl rand -base64 18`, update `.env`, run `ALTER USER` in the container.
|
||||||
+6
-5
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"permission": {
|
"instructions": ["AGENTS.md"],
|
||||||
"bash": "ask",
|
"permission": {
|
||||||
"external_directory": "ask"
|
"bash": "ask",
|
||||||
}
|
"external_directory": "ask"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ services:
|
|||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
- ./nginx/stream.d:/etc/nginx/stream.d:ro
|
- ./nginx/stream.d:/etc/nginx/stream.d:ro
|
||||||
- ./ssl:/etc/nginx/certs:ro
|
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||||
- ./html:/usr/share/nginx/html:ro
|
- ./html:/usr/share/nginx/html:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ upstream gitea {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name nc.home;
|
server_name nc.qmoln.se;
|
||||||
return 301 https://nc.home$request_uri;
|
return 301 https://nc.qmoln.se$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name git.home;
|
server_name git.qmoln.se;
|
||||||
return 301 https://git.home$request_uri;
|
return 301 https://git.qmoln.se$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name raspen.home;
|
server_name qmoln.se;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name nc.home;
|
server_name nc.qmoln.se;
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/ssl.crt;
|
ssl_certificate /etc/letsencrypt/live/qmoln.se/fullchain.pem;
|
||||||
ssl_certificate_key /etc/nginx/certs/ssl.key;
|
ssl_certificate_key /etc/letsencrypt/live/qmoln.se/privkey.pem;
|
||||||
|
|
||||||
client_max_body_size 10G;
|
client_max_body_size 10G;
|
||||||
client_body_timeout 3600s;
|
client_body_timeout 3600s;
|
||||||
@@ -63,10 +63,10 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name git.home;
|
server_name git.qmoln.se;
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/ssl.crt;
|
ssl_certificate /etc/letsencrypt/live/qmoln.se/fullchain.pem;
|
||||||
ssl_certificate_key /etc/nginx/certs/ssl.key;
|
ssl_certificate_key /etc/letsencrypt/live/qmoln.se/privkey.pem;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://gitea;
|
proxy_pass http://gitea;
|
||||||
@@ -82,10 +82,10 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name raspen.home;
|
server_name qmoln.se;
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/ssl.crt;
|
ssl_certificate /etc/letsencrypt/live/qmoln.se/fullchain.pem;
|
||||||
ssl_certificate_key /etc/nginx/certs/ssl.key;
|
ssl_certificate_key /etc/letsencrypt/live/qmoln.se/privkey.pem;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[Interface]
|
[Interface]
|
||||||
Address = 10.0.0.1/30
|
Address = 10.0.0.1/30
|
||||||
PrivateKey = 0Junydsr+YBVFgkHbDEEmWAXAhR7JCpSWyT1yzSzjFU=
|
PrivateKey = EFp3S6XsMQEEM8o6KJBNv5gybTfS28xnO/XwWSLue2k=
|
||||||
|
ListenPort = 51820
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = 02k4BaH3iZTQnPZe7zifcaS9n8xxrwCLyIOLTBWLdgk=
|
PublicKey = 02k4BaH3iZTQnPZe7zifcaS9n8xxrwCLyIOLTBWLdgk=
|
||||||
Endpoint = 192.168.1.188:51820
|
|
||||||
AllowedIPs = 10.0.0.2/32
|
AllowedIPs = 10.0.0.2/32
|
||||||
PersistentKeepalive = 25
|
PersistentKeepalive = 25
|
||||||
|
|||||||
Reference in New Issue
Block a user