Files
petal/docker-compose.yml
T
prosolis df6bc4989c Run the container as the host uid that owns the ./data bind mount
The image's own petal user (uid 10001) has no claim on a bind-mounted
host directory, so SQLite came up with "unable to open database file
(14)" and the container restart-looped. Run as the stack directory's
owner instead of chowning ./data to 10001 -- the backup script gzips
snapshots in place from the host, so that account needs write access to
the same directory. Still non-root.
2026-07-26 23:11:00 -07:00

111 lines
4.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 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
traefik.http.services.petal.loadbalancer.server.port: "8080"
# 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: