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 |
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
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