feat: batteries-included deployment — compose, .env.example, sane defaults
Some checks failed
CI / Check / Test (push) Failing after 6m21s

This commit is contained in:
2026-06-04 17:32:34 +02:00
parent 4bd8dcbf05
commit 7e8a1b8379
5 changed files with 89 additions and 83 deletions

View File

@@ -1,72 +1,62 @@
# Database backend — "sqlite" (default) or "postgres" # ── Required ──────────────────────────────────────────────────
DATABASE_BACKEND=sqlite
# Option A: SQLite (default, zero external dependencies) # Database (SQLite — file auto-created on first run)
DATABASE_URL=sqlite://movies.db DATABASE_URL=sqlite:///data/movies.db
# Option B: PostgreSQL
# DATABASE_BACKEND=postgres
# DATABASE_URL=postgres://user:password@localhost:5432/movies_diary
# Authentication # Authentication
JWT_SECRET=change-me JWT_SECRET=change-me-to-a-random-string
JWT_TTL_SECONDS=86400
# OMDb/TMDB metadata # Movie metadata — one of these is required (TMDB preferred)
OMDB_API_KEY=your-key # TMDB_API_KEY=your-tmdb-key
TMDB_API_KEY=your-key OMDB_API_KEY=your-omdb-key
# Poster storage — Option A (local) is active. To use S3, comment it out and uncomment Option B: # ── Recommended ──────────────────────────────────────────────
# Option A: local filesystem (zero external dependencies) # Public URL (used for ActivityPub federation and canonical links)
POSTER_STORAGE_BACKEND=local BASE_URL=https://yourdomain.example.com
POSTER_STORAGE_PATH=./posters
# Option B: S3-compatible (MinIO, AWS S3, etc.) # Enable sign-ups (default: false — set true so you can register)
# POSTER_STORAGE_BACKEND=s3 ALLOW_REGISTRATION=true
# ── Image Storage (defaults to local filesystem) ─────────────
# IMAGE_STORAGE_BACKEND=local # default
# IMAGE_STORAGE_PATH=./images # default
# S3-compatible alternative (MinIO, AWS S3, etc.):
# IMAGE_STORAGE_BACKEND=s3
# MINIO_ENDPOINT=http://localhost:9000 # MINIO_ENDPOINT=http://localhost:9000
# MINIO_BUCKET=posters # MINIO_BUCKET=posters
# MINIO_REGION=minio # MINIO_REGION=minio
# MINIO_ACCESS_KEY_ID=minioadmin # MINIO_ACCESS_KEY_ID=minioadmin
# MINIO_SECRET_ACCESS_KEY=minioadmin # MINIO_SECRET_ACCESS_KEY=minioadmin
# Optional # ── Optional ─────────────────────────────────────────────────
HOST=0.0.0.0
PORT=3000
BASE_URL=http://localhost:3000
SECURE_COOKIES=false
ALLOW_REGISTRATION=false
RATE_LIMIT=20
POSTER_FETCH_TIMEOUT_SECONDS=30
# Event bus — "db" (default) or "nats" # TMDb enrichment (cast, crew, genres — separate from search metadata above)
# The worker binary must run alongside the presentation to process events. # TMDB_API_KEY=your-tmdb-key
EVENT_BUS_BACKEND=db
# Option A: DB queue (default — no extra infrastructure needed) # Image conversion (converts uploaded posters to AVIF or WebP)
# Events are persisted in the same database as the app and polled by the worker.
# EVENT_QUEUE_POLL_INTERVAL_MS=500 # polling interval (default: 500ms)
# EVENT_QUEUE_BATCH_SIZE=10 # rows claimed per poll cycle (default: 10)
# EVENT_QUEUE_MAX_ATTEMPTS=5 # retries before dead-lettering (default: 5)
# Option B: NATS (at-least-once delivery, recommended for higher throughput)
# EVENT_BUS_BACKEND=nats
# NATS_URL=nats://localhost:4222
# NATS_MODE=jetstream # "jetstream" (default, at-least-once) or "core" (fire-and-forget)
# NATS_SUBJECT_PREFIX=movies-diary.events
# NATS_STREAM_NAME=MOVIES_DIARY_EVENTS
# NATS_CONSUMER_NAME=worker
# Image conversion (optional — converts stored images to save disk space)
# Disable by default; enable in the worker by setting ENABLED=true.
# IMAGE_CONVERSION_ENABLED=false # IMAGE_CONVERSION_ENABLED=false
# IMAGE_CONVERSION_FORMAT=avif # avif | webp # IMAGE_CONVERSION_FORMAT=avif
# Annual Wrap-Up (video generation — optional, requires ffmpeg) # Wrap-Up video generation (requires ffmpeg in container — included in Docker image)
# WRAPUP_FONT_PATH=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf # WRAPUP_FONT_PATH=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
# WRAPUP_LOGO_PATH=./static/logo.webp # WRAPUP_LOGO_PATH=./static/logo.webp
# WRAPUP_BG_DIR=./static/wrapup-backgrounds # directory of jpg/png/webp images for video slide backgrounds # WRAPUP_BG_DIR=./static/wrapup-backgrounds
# FFMPEG_PATH=ffmpeg # FFMPEG_PATH=ffmpeg
# WRAPUP_MAX_CONCURRENT=2 # WRAPUP_MAX_CONCURRENT=2
RUST_LOG=presentation=debug,tower_http=debug,worker=info,application=info # Server
# HOST=0.0.0.0
# PORT=3000
# RATE_LIMIT=60
# SECURE_COOKIES=true
# RUST_LOG=presentation=info,tower_http=info,worker=info
# CORS (for SPA development only)
# CORS_ORIGINS=http://localhost:5173
# Event bus — "db" (default, uses same database) or "nats"
# EVENT_BUS_BACKEND=db
# NATS_URL=nats://localhost:4222

View File

@@ -203,38 +203,28 @@ The `application` crate has unit tests for core use cases backed by in-memory fa
## Docker ## Docker
The image contains both `presentation` (HTTP server) and `worker` (event processor), plus `ffmpeg` and DejaVu fonts for wrap-up video generation. Run them as separate containers sharing the same data volume: ### Quick start
```bash ```bash
# Build (SQLite + federation + NATS support) cp .env.example .env
docker build -t movies-diary \ # Edit .env — set JWT_SECRET and OMDB_API_KEY (or TMDB_API_KEY)
--build-arg FEATURES=sqlite,sqlite-federation,nats . docker compose up -d
# HTTP server
docker run -p 3000:3000 \
-e DATABASE_URL=sqlite:///data/movies.db \
-e JWT_SECRET=change-me \
-e OMDB_API_KEY=your-key \
-e BASE_URL=https://yourdomain.example.com \
-e EVENT_BUS_BACKEND=nats \
-e NATS_URL=nats://nats:4222 \
-v $(pwd)/data:/data \
movies-diary
# Event worker (separate container, same image)
docker run \
-e DATABASE_URL=sqlite:///data/movies.db \
-e JWT_SECRET=change-me \
-e OMDB_API_KEY=your-key \
-e BASE_URL=https://yourdomain.example.com \
-e EVENT_BUS_BACKEND=nats \
-e NATS_URL=nats://nats:4222 \
-v $(pwd)/data:/data \
--entrypoint ./worker \
movies-diary
``` ```
To build for PostgreSQL: `--build-arg FEATURES=postgres,postgres-federation,nats` This builds and starts the HTTP server (port 3000) and event worker. Data is persisted in a Docker volume.
### Manual docker run
The image contains both `presentation` and `worker` binaries. Run them as separate containers sharing the same data volume:
```bash
docker build -t movies-diary .
docker run -p 3000:3000 --env-file .env -v movies-diary-data:/data movies-diary
docker run --env-file .env -v movies-diary-data:/data --entrypoint ./worker movies-diary
```
Build for PostgreSQL: `--build-arg FEATURES=postgres,postgres-federation`
## Media Server Integration ## Media Server Integration

View File

@@ -6,8 +6,7 @@ pub struct StorageConfig(Arc<dyn ObjectStore>);
impl StorageConfig { impl StorageConfig {
pub fn from_env() -> anyhow::Result<Self> { pub fn from_env() -> anyhow::Result<Self> {
let backend = std::env::var("IMAGE_STORAGE_BACKEND") let backend = std::env::var("IMAGE_STORAGE_BACKEND").unwrap_or_else(|_| "local".into());
.context("IMAGE_STORAGE_BACKEND required (valid values: s3, local)")?;
let store: Arc<dyn ObjectStore> = match backend.as_str() { let store: Arc<dyn ObjectStore> = match backend.as_str() {
"s3" => build_s3_store( "s3" => build_s3_store(
@@ -19,8 +18,7 @@ impl StorageConfig {
&std::env::var("MINIO_REGION").unwrap_or_else(|_| "minio".to_string()), &std::env::var("MINIO_REGION").unwrap_or_else(|_| "minio".to_string()),
)?, )?,
"local" => build_local_store( "local" => build_local_store(
&std::env::var("IMAGE_STORAGE_PATH") &std::env::var("IMAGE_STORAGE_PATH").unwrap_or_else(|_| "./images".into()),
.context("IMAGE_STORAGE_PATH required when IMAGE_STORAGE_BACKEND=local")?,
)?, )?,
other => { other => {
anyhow::bail!("Unknown IMAGE_STORAGE_BACKEND: {other:?}. Valid values: s3, local") anyhow::bail!("Unknown IMAGE_STORAGE_BACKEND: {other:?}. Valid values: s3, local")

View File

@@ -25,7 +25,7 @@ impl AppConfig {
let rate_limit = std::env::var("RATE_LIMIT") let rate_limit = std::env::var("RATE_LIMIT")
.ok() .ok()
.and_then(|v| v.parse().ok()) .and_then(|v| v.parse().ok())
.unwrap_or(20); .unwrap_or(60);
Self { Self {
allow_registration, allow_registration,
base_url, base_url,

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
app:
build:
context: .
args:
FEATURES: sqlite,sqlite-federation
ports:
- "3000:3000"
env_file: .env
volumes:
- data:/data
restart: unless-stopped
worker:
build:
context: .
args:
FEATURES: sqlite,sqlite-federation
entrypoint: ["./worker"]
env_file: .env
volumes:
- data:/data
restart: unless-stopped
depends_on:
- app
volumes:
data: