Installation
Install Savvy with Docker Compose — persistent volume, environment variables and health-check endpoints for a production-ready self-hosted expense tracker.
Docker Compose is the recommended way to run Savvy for anything beyond a quick look. It gives you a named volume for your data, a public URL and a health check, all in one file you can version-control.
Docker Compose
Create the compose file
Save this as docker-compose.yml. The image: tag is pinned to the current
release so your deploy is reproducible:
services: savvy: image: truenormis/savvy:v1.2.3 container_name: savvy restart: unless-stopped ports: - "3000:80" volumes: - savvy-data:/data environment: - APP_URL=https://savvy.yourdomain.com - TZ=Europe/Kyiv healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/livez"] interval: 10s timeout: 3s start_period: 60s retries: 3volumes: savvy-data:Start it
docker compose up -dMigrations run automatically on first start. Once the container is healthy, open
your APP_URL (or localhost:3000 locally) and create your account.
Environment variables
| Variable | Description | Default |
|---|---|---|
APP_URL | Public URL of your instance | http://localhost |
TZ | Timezone | UTC |
Set APP_URL correctly
APP_URL must match the URL users actually reach the app on (including
https:// when behind a proxy). It drives absolute links, redirects and
cookie scoping — a mismatch breaks logins and 2FA. See
Reverse proxy.
Data & persistence
Everything lives in the /data volume: the SQLite database (database.sqlite)
and uploads. As long as that volume survives, your data survives container
restarts, upgrades and re-creates. Never bind-mount /data onto a network
filesystem — SQLite needs a real local disk for its locking to work.
Health checks
Two probe endpoints are exposed for orchestrators and uptime monitoring
(responses use the IETF application/health+json format):
| Endpoint | Purpose | Healthy | Unhealthy |
|---|---|---|---|
/livez | Liveness — the app process is up. Use it for restart decisions. | 200 | — |
/readyz | Readiness — database reachable and migrations applied. Gate traffic. | 200 | 503 |
/livez stays up during maintenance mode; /readyz returns 503 so traffic
drains while the instance is not ready.
How it works
One container runs everything under Supervisor — Nginx, PHP-FPM, the scheduler
(recurring transactions, automatic exchange-rate updates) and a queue worker for
background jobs. SQLite lives in /data; no external database, cache or queue
service is required.
Get started
Self-host Savvy — a multi-currency expense tracker that runs as a single Docker container with SQLite. Up and running in one command.
Reverse proxy
Put Savvy behind HTTPS with Traefik or Nginx Proxy Manager. Forwarded-header handling means HTTPS links and real client IPs work with no extra config.