deploy/README.md becomes the real runbook: the VPS stack, Traefik, the headscale LLM link, the interim edge gate, backups and restore. The millenia Piper notes move to an appendix -- that instance still runs them, and it is still canonical. Two items are called out as outstanding rather than done, because both need access to millenia: vLLM is not bound to its headscale interface, so no AI pass works from the VPS yet, and parodia's ssh key is not authorized there, so backups are VPS-local only -- which is not a backup in the sense that matters. Each has its one-command fix written down. Also folds in DreamDict gaining Spanish: es was explicitly gated on that dataset existing, so it moves from "Later / not now" to a normal follow-on pair after pt-PT, and the Phase 20 provider seam should cover it from the start.
244 lines
9.2 KiB
Markdown
244 lines
9.2 KiB
Markdown
# Deploying Petal
|
|
|
|
Two deployments exist right now:
|
|
|
|
| | host | shape | status |
|
|
|---|---|---|---|
|
|
| **millenia** | `192.168.1.212` / `100.64.0.2` | bare binary in a screen session on `:8088`, Piper as user systemd units | **canonical** — her real writing lives here |
|
|
| **parodia** | `petal.parodia.dev` / `100.64.0.1` | docker compose behind the host's Traefik | staging; empty database |
|
|
|
|
millenia stays canonical until Phase 16's auth lands, so she only moves accounts
|
|
once. Everything below is the VPS side; the millenia Piper notes are kept in the
|
|
appendix because that instance still runs them.
|
|
|
|
---
|
|
|
|
## 1. The VPS stack
|
|
|
|
`docker-compose.yml` at the repo root brings up three containers:
|
|
|
|
- **petal** — the single Go binary with the frontend embedded. Publishes no host
|
|
port; Traefik is the only way in.
|
|
- **piper-en** / **piper-zh** — read-aloud. Each Piper HTTP server loads exactly
|
|
one voice, so English and Chinese are separate containers off one image, with
|
|
the models cached in a shared volume. They sit on an internal network with no
|
|
published ports, so only Petal can reach them. Adding pt-PT in Phase 21 is a
|
|
fourth service, not a new image.
|
|
|
|
They run as containers rather than the host systemd units millenia uses because
|
|
Piper was never actually installed on the VPS, and the `reala` account has no
|
|
lingering session to keep user units alive across logout.
|
|
|
|
### Prerequisites on the host
|
|
|
|
- Docker with the compose plugin, and the existing external `traefik` network
|
|
- A DNS A record for the hostname pointing at the VPS (`petal.parodia.dev` is
|
|
already in place)
|
|
|
|
### First deploy
|
|
|
|
```bash
|
|
ssh reala@100.64.0.1
|
|
git clone https://gitea.parodia.dev/drwily/petal.git ~/petal
|
|
cd ~/petal
|
|
cp deploy/petal.env.example .env
|
|
```
|
|
|
|
Then edit `.env`:
|
|
|
|
- `PETAL_UID` / `PETAL_GID` — `id -u` / `id -g` for this account. `./data` is a
|
|
bind mount, so the image's own `petal` user has no claim on it; a mismatch
|
|
shows up as `unable to open database file (14)` and a restart loop.
|
|
- `PETAL_BASIC_AUTH` — the interim edge gate, see §4.
|
|
- `LLM_MODEL` / `LLM_CHAT_MODEL` — see §3.
|
|
|
|
```bash
|
|
mkdir -p data/backups
|
|
docker compose up -d --build
|
|
docker compose ps # all three healthy
|
|
```
|
|
|
|
### Updating
|
|
|
|
```bash
|
|
cd ~/petal && git pull && docker compose up -d --build
|
|
```
|
|
|
|
The frontend is embedded in the binary, so a rebuild is the whole deploy. The
|
|
client polls `/api/version` (a hash of the built `index.html`) and offers a
|
|
refresh when it changes.
|
|
|
|
---
|
|
|
|
## 2. What Traefik does
|
|
|
|
Labels follow the convention the other services on this box use: the external
|
|
`traefik` network, the `web-secure` entrypoint, the `default` cert resolver and
|
|
`compression@file`. Petal adds its own response-header middleware
|
|
(`frame-ancestors 'self'`, HSTS, nosniff, `Referrer-Policy: same-origin`).
|
|
|
|
`/api/health` is deliberately on its own higher-priority router with no
|
|
middleware: a monitoring probe must not need a credential, and the endpoint
|
|
carries no user data.
|
|
|
|
---
|
|
|
|
## 3. The LLM link over headscale
|
|
|
|
The vLLM backend stays on millenia and is reached over headscale
|
|
(`100.64.0.2`). **This is the only cross-VPN dependency**, and by the
|
|
LLM-minimalism principle it never gates essential functionality — spell check,
|
|
gloss, vocabulary garden, search, export and read-aloud all keep working with
|
|
the link down, and the status bar shows the warm
|
|
`🌙 小助手在休息 · Petal's helper is resting · 文字已保存`.
|
|
|
|
`LLM_TIMEOUT` is raised from the local-network default of 30s to **90s**: the
|
|
voice and collocation passes send a whole document, the timeout is a hard
|
|
deadline on the completion call, and a WAN+VPN round trip eats the margin.
|
|
|
|
### Outstanding — vLLM is not yet listening on headscale
|
|
|
|
As of the Phase 15 deploy, `100.64.0.2:8000` refuses connections from the VPS:
|
|
vLLM is bound to loopback or the LAN interface, not the headscale one. Petal
|
|
degrades correctly (verified — `POST /api/docs/{id}/check` returns the warm
|
|
502), but no AI pass will work until this is fixed on millenia:
|
|
|
|
- start vLLM with `--host 100.64.0.2` — the headscale address specifically,
|
|
**not** `0.0.0.0`; the other end of this link is a public host
|
|
- confirm with `ss -lntp | grep 8000` that it binds only that interface
|
|
- from the VPS: `curl -s http://100.64.0.2:8000/v1/models`
|
|
- put the model id it reports into `LLM_MODEL` / `LLM_CHAT_MODEL` in `.env`,
|
|
then `docker compose up -d`
|
|
|
|
---
|
|
|
|
## 4. Interim edge gate (delete when Phase 16 lands)
|
|
|
|
Petal authenticates nobody yet — `StaticResolver` hands every request the same
|
|
`local` user. On a public host that means anyone who finds the hostname can read
|
|
and write documents and fill the disk with image uploads, so Traefik holds the
|
|
door with basic auth until the OIDC flow exists.
|
|
|
|
Generate a credential:
|
|
|
|
```bash
|
|
htpasswd -nbB petal 'your-password' # or any bcrypt htpasswd generator
|
|
```
|
|
|
|
and put the resulting `user:hash` pair in `.env` as `PETAL_BASIC_AUTH`.
|
|
|
|
When Phase 16 lands, delete the `petal-auth` middleware label, the
|
|
`petal-health` router labels, and this section.
|
|
|
|
---
|
|
|
|
## 5. Backups
|
|
|
|
`deploy/backup-petal.sh` runs nightly from cron at 03:15:
|
|
|
|
```
|
|
15 3 * * * REMOTE_HOST= /bin/bash $HOME/petal/deploy/backup-petal.sh >> $HOME/petal/data/backups/backup.log 2>&1
|
|
```
|
|
|
|
The snapshot goes through `petal -backup`, which uses SQLite's `VACUUM INTO`.
|
|
That matters: Petal runs in WAL mode, so the newest committed pages may live in
|
|
`petal.db-wal` rather than `petal.db`, and copying the three files separately can
|
|
capture a torn mid-checkpoint state. `VACUUM INTO` reads one coherent snapshot
|
|
including the WAL, takes no write lock (so it is safe against the live app), and
|
|
emits a single file with no `-wal`/`-shm` companions. It refuses an existing
|
|
destination, so a failed run cannot destroy the last good backup.
|
|
|
|
The script then gzips, pushes off-box, verifies the transferred size, and prunes
|
|
both sides (7 days local, 30 days remote).
|
|
|
|
### Outstanding — the off-VPS push is not yet enabled
|
|
|
|
`REMOTE_HOST` is empty in the cron line, so backups are currently **local to the
|
|
VPS only** — which is not a backup in the sense that matters. parodia's ssh key
|
|
is not authorized on millenia. To enable it, on millenia:
|
|
|
|
```bash
|
|
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICGL6fS7IKCs8xFxUoH/bI/0kq4AzW05bcfV2XHoEXYM ditto-deploy@parodia-box' >> ~/.ssh/authorized_keys
|
|
```
|
|
|
|
then on the VPS, prove it works by hand before touching cron:
|
|
|
|
```bash
|
|
cd ~/petal && REMOTE_USER=<millenia-user> ./deploy/backup-petal.sh
|
|
```
|
|
|
|
and set `REMOTE_USER=<millenia-user> REMOTE_HOST=100.64.0.2` in the cron line.
|
|
|
|
### Restore
|
|
|
|
```bash
|
|
cd ~/petal
|
|
docker compose stop petal # stop writers first
|
|
mv data/petal.db data/petal.db.before-restore # keep the current state
|
|
rm -f data/petal.db-wal data/petal.db-shm # a stale WAL against a new file
|
|
gunzip -c data/backups/petal-<stamp>.db.gz > data/petal.db
|
|
docker compose start petal
|
|
docker compose logs petal --tail 5 # expect "database ready"
|
|
```
|
|
|
|
To sanity-check an archive before committing to it, restore it into a scratch
|
|
directory and have Petal open it:
|
|
|
|
```bash
|
|
mkdir -p /tmp/restore-check
|
|
gunzip -c data/backups/petal-<stamp>.db.gz > /tmp/restore-check/petal.db
|
|
docker run --rm -v /tmp/restore-check:/data --user "$(id -u):$(id -g)" \
|
|
--entrypoint sh petal:local -c '/app/petal -backup /data/verify.db'
|
|
```
|
|
|
|
A clean exit means the file opens and reads end to end.
|
|
|
|
---
|
|
|
|
## Appendix — Piper on millenia (user systemd units)
|
|
|
|
millenia still runs Piper as user services; these are the original notes.
|
|
|
|
```bash
|
|
scp deploy/piper.service deploy/setup-piper.sh 192.168.1.212:/tmp/
|
|
ssh 192.168.1.212 'cd /tmp && sudo ./setup-piper.sh'
|
|
```
|
|
|
|
`setup-piper.sh` creates `~/piper/venv`, installs `piper-tts[http]`, downloads
|
|
`en_US-amy-medium` into `~/piper/voices`, installs and enables `piper.service`
|
|
(loopback `:5005`), and smoke-tests it. Idempotent. Check it with
|
|
`systemctl status piper` / `journalctl -u piper -f`.
|
|
|
|
Chinese runs as a second instance (`piper-zh.service`, `:5006`,
|
|
`zh_CN-huayan-medium`):
|
|
|
|
```bash
|
|
scp deploy/piper-zh.service 192.168.1.212:~/.config/systemd/user/
|
|
ssh 192.168.1.212 'export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
|
~/piper/venv/bin/python -m piper.download_voices zh_CN-huayan-medium --data-dir ~/piper/voices
|
|
systemctl --user daemon-reload && systemctl --user enable --now piper-zh.service'
|
|
```
|
|
|
|
Petal's env then carries `TTS_ENDPOINT=http://127.0.0.1:5005`,
|
|
`TTS_ENDPOINT_ZH=http://127.0.0.1:5006` and the matching voice ids. The handler
|
|
maps language → instance from config, so another language is another instance
|
|
plus an env pair, no code change.
|
|
|
|
**Piper version note:** piper-tts moved synthesis from `POST /` to
|
|
`POST /synthesize` in 1.6.0, with an identical request body. `TTS_PATH` selects
|
|
which — it defaults to `/` for millenia's older server, and the VPS compose sets
|
|
`/synthesize` for the 1.6.0 sidecars. If read-aloud starts returning 502 after a
|
|
Piper upgrade on millenia, that flag is the fix.
|
|
|
|
Verify end to end (through Petal, including the ffmpeg transcode):
|
|
|
|
```bash
|
|
curl -sf -X POST localhost:8088/api/tts \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"text":"hello there","lang":"en-US"}' -o /tmp/petal-tts.mp3 \
|
|
&& file /tmp/petal-tts.mp3 # expect: MPEG ADTS, layer III
|
|
```
|
|
|
|
A second identical call is served from the cache; a language with no configured
|
|
instance returns 404 so the client falls back to Web Speech.
|