From fbf9e7501d82210fd079957f3a9445d963e17e8d Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Thu, 9 Jul 2026 17:29:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(gokapi):=20config=20publicznego=20file-sha?= =?UTF-8?q?re=20na=20VPS=20(share.okit.pl,=20E2E,=20Tailscale-bind=20za=20?= =?UTF-8?q?npm@VPS,=20disk-protection)=20=E2=80=94=20do=20deployu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nextcloud zostaje prywatny (mesh/kapala.org); Gokapi to osobny publiczny serwis do wysylania linkow do plikow na zewnatrz (Firefox Send alt). owner_node=vps, storage lokalny dysk (nie S3), E2E encryption ON, port 53842 bindowany tylko na TAILSCALE_BIND_IP (hairpin NAT przez npm@VPS, nigdy 0.0.0.0). Cutover checklist w README (DNS, wildcard *.okit.pl na npm@VPS, setup wizard) — nie zdeployowane w tym commicie. Co-Authored-By: Claude Sonnet 5 --- hosts/vps/services.yaml | 17 +++++ inventory/topology.yaml | 2 + services/gokapi/README.md | 119 +++++++++++++++++++++++++++++ services/gokapi/docker-compose.yml | 43 +++++++++++ services/gokapi/env.example | 47 ++++++++++++ services/gokapi/healthcheck.sh | 27 +++++++ services/gokapi/service.yaml | 34 +++++++++ 7 files changed, 289 insertions(+) create mode 100644 services/gokapi/README.md create mode 100644 services/gokapi/docker-compose.yml create mode 100644 services/gokapi/env.example create mode 100755 services/gokapi/healthcheck.sh create mode 100644 services/gokapi/service.yaml diff --git a/hosts/vps/services.yaml b/hosts/vps/services.yaml index ebfd697..186465d 100644 --- a/hosts/vps/services.yaml +++ b/hosts/vps/services.yaml @@ -59,3 +59,20 @@ services: config_path: /opt/homelab/config/fleet-prometheus data_path: /opt/homelab/data/fleet-prometheus logs_path: /opt/homelab/logs/fleet-prometheus + + gokapi: + role: public-file-share + deployment_model: docker-compose + exposure: public + offline_required: false + depends_on: + local: [] + external: [] + ports: + - name: http + container_port: 53842 + protocol: tcp + runtime: + config_path: /opt/homelab/config/gokapi + data_path: /opt/homelab/data/gokapi + logs_path: /opt/homelab/logs/gokapi diff --git a/inventory/topology.yaml b/inventory/topology.yaml index e1f88da..83dffe0 100644 --- a/inventory/topology.yaml +++ b/inventory/topology.yaml @@ -74,6 +74,8 @@ nodes: - joplin # Note sync server (joplin-server + postgres) - ai-cluster # AI workers: codex-worker, openclaw, planner-worker, # service-ops-worker, redis, mosquitto + - gokapi # Public file-share (Firefox Send alt), share.okit.pl via npm@VPS, + # local disk storage, E2E encryption ON — separate from private Nextcloud chelsty-infra: site: chelsty diff --git a/services/gokapi/README.md b/services/gokapi/README.md new file mode 100644 index 0000000..7083303 --- /dev/null +++ b/services/gokapi/README.md @@ -0,0 +1,119 @@ +# Gokapi (publiczne udostępnianie plików) + +Lekki self-hosted "Firefox Send" alternative — link do jednego pliku, na +zewnątrz, z limitem pobrań/czasu. **Osobny serwis od Nextclouda, celowo.** + +| | Nextcloud | Gokapi | +|---|---|---| +| Rola | twierdza — prywatna, mesh/`kapala.org` | publiczne dzielenie — `okit.pl` | +| Dostęp | Tailscale/LAN only, zero public ingress | publiczny internet, przez npm@VPS | +| Host | PIHA | **VPS** (Hetzner, publiczny) | +| Użycie | sync telefon/desktop, KB source | "wyślij komuś link do pliku" | + +Host: **VPS** (Hetzner, `135.181.153.108`) — publiczny serwis na publicznym +hoście, węzły domowe nietknięte. `owner_node: vps`. + +## Stack + +| Kontener | Obraz | Rola | +|---|---|---| +| `gokapi` | `f0rc3/gokapi:v2.2.4` | cała aplikacja (Go binary + wbudowana baza SQLite), port 53842 | + +Wersja przypięta na `v2.2.4` (aktualny stable na 2026-07-09, zweryfikowane +przez github.com/Forceu/Gokapi releases + tagi `f0rc3/gokapi` na Docker +Hub). **TODO PRZY DEPLOYU**: potwierdzić, czy nie wyszła nowsza wersja, +zanim `docker compose up`. + +## Storage — lokalny dysk VPS (nie S3) + +Decyzja Oskara: bez S3, dysk lokalny VPS. VPS ma tylko 80 GB SSD dzielone z +npm/outline/joplin/ai-cluster/fleet-prometheus — więc **ograniczenia +rozmiaru + brak globalnego domyślnego expiry to jedyna ochrona dysku**: + +- `GOKAPI_MAX_FILESIZE=5120` (5 GB/plik; upstream default to 100 GB — za + dużo na ten dysk). +- `GOKAPI_MIN_FREE_SPACE=2048` (2 GB headroom zanim Gokapi odmówi uploadu; + upstream default 400 MB, za mało przy dzielonym dysku). +- **Gokapi NIE MA globalnego domyślnego expiry/limitu pobrań** — to wybór + per-upload w formularzu web. Nie da się tego wymusić przez env var ani + config. Praktyka: przy każdym uploadzie ustawiać rozsądne wartości (np. + **7 dni / 10 pobrań**), żeby wygasające linki faktycznie czyściły dysk. + +## Szyfrowanie E2E — WŁĄCZONE (decyzja Oskara) + +Gokapi ma 3 poziomy szyfrowania (żaden / lokalny / **end-to-end**). Wybór +robi się w kroku "Encryption" wizardu `/setup` przy pierwszym starcie — nie +ma env vara. Level 3 (E2E) = plik szyfrowany w przeglądarce przed uploadem, +serwer nigdy nie widzi treści w plaintext. Uwaga upstream: implementacja +szyfrowania nie była niezależnie audytowana; Firefox ma problemy z +pobieraniem zaszyfrowanych plików (znane ograniczenie, nie nasz bug). + +Klucz szyfrowania trafia do `config.json` w `/app/config` — **to jest część, +którą trzeba backupować**, inaczej utrata configu = utrata dostępu do już +zaszyfrowanych plików. + +## Sieć — bind tylko na Tailscale, nigdy 0.0.0.0 + +Port 53842 binduje się **wyłącznie** na Tailscale IP VPS-a +(`TAILSCALE_BIND_IP=100.95.58.48`), nigdy na `0.0.0.0`. Publiczny adres +Hetznera (`135.181.153.108`) w ogóle nie widzi tego portu — jedyna droga na +zewnątrz to `npm@VPS` (TLS na 443) → `share.okit.pl`. npm i gokapi żyją na +tym samym hoście jako osobne stacki compose; npm dociera do gokapi przez +Docker hairpin NAT po tym samym realnym interfejsie (ten sam trik co +`fleet-prometheus`/`nextcloud` — `127.0.0.1` by tu NIE zadziałało). +`GOKAPI_TRUSTED_PROXIES=172.16.0.0/12` mówi Gokapi, żeby ufał +`X-Forwarded-For` z tego zakresu (podsieć mostka Docker), bo źródłowy IP po +hairpinie to brama bridge'a, nie prawdziwy klient. + +## Backup + +- `/opt/homelab/data/gokapi/data` — pliki użytkowników. **Ulotne z + założenia** (linki wygasają), nie wymaga backupu. +- `/opt/homelab/data/gokapi/config` — `config.json` + klucz szyfrowania E2E. + **To backupować** — utrata = utrata dostępu do configu (hasło admina, + ustawienia) i do zaszyfrowanych plików, jeśli klucz trzymany lokalnie. + Sugerowana retencja: dołączyć do tej samej pętli nocnej co inne configi na + VPS (mały rozmiar, plik+SQLite). + +## Cutover checklist (przy deployu — NIE teraz, ten PR to tylko config) + +1. `git pull` na VPS. +2. `mkdir -p /opt/homelab/data/gokapi/{data,config}` na VPS. +3. `cp services/gokapi/env.example services/gokapi/.env`, potwierdzić + `TAILSCALE_BIND_IP` (`tailscale ip -4` na żywym hoście — powinno być + `100.95.58.48`). +4. **Cloudflare**: rekord A `share.okit.pl` → `135.181.153.108`, **DNS + only** (szara chmurka, bez proxy Cloudflare — spójne z resztą wpisów + okit.pl/kapala.org w tym repo). +5. **Wildcard `*.okit.pl` na npm@VPS** — **NAJPIERW SPRAWDZIĆ, czy już + istnieje**. Kontekst: wildcard `*.okit.pl` DNS-01 jest już zrobiony na + `npm@PIHA` (cert #51, ważny do 2026-10-05, token Cloudflare + `npm-dns01-all-zones` — Zone:DNS:Edit, All zones). Sesja + `docs/sessions/2026-07-07-okit-wildcard.md` zostawiła to jako TODO: + *"Drugi npm na VPS (outline/joplin/agents tam) — analogiczny wildcard + `*.okit.pl`?"* — to jest ten moment, żeby to sprawdzić/zrobić. + Jeśli brak: npm@VPS → SSL → New Certificate → Let's Encrypt → domeny + `*.okit.pl` + `okit.pl` → DNS Challenge → Cloudflare → wkleić token + `npm-dns01-all-zones` (ten sam token co PIHA, All-zones więc obejmuje + okit.pl). Pułapka z poprzedniej sesji: literówka `.okit.pl` zamiast + `okit.pl` w drugim polu dawała Internal Error. +6. **vhost w npm@VPS**: nowy Proxy Host, domena `share.okit.pl`, Forward + Hostname/IP = `100.95.58.48` (Tailscale IP VPS, TAILSCALE_BIND_IP), port + `53842`, SSL = cert wildcard `*.okit.pl` z kroku 5, Force SSL + HTTP/2. +7. `docker compose -f services/gokapi/docker-compose.yml up -d`. +8. Otworzyć `https://share.okit.pl/setup` i przejść wizard: + - Database: SQLite (domyślnie, wbudowane). + - Webserver: potwierdzić public URL `https://share.okit.pl`. + - Authentication: Username/Password, ustawić **prawdziwe** hasło admina + (nie ma tu env vara — to jedyny moment, żeby to zrobić w UI). + - Storage: **Local** (nie S3 — decyzja Oskara). + - Encryption: **Level 3 / End-to-End** (decyzja Oskara — E2E ON). +9. Przy każdym uploadzie: ustawiać rozsądny expiry/limit pobrań ręcznie + (patrz sekcja Storage wyżej — brak globalnego defaultu w Gokapi). +10. `./healthcheck.sh` z hosta VPS, potem z zewnątrz: `curl -I + https://share.okit.pl/`. + +## Rejestracja w repo (zrobione w tym PR) + +- `hosts/vps/services.yaml` — wpis `gokapi`. +- `inventory/topology.yaml` — `gokapi` w liście serwisów `vps`. diff --git a/services/gokapi/docker-compose.yml b/services/gokapi/docker-compose.yml new file mode 100644 index 0000000..1d63b5e --- /dev/null +++ b/services/gokapi/docker-compose.yml @@ -0,0 +1,43 @@ +# Gokapi — public file-share (Firefox Send-style), separate from Nextcloud. +# +# Nextcloud stays the private "twierdza" (mesh/kapala.org only). Gokapi is +# the OPPOSITE end of the spectrum on purpose: a small, single-container, +# genuinely public link-share for sending someone outside the mesh a file. +# Runs on VPS (Hetzner, public host) — home nodes stay untouched. +services: + gokapi: + # Pinned to the current stable release (v2.2.4, verified against + # github.com/Forceu/Gokapi releases + Docker Hub f0rc3/gokapi tags on + # 2026-07-09) — never :latest, so upgrades are a deliberate git diff, + # same convention as nextcloud. + image: f0rc3/gokapi:v2.2.4 + container_name: gokapi + restart: unless-stopped + env_file: + - .env + volumes: + # /opt/homelab/data convention. `data` holds uploaded files (churns + # constantly — links expire and get deleted); `config` holds + # config.json + the E2E encryption master key. See README backup note: + # config is the part that actually needs backing up. + - /opt/homelab/data/gokapi/data:/app/data + - /opt/homelab/data/gokapi/config:/app/config + ports: + # PUBLIC reachability is via npm@VPS + share.okit.pl, NOT this bind. + # Bound ONLY to the VPS Tailscale interface (TAILSCALE_BIND_IP), never + # 0.0.0.0 — same defense-in-depth convention as fleet-prometheus: the + # raw port does not exist on the public Hetzner IP (135.181.153.108) + # at all. npm, running as its own container on the same host, reaches + # gokapi via Docker hairpin NAT through this real interface IP + # (loopback would NOT work for that trick — see fleet-prometheus / + # nextcloud for the same pattern). Requires .env (from env.example) + # next to this file at deploy. + - "${TAILSCALE_BIND_IP}:53842:53842" + # Image ships curl (alpine-based), so an in-container check works here — + # unlike vikunja's image, which has neither curl nor wget. + healthcheck: + test: ["CMD", "curl", "-fs", "--max-time", "5", "http://127.0.0.1:53842/"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 15s diff --git a/services/gokapi/env.example b/services/gokapi/env.example new file mode 100644 index 0000000..bd13fe7 --- /dev/null +++ b/services/gokapi/env.example @@ -0,0 +1,47 @@ +# Gokapi host-local binds + disk-protection tuning — copy to .env (gitignored) +# next to docker-compose.yml and fill in real values. Never commit .env. +# No auth secrets here: Gokapi has no headless admin env vars — the admin +# account is created in the one-time /setup web wizard (see README). + +# Tailscale IP of the VPS node (ubuntu-4gb-hel1-1). Bind the gokapi port +# ONLY to this interface — never 0.0.0.0. The public internet never reaches +# port 53842 directly; share.okit.pl reaches gokapi only through npm@VPS +# (which runs on the same host and connects to this IP via Docker hairpin +# NAT — see docker-compose.yml). Verify when rebuilding the host: +# tailscale ip -4. +TAILSCALE_BIND_IP=100.95.58.48 + +TZ=Europe/Warsaw + +# --- Disk protection ------------------------------------------------------- +# VPS has an 80 GB disk shared with npm, outline, joplin, ai-cluster, +# fleet-prometheus's TSDB, etc. — gokapi is NOT the only tenant, and it has +# no S3 backend (local disk only, by design). Keep uploads small and leave +# headroom so expiring links actually reclaim space before the disk fills. + +# Per-file upload cap in MB. 5120 = 5 GB. Upstream default is 102400 +# (100 GB), which would let a single upload fill the whole disk. +GOKAPI_MAX_FILESIZE=5120 + +# Refuse new uploads once free disk space drops below this many MB. Upstream +# default is 400; raised here for the same reason as above. +GOKAPI_MIN_FREE_SPACE=2048 + +# npm@VPS and gokapi run on the same host; npm reaches gokapi's Tailscale- +# bound port via Docker hairpin NAT, so the connection arrives from the +# docker bridge gateway, not the real client IP. Trust that range so +# X-Forwarded-For / IP logging work correctly — same lesson as nextcloud's +# TRUSTED_PROXIES. Narrow to the exact /24 once the stack is up on the live +# host: docker network inspect gokapi_default. +GOKAPI_TRUSTED_PROXIES=172.16.0.0/12 + +# --- NOT env vars — Gokapi has no headless config for these. Set them in +# the one-time /setup wizard on first boot (see README Cutover checklist): +# - Admin username/password (Authentication step) +# - Storage backend: Local (Storage step — NOT S3, +# per Oskar's decision) +# - Encryption level: End-to-End (Level 3) (Encryption step — +# Oskar wants E2E ON) +# - Default expiry / max downloads: there is NO global default in Gokapi — +# it's chosen per-upload in the web form. Use conservative values (e.g. +# 7 days / 10 downloads) each time to keep the disk from filling. diff --git a/services/gokapi/healthcheck.sh b/services/gokapi/healthcheck.sh new file mode 100755 index 0000000..f4bffd4 --- /dev/null +++ b/services/gokapi/healthcheck.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Healthcheck for gokapi (public file-share, https://share.okit.pl via npm@VPS) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# The port is bound to the Tailscale interface only, so localhost won't answer. +# Read the bind IP from .env (same file compose uses for the port mapping). +if [ -f "$SCRIPT_DIR/.env" ]; then + # shellcheck disable=SC1091 + source "$SCRIPT_DIR/.env" +fi +BIND_IP="${TAILSCALE_BIND_IP:-127.0.0.1}" + +# Container must be running +if ! docker ps --filter "name=gokapi" --filter "status=running" | grep -qw "gokapi"; then + echo "[FAIL] gokapi container is not running" + exit 1 +fi + +# Web server must answer (setup wizard or login page, either is fine) +if ! curl -sf --max-time 5 "http://${BIND_IP}:53842/" > /dev/null; then + echo "[FAIL] gokapi is not responding on ${BIND_IP}:53842" + exit 1 +fi + +echo "[OK] gokapi is healthy" +exit 0 diff --git a/services/gokapi/service.yaml b/services/gokapi/service.yaml new file mode 100644 index 0000000..1116228 --- /dev/null +++ b/services/gokapi/service.yaml @@ -0,0 +1,34 @@ +service: + name: gokapi + owner_node: vps + role: public-file-share # Firefox Send-style link sharing, deliberately separate from Nextcloud + exposure: public # public via npm@VPS (share.okit.pl). The container's own port binds + # to TAILSCALE_BIND_IP only, never 0.0.0.0 — npm is the sole public + # entry point (see docker-compose.yml). Same pattern as vikunja. + dependencies: [] # standalone; npm@VPS proxies to it but gokapi has no upstream deps + ports: + - container: 53842 + host: 53842 + protocol: tcp + healthcheck: + type: http + endpoint: http://localhost:53842/ # setup wizard until first admin login, then the login page + interval: 30s + timeout: 10s + retries: 5 + restart_policy: unless-stopped + persistence: + paths: + - /opt/homelab/data/gokapi/data # uploaded files — ephemeral by design, links expire + - /opt/homelab/data/gokapi/config # config.json + E2E encryption master key — BACK THIS UP + runtime: + config_files: + - .env # host-local binds + disk-protection tuning (gitignored, from env.example) + env_vars: + - TAILSCALE_BIND_IP # required — mesh-only port bind; npm@VPS reaches it via hairpin NAT + - GOKAPI_MAX_FILESIZE # per-file cap in MB — keeps the shared VPS disk from filling + - GOKAPI_MIN_FREE_SPACE # MB headroom before uploads are refused + - GOKAPI_TRUSTED_PROXIES # npm@VPS as seen through Docker hairpin NAT (docker bridge subnet) + # No admin/auth env vars: Gokapi has no headless setup — admin account, + # storage backend, encryption level are all set in the /setup web + # wizard on first boot (deploy-time step, see README).