- Python 45.5%
- HTML 28%
- CSS 17.3%
- JavaScript 5.3%
- Shell 3%
- Other 0.9%
|
All checks were successful
build-and-push / build (push) Successful in 59s
|
||
|---|---|---|
| .forgejo/workflows | ||
| core | ||
| ledger | ||
| locale | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| LICENSE | ||
| manage.py | ||
| README.md | ||
| requirements.txt | ||
| startup.py | ||
Ledger
A self-hosted personal finance tracker. Create multiple ledgers (e.g. per bank account, per currency), record income and expenses, and see your running balance.
Built with Django. Small, self-hosted, deployable as a single Docker container. SQLite for simple setups, Postgres when you want it.
Features
- Multiple ledgers per user, each with its own currency and starting balance
- Income and expense transactions with automatic running balance
- Per-user, customizable categories (seeded with a sensible default set)
- Ledger sharing — share a ledger with other users, optionally granting edit rights; shared ledgers appear with a "Shared" badge on the dashboard and an overlapping avatar stack in the ledger header; the share list shows user avatars and updates without closing the settings modal
- Dashboard totals — currency-grouped sum across all visible ledgers
- Modal forms for quick ledger edits and transaction entry
- Toast notifications for feedback instead of static banners
- Custom confirm dialogs for all destructive actions (no browser
confirm()) - 12 built-in themes — 6 dark + 6 light (Midnight, Ocean Breeze, Forest, Sunset, Lavender, Monochrome) with a theme-aware favicon
- Internationalization: English, German, French — selectable per user
- Regional formatting profiles (Europe / US / UK) for dates, numbers, currencies, powered by Babel
- Profile pictures — upload an avatar (center-cropped, resized to 200×200) displayed in the navbar
- Instance branding — admins can set a custom name shown in the header, footer, and browser tab
- User roles and admin panel — admins get Settings tabs for General (branding) and Users (add/block/unblock users, reset passwords)
- Version display and update check — footer shows the running version; a badge appears when a newer tag exists in the container registry (configurable, 6h refresh)
- Startup diagnostics — clear, actionable error messages on boot when the database is unreachable, credentials are wrong, or migrations fail
- Security hardening — HSTS, secure cookies, required secret key in production, login rate limiting (django-axes: 5 attempts / 30min lockout), strengthened password validators
- Health endpoint —
GET /healthzfor reverse-proxy and orchestrator liveness checks (returns 200/503, no auth)
Quick start (Docker)
mkdir ledger && cd ledger
curl -O https://forgejo.marcelleismann.de/lecram345/ledger/raw/branch/main/docker-compose.yml
curl -O https://forgejo.marcelleismann.de/lecram345/ledger/raw/branch/main/.env.example
mv .env.example .env
# edit .env — set DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS, admin credentials
docker compose pull
docker compose up -d
The app listens on 127.0.0.1:8000 by default — point your reverse proxy (nginx, Caddy, Traefik, …) at it.
Generating a secret key
python -c 'import secrets; print(secrets.token_urlsafe(64))'
Admin user
Set DJANGO_SUPERUSER_USERNAME, DJANGO_SUPERUSER_EMAIL, and DJANGO_SUPERUSER_PASSWORD in .env. On first startup the entrypoint creates the user if it doesn't already exist. Leaving the vars unset skips auto-creation — you can always create one manually:
docker compose exec ledger python manage.py createsuperuser
Configuration
All configuration is via environment variables, loaded from .env.
| Variable | Required | Default | Description |
|---|---|---|---|
DJANGO_SECRET_KEY |
yes | — | Long random string. Never commit. |
DJANGO_DEBUG |
no | false |
Set to true only for local development. |
DJANGO_ALLOWED_HOSTS |
yes | — | Comma-separated list, e.g. ledger.home. |
DJANGO_CSRF_TRUSTED_ORIGINS |
yes | — | Comma-separated, scheme included, e.g. https://ledger.home. |
SQLITE_PATH |
no | /data/db.sqlite3 |
Path inside the container. Persisted via the ledger-data volume. |
POSTGRES_DB |
no | — | If set, Postgres is used and SQLITE_PATH is ignored. |
POSTGRES_USER |
iff Postgres | — | |
POSTGRES_PASSWORD |
iff Postgres | — | |
POSTGRES_HOST |
no | localhost |
Use host.docker.internal to reach a Postgres on the Docker host, or the container/service name if both are on the same Docker network. |
POSTGRES_PORT |
no | 5432 |
|
DJANGO_SUPERUSER_USERNAME |
no | — | If set with password, creates admin on first boot (idempotent). |
DJANGO_SUPERUSER_EMAIL |
no | "" |
|
DJANGO_SUPERUSER_PASSWORD |
no | — | Required together with username for auto-creation. |
LEDGER_TAG |
no | latest |
Image tag pulled by compose. |
LEDGER_UPDATE_CHECK |
no | true |
When true, the app queries the registry for newer semver tags every 6h and shows an "available" badge in the footer to signed-in users. Set false to disable outbound checks. |
LEDGER_UPDATE_CHECK_URL |
no | Forgejo packages API for lecram345/ledger |
Override if you host the image elsewhere. The default uses the packages REST API, which doesn't require auth for public packages (the Docker Registry /v2/ endpoint does). |
LEDGER_UPDATE_CHECK_PACKAGE |
no | ledger |
Package name to match in the response (used when the API returns entries for multiple packages). |
LEDGER_UPDATE_CHECK_TOKEN |
no | — | Bearer token if you point at an auth-required endpoint. Leave unset for public. |
Switching to Postgres
Uncomment and fill the POSTGRES_* block in .env. The app uses Postgres whenever POSTGRES_DB is set; otherwise it falls back to SQLite. No code change required.
If Postgres runs in a separate Docker stack, create a shared external network and add it to both compose files so the Ledger container can resolve the Postgres hostname.
Local development
python -m venv .venv
source .venv/bin/activate # fish: source .venv/bin/activate.fish
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
Then open http://127.0.0.1:8000.
Building the image locally
docker build -t ledger:dev .
docker run --rm -p 8000:8000 \
-e DJANGO_SECRET_KEY=dev \
-e DJANGO_ALLOWED_HOSTS=localhost \
-e DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000 \
ledger:dev
CI (Forgejo Actions, .forgejo/workflows/build.yml) builds and pushes to the Forgejo registry on every push to main and on v* tags.
Data & backups
The SQLite database lives in the ledger-data named volume at /data/db.sqlite3. Back it up with:
docker compose exec ledger sqlite3 /data/db.sqlite3 ".backup '/data/backup.sqlite3'"
docker compose cp ledger:/data/backup.sqlite3 ./backup-$(date +%F).sqlite3
For Postgres deployments, back up using your usual pg_dump workflow against the Postgres instance.
License
Copyright (C) 2026 Marcel Leismann.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3.0 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for the full text.
AGPL-3.0 is copyleft and covers network use: if you run a modified version of this software as a service, you must offer the modified source code to its users.