feat(gokapi): config publicznego file-share na VPS (share.okit.pl, E2E, Tailscale-bind za npm@VPS, disk-protection) — do deployu
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 <noreply@anthropic.com>
This commit is contained in:
parent
7e5577c58f
commit
fbf9e7501d
|
|
@ -59,3 +59,20 @@ services:
|
||||||
config_path: /opt/homelab/config/fleet-prometheus
|
config_path: /opt/homelab/config/fleet-prometheus
|
||||||
data_path: /opt/homelab/data/fleet-prometheus
|
data_path: /opt/homelab/data/fleet-prometheus
|
||||||
logs_path: /opt/homelab/logs/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
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ nodes:
|
||||||
- joplin # Note sync server (joplin-server + postgres)
|
- joplin # Note sync server (joplin-server + postgres)
|
||||||
- ai-cluster # AI workers: codex-worker, openclaw, planner-worker,
|
- ai-cluster # AI workers: codex-worker, openclaw, planner-worker,
|
||||||
# service-ops-worker, redis, mosquitto
|
# 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:
|
chelsty-infra:
|
||||||
site: chelsty
|
site: chelsty
|
||||||
|
|
|
||||||
119
services/gokapi/README.md
Normal file
119
services/gokapi/README.md
Normal file
|
|
@ -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`.
|
||||||
43
services/gokapi/docker-compose.yml
Normal file
43
services/gokapi/docker-compose.yml
Normal file
|
|
@ -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
|
||||||
47
services/gokapi/env.example
Normal file
47
services/gokapi/env.example
Normal file
|
|
@ -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.
|
||||||
27
services/gokapi/healthcheck.sh
Executable file
27
services/gokapi/healthcheck.sh
Executable file
|
|
@ -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
|
||||||
34
services/gokapi/service.yaml
Normal file
34
services/gokapi/service.yaml
Normal file
|
|
@ -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).
|
||||||
Loading…
Reference in a new issue