Container Deployment¶
FenLiu ships with a production-ready Containerfile for Podman (or Docker). The multi-stage build produces a minimal image (~207 MB) based on python:3.13-slim-bookworm.
Prerequisites¶
Building the Image¶
# With Podman (recommended)
podman build -t fenliu -f Containerfile .
# Or with Docker
docker build -t fenliu -f Containerfile .
The build uses two stages:
- Builder stage — installs
uv, syncs dependencies, and installs FenLiu as an editable package. - Runtime stage — copies only the virtual environment and application source into a clean image. No build tools are present in the final image.
Configuration¶
FenLiu is configured via environment variables. Copy the example file and edit it before running:
cp .env.example .env
# Edit .env with your settings
The most important variables to set for production:
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
your-secret-key-change-in-production |
Session signing key — must be changed |
DATABASE_URL |
sqlite:////app/data/fenliu.db |
Database location (container default uses /app/data) |
DEBUG |
false |
Enable verbose logging |
UI_AUTH_ENABLED |
true |
Session-based web UI auth. Set false only for personal/private-LAN deployments |
DEFAULT_INSTANCE |
mastodon.social |
Default Fediverse instance for hashtag monitoring |
API_TIMEOUT |
30 |
HTTP timeout (seconds) for Fediverse API calls |
MAX_POSTS_PER_FETCH |
20 |
Posts fetched per hashtag stream request |
RATE_LIMIT_DELAY |
1.0 |
Delay between API requests (seconds) |
POSTS_PER_PAGE |
20 |
Posts shown per page in the web UI |
REFRESH_INTERVAL |
300 |
UI auto-refresh interval (seconds, 0 = disabled) |
Generate a secure SECRET_KEY:
python -c "import secrets; print(secrets.token_urlsafe(32))"
See Configuration for the full reference.
Running the Container¶
With an env file (recommended)¶
podman run -d \
--name fenliu \
-p 8000:8000 \
-v fenliu-data:/app/data \
-v fenliu-logs:/app/logs \
--env-file .env \
fenliu
With explicit environment variables¶
podman run -d \
--name fenliu \
-p 8000:8000 \
-v fenliu-data:/app/data \
-v fenliu-logs:/app/logs \
-e DATABASE_URL="sqlite:////app/data/fenliu.db" \
-e SECRET_KEY="your-production-secret-key" \
fenliu
Then open http://localhost:8000 in your browser.
Volumes¶
Two volumes provide persistence across container restarts:
| Mount point | Purpose |
|---|---|
/app/data |
SQLite database file |
/app/logs |
Application log files |
Use named volumes or bind mounts
Without persistent volumes the database is lost when the container is removed.
Bind mount example (host directory)¶
mkdir -p ~/fenliu/data ~/fenliu/logs
podman run -d \
--name fenliu \
-p 8000:8000 \
-v ~/fenliu/data:/app/data \
-v ~/fenliu/logs:/app/logs \
--env-file .env \
fenliu
Compose Example¶
Save as compose.yml (works with both podman-compose and docker compose):
services:
fenliu:
build: .
ports:
- "8000:8000"
volumes:
- fenliu-data:/app/data
- fenliu-logs:/app/logs
env_file: .env
restart: unless-stopped
volumes:
fenliu-data:
fenliu-logs:
Start:
podman-compose up -d
# or
docker compose up -d
Podman Quadlet¶
Quadlets are the recommended way to run containers as systemd user services. Because quadlets don't support --env-file directly as a CLI flag, provide secrets via EnvironmentFile=.
1. Create an env file for secrets¶
mkdir -p ~/.config/containers/systemd
# Generate a secure SECRET_KEY
python -c "import secrets; print(secrets.token_urlsafe(32))"
Write the output to an env file — keep it readable only by your user:
cat > ~/.config/containers/systemd/fenliu.env <<'EOF'
SECRET_KEY=<paste-generated-key-here>
EOF
chmod 600 ~/.config/containers/systemd/fenliu.env
2. Reference the env file in your quadlet¶
~/.config/containers/systemd/fenliu.container:
[Container]
ContainerName=fenliu
Image=codeberg.org/marvinsmastodontools/fenliu:latest
EnvironmentFile=%h/.config/containers/systemd/fenliu.env
Volume=fenliu-data.volume:/app/data
Volume=fenliu-logs.volume:/app/logs
Pod=fenliu.pod
[Service]
Restart=on-failure
TimeoutStopSec=70
%h expands to your home directory. Reload and restart:
systemctl --user daemon-reload
systemctl --user restart fenliu.service
SECRET_KEY is required
FenLiu refuses to start if SECRET_KEY is not set or is still the default placeholder. The container will exit immediately and the structured log will show a CRITICAL message with the generation command.
First-Run Authentication¶
FenLiu defaults to UI_AUTH_ENABLED=true. On first start, the database has no password stored, so all web UI requests redirect to /setup.
- Start the container as normal and open
http://host:8000in a browser. - The app detects no password and redirects to
/setup. - Read the guidance on the setup page, then enter and confirm a password for the
adminaccount. - On submit the password is hashed and stored in the database, then you are redirected to
/login. - Log in. The DB volume persists the hash across container restarts — no re-setup needed.
To reset credentials: stop the container, delete or wipe the DB volume, and restart (redirects to /setup again). Alternatively set UI_AUTH_ENABLED=false temporarily to access the app and change the password via Settings → Change Password, then re-enable auth and restart.
Health checks
HTTP health checks against /health are unaffected — that endpoint is always public. Health checks that target the web root (/) will see 302 redirects until setup is complete.
Upgrading from a Version Without Web UI Authentication¶
Breaking change
UI_AUTH_ENABLED defaults to true. On first start after upgrading, the app finds no ui_password_hash in the DB and redirects all web UI requests to /setup. The API, /health, and /setup itself remain accessible.
Interactive upgrade path: upgrade → start → open browser → complete /setup → log in → done.
Hands-off / unattended upgrade path: add UI_AUTH_ENABLED=false to the env file before upgrading to keep existing behaviour; access /setup at your convenience to set a password; then set UI_AUTH_ENABLED=true and restart.
No database migration is required — ui_password_hash is stored as a new AppSetting row.
Startup Behaviour¶
The container entrypoint (entrypoint.sh) runs as root briefly to:
- Create
/app/dataand/app/logsdirectories if missing. - Set permissions so the application can write to them regardless of the host UID.
- Drop privileges to the
fenliuuser (UID 1000) before starting the app.
FenLiu then runs alembic upgrade head automatically on startup to apply any pending database migrations before the web server begins accepting requests.
Backups¶
The container includes the sqlite3 CLI, which uses SQLite's online backup API to produce a consistent snapshot without stopping the application.
Manual backup¶
podman exec fenliu sqlite3 /app/data/fenliu.db \
".backup '/app/data/fenliu-backup.db'"
podman cp fenliu:/app/data/fenliu-backup.db \
./fenliu-$(date +%Y%m%d).db
podman exec fenliu rm /app/data/fenliu-backup.db
This gives you a clean .db file you can inspect, query, or restore directly.
Restore¶
Stop the container, replace the database file, then restart:
systemctl --user stop fenliu.service
podman cp ./fenliu-20260312.db fenliu:/app/data/fenliu.db
systemctl --user start fenliu.service
Automated backup with a systemd timer¶
~/.config/containers/systemd/fenliu-backup.service:
[Unit]
Description=Backup FenLiu database
After=fenliu.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
podman exec fenliu sqlite3 /app/data/fenliu.db \
".backup /app/data/fenliu-backup.db" && \
podman cp fenliu:/app/data/fenliu-backup.db \
%h/backups/fenliu-$(date +%%Y%%m%%d).db && \
podman exec fenliu rm /app/data/fenliu-backup.db'
~/.config/containers/systemd/fenliu-backup.timer:
[Unit]
Description=Daily FenLiu backup
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
mkdir -p ~/backups
systemctl --user daemon-reload
systemctl --user enable --now fenliu-backup.timer
Security Notes¶
- The container runs as a non-root user (
fenliu, UID 1000) after the initial setup. /app/datais world-writable (0o777) so host bind-mounts with any UID can write to it — consider using named volumes instead if this is a concern.- Always set a strong, unique
SECRET_KEYin production. - Use HTTPS in front of the container (nginx, Caddy, Traefik, etc.) in production.
Troubleshooting¶
Container exits immediately¶
Check logs:
podman logs fenliu
Common causes:
- Missing or invalid
SECRET_KEY DATABASE_URLpointing to a path that isn't mounted- Port 8000 already in use (change with
-p 8001:8000)
Permission denied on /app/data¶
Ensure the volume is mounted correctly and the host directory is accessible. Named volumes are recommended over bind mounts for simplicity.
Database migration errors¶
Migrations run automatically on startup. If they fail, the logs will show the Alembic error. Running the container again after fixing the issue will re-attempt migrations.
Next Steps¶
- Configuration — full environment variable reference
- Quick Start — explore FenLiu's features
- API Reference — integrate with external systems