The mountpoint directory exists whether or not the encrypted volume is mounted, so a boot where the unlock failed would start Petal against an empty unencrypted directory and serve a blank database -- the failure mode that looks like data loss. .volume-ok lives on the encrypted filesystem and is bind-mounted with create_host_path:false, so its absence is a container start failure instead of a silent empty DB.
143 lines
6.1 KiB
YAML
143 lines
6.1 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
|
|
# Mount-liveness guard. On the VPS ./data is an encrypted LUKS volume, and
|
|
# the mountpoint directory still exists when that volume is NOT mounted —
|
|
# so without this, a boot where the unlock failed would start Petal
|
|
# against an empty unencrypted directory and quietly serve a blank
|
|
# database. .volume-ok lives on the encrypted filesystem, and
|
|
# create_host_path: false turns its absence into a container start
|
|
# failure instead. Harmless elsewhere: create the file once and it is a
|
|
# no-op. See deploy/README.md §6.
|
|
- type: bind
|
|
source: ./data/.volume-ok
|
|
target: /data/.volume-ok
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
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:
|