Skip to content

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

  • Podman (recommended) or Docker
  • The FenLiu source repository

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:

  1. Builder stage — installs uv, syncs dependencies, and installs FenLiu as an editable package.
  2. 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

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:

  1. Create /app/data and /app/logs directories if missing.
  2. Set permissions so the application can write to them regardless of the host UID.
  3. Drop privileges to the fenliu user (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/data is 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_KEY in 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_URL pointing 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