Petal authenticates nobody yet -- StaticResolver hands every request the same local user -- so on a public host the whole API is open: anyone who finds the hostname can read and write documents and fill the disk with image uploads. Traefik holds the door until the OIDC flow exists. /api/health keeps its own higher-priority router with no middleware, so the acceptance criterion (public health endpoint, reachable by the monitoring on this box) still holds. Both the middleware and that router are deleted when Phase 16 lands.
129 lines
5.4 KiB
YAML
129 lines
5.4 KiB
YAML
# Petal on the parodia.dev VPS.
|
|
#
|
|
# docker compose up -d --build
|
|
#
|
|
# Fronted by the host's existing Traefik (external `traefik` network, the
|
|
# `web-secure` entrypoint and the `default` cert resolver — same convention the
|
|
# other services on this box use). Petal itself never binds a host port; the
|
|
# only way in is through Traefik over HTTPS.
|
|
#
|
|
# Read-aloud runs as two sibling containers rather than host systemd services:
|
|
# each Piper HTTP server loads exactly one voice, the host has no lingering
|
|
# user session to keep systemd units alive, and keeping them on the internal
|
|
# network means the TTS ports are unreachable from anywhere but Petal.
|
|
#
|
|
# Copy deploy/petal.env.example to .env before the first `up`.
|
|
|
|
name: petal
|
|
|
|
services:
|
|
petal:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
image: petal:local
|
|
container_name: petal
|
|
restart: unless-stopped
|
|
# ./data is a bind mount, so the image's own `petal` user (uid 10001) has no
|
|
# claim on it — the host's ownership wins and the container can't open
|
|
# petal.db. Run as whoever owns the stack directory instead. Keeping it the
|
|
# host user (rather than chowning ./data to 10001) is deliberate: the backup
|
|
# script gzips snapshots in place from the host, so the host account needs
|
|
# write access to the same directory. Still never root.
|
|
user: "${PETAL_UID:-1001}:${PETAL_GID:-1001}"
|
|
env_file: .env
|
|
environment:
|
|
# Fixed by the image layout; kept here so they're visible at a glance.
|
|
PORT: "8080"
|
|
DATABASE_PATH: /data/petal.db
|
|
IMAGE_DIR: /data/images
|
|
TTS_CACHE_DIR: /data/tts
|
|
# Piper sidecars. Each server loads one voice, so English and Chinese are
|
|
# separate containers; the handler maps language → instance from config.
|
|
TTS_ENDPOINT: http://piper-en:5000
|
|
TTS_ENDPOINT_ZH: http://piper-zh:5000
|
|
# The sidecars run piper-tts 1.6.0, which serves synthesis on
|
|
# /synthesize; millenia's older server keeps the default "/".
|
|
TTS_PATH: /synthesize
|
|
# The companion's bedtime nag and night mode read the local clock.
|
|
TZ: ${TZ:-Europe/Lisbon}
|
|
volumes:
|
|
# A bind mount, not a named volume: petal.db must be trivially reachable
|
|
# from the host for the nightly backup and for a restore.
|
|
- ./data:/data
|
|
networks:
|
|
- traefik
|
|
- internal
|
|
depends_on:
|
|
- piper-en
|
|
- piper-zh
|
|
labels:
|
|
traefik.enable: "true"
|
|
traefik.docker.network: traefik
|
|
traefik.http.routers.petal.rule: Host(`${PETAL_HOST:-petal.parodia.dev}`)
|
|
traefik.http.routers.petal.entrypoints: web-secure
|
|
traefik.http.routers.petal.tls: "true"
|
|
traefik.http.routers.petal.tls.certResolver: default
|
|
traefik.http.routers.petal.service: petal
|
|
traefik.http.routers.petal.middlewares: compression@file,petal-headers,petal-auth
|
|
traefik.http.services.petal.loadbalancer.server.port: "8080"
|
|
# INTERIM — delete this middleware and the petal-health router when Phase
|
|
# 16's OIDC login lands. Petal has no authentication of its own yet
|
|
# (StaticResolver hands every request the same local user), so without a
|
|
# gate at the edge anyone who finds the hostname can read and write
|
|
# documents and upload images. PETAL_BASIC_AUTH is a user:bcrypt-hash
|
|
# pair; see deploy/README.md for generating it.
|
|
traefik.http.middlewares.petal-auth.basicauth.users: ${PETAL_BASIC_AUTH:?set PETAL_BASIC_AUTH in .env}
|
|
# /api/health stays open on its own higher-priority router: a monitoring
|
|
# probe must not need a credential, and the endpoint carries no user data.
|
|
traefik.http.routers.petal-health.rule: Host(`${PETAL_HOST:-petal.parodia.dev}`) && Path(`/api/health`)
|
|
traefik.http.routers.petal-health.priority: "100"
|
|
traefik.http.routers.petal-health.entrypoints: web-secure
|
|
traefik.http.routers.petal-health.tls: "true"
|
|
traefik.http.routers.petal-health.tls.certResolver: default
|
|
traefik.http.routers.petal-health.service: petal
|
|
# Petal is a private writing space: no framing, no sniffing, HSTS on.
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Content-Security-Policy: frame-ancestors 'self'
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Strict-Transport-Security: max-age=31536000; includeSubDomains
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.X-Content-Type-Options: nosniff
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Referrer-Policy: same-origin
|
|
|
|
piper-en:
|
|
build:
|
|
context: deploy/piper
|
|
image: petal-piper:local
|
|
container_name: petal-piper-en
|
|
restart: unless-stopped
|
|
environment:
|
|
PIPER_VOICE: ${TTS_VOICE_EN:-en_US-amy-medium}
|
|
volumes:
|
|
- piper-voices:/voices
|
|
networks:
|
|
- internal
|
|
|
|
piper-zh:
|
|
build:
|
|
context: deploy/piper
|
|
image: petal-piper:local
|
|
container_name: petal-piper-zh
|
|
restart: unless-stopped
|
|
environment:
|
|
PIPER_VOICE: ${TTS_VOICE_ZH:-zh_CN-huayan-medium}
|
|
volumes:
|
|
- piper-voices:/voices
|
|
networks:
|
|
- internal
|
|
|
|
networks:
|
|
# Created and owned by the host's Traefik stack.
|
|
traefik:
|
|
external: true
|
|
# Petal ↔ Piper only. Not reachable from the internet or the other stacks.
|
|
internal:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
# Downloaded voice models, shared read-mostly by both Piper instances so the
|
|
# same model is never fetched twice.
|
|
piper-voices:
|