Skip to content

anym001/pocketlog

Repository files navigation

PocketLog

Tests License: AGPL v3 Release GHCR Docker Hub

A privacy-first household budget book you fully self-host — a fast PWA on every device, backed by a real scoped API for automation. No cloud, no accounts, no tracking, no telemetry.

Runs in the browser on all major platforms — iOS, Android, macOS, Windows, Linux — and installs to the home screen like a native app.

Designed for private self-hosting: data lives only on your own server — by default in an embedded SQLite file (no separate database required), optionally in an external MariaDB, all in your own container. Every asset (fonts, icons, JS libraries) is served from your own origin, so the app makes no CDN calls and no external connections at all.

Screenshots

Transaction ledger Category management Recurring rules Category analysis report Savings goals Per-category budgets

PocketLog on desktop, dark theme

Mobile — the app's primary form factor: ledger, categories, recurring rules, category analysis, savings goals, per-category budgets. Below: the desktop sidebar layout in dark theme.

Contents

Features

  • Transactions — income & expenses with date, amount, category, and tags
  • Categories — freely definable (name, icon, color); a default set is created on first use
  • Tags — free-form labels per transaction; rename or delete centrally
  • Goals — savings targets and debt tracker in one: link a goal to a category and PocketLog calculates your progress automatically from that category's transactions. Goals are tracked separately and do not affect your balance
  • Budgets — per-category spending caps: set a limit per category over a monthly, quarterly, or yearly period and PocketLog shows how much of it you've used in the current period, with the bar turning red once you go over. Calculated automatically from that category's expenses; budgets do not affect your balance, and a category can have both a goal and a budget
  • Recurring transactions — define rules for transactions that repeat (rent, subscriptions, salaries) and PocketLog books them automatically at the next due date. Frequencies daily / weekly / monthly / quarterly / yearly with a configurable interval, anchored to a weekday or day of the month. Optional end date or maximum number of occurrences; individual upcoming occurrences can be skipped. Rules can be paused and resumed at any time, and tags can be assigned to a rule so every auto-booked transaction inherits them. A small icon marks auto-booked transactions in the list; a notification reports how many were added since the last visit
  • Reports & Charts — monthly/yearly overview, category and tag reports, trend view and forecast
  • Bulk actions — select multiple transactions at once to reassign a category, add or remove tags, or delete them in one step
  • Search — full-text, category, and tag filtering in the transaction list
  • CSV Import / Export — UTF-8 or CP1252, max. 5 MB; export all transactions as semicolon-delimited CSV. Import is also available over the API for automated workflows (see API Access), with duplicate rows skipped on re-import so overlapping bank exports don't double-book
  • API access — per-user API keys (bearer tokens) with scoped permissions (read, import, write) for programmatic access; lets external tools drive the CSV import — or full read/write sync — without a browser session (see API Access)
  • Offline capability — app works without a connection; changes sync automatically when back online
  • Themes — Light, Dark, System (saved in settings)
  • Language & Currency — German/English with regional variants (de-DE, de-AT, de-CH, en-GB, en-US) control both translation and date/number formatting; display currency (EUR, USD, GBP, CHF, JPY) freely selectable. Configurable per user; instance default via DEFAULT_LOCALE / DEFAULT_CURRENCY (see Configuration)
  • Multi-user — each identity has its own data; admin creates additional users
  • Built-in login — username/password with admin role, setup flow, and brute-force protection

Requirements

  • Docker (or Podman)
  • Optional: MariaDB 10.6+ (external instance) — only if you do not want to use the built-in SQLite database

Quick Start

By default, PocketLog uses an embedded SQLite database at /config/db/pocketlog.db — no separate database, no DB variables required. Mount /config to the host so that data survives container updates.

1. Start the container

docker run -d \
  --name pocketlog \
  -p 8000:8000 \
  -e PUID=1000 -e PGID=1000 \
  -e TZ=Europe/Berlin \
  -v /mnt/user/appdata/pocketlog:/config \
  ghcr.io/anym001/pocketlog:latest

PUID/PGID determine which host user owns the files under /config (see Configuration). On Unraid, typically PUID=99 / PGID=100.

2. Initial setup

On first access (http://<host>:8000) a setup screen appears. Create the first admin account (username + password, minimum 12 characters with upper/lowercase letters, a number, and a special character). Additional users are created by the admin under Settings → User Management.

External MariaDB (optional)

If you prefer to run an external MariaDB, create a database there …

CREATE DATABASE pocketlog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pocketlog'@'%' IDENTIFIED BY 'your-password';
GRANT ALL ON pocketlog.* TO 'pocketlog'@'%';
FLUSH PRIVILEGES;

… and set the DB_* variables at startup. As soon as any DB_* variable is set, PocketLog switches to MariaDB (instead of SQLite); DB_PASSWORD is then required:

docker run -d \
  --name pocketlog \
  -p 8000:8000 \
  -e DB_HOST=mariadb \
  -e DB_NAME=pocketlog \
  -e DB_USER=pocketlog \
  -e DB_PASSWORD=your-password \
  -e TZ=Europe/Berlin \
  ghcr.io/anym001/pocketlog:latest

MariaDB 11.6+ (innodb_snapshot_isolation): From MariaDB 11.6 the innodb_snapshot_isolation setting can be ON by default. Under it, two requests writing the same row at once (e.g. the offline outbox reconnecting and replaying queued writes, plus the per-request session refresh) get a transient ERROR 1020 … try restarting transaction. PocketLog retries these automatically, so no action is required. If you'd rather remove the errors at the source, set innodb_snapshot_isolation = OFF in your MariaDB config — PocketLog does not rely on snapshot isolation. Standalone InnoDB before 11.6 and the default SQLite backend are unaffected.

Configuration

Variable Default Description
PUID 1000 Host user ID that owns files under /config (Unraid: 99)
PGID 1000 Host group ID for /config (Unraid: 100)
SQLITE_PATH /config/db/pocketlog.db Path to the SQLite file (only without DB_*)
DB_HOST mariadb MariaDB option only: hostname or IP. Setting any DB_* variable switches from SQLite to MariaDB.
DB_PORT 3306 MariaDB option only: port
DB_NAME pocketlog MariaDB option only: database name
DB_USER pocketlog MariaDB option only: database user
DB_PASSWORD MariaDB option only: password (required once MariaDB is active)
DATABASE_URL Advanced: full database connection URL; overrides DB_*/SQLite (e.g. for SSL, socket, custom driver)
TZ UTC Container timezone
LOG_LEVEL INFO Log level (DEBUG, INFO, WARNING, ERROR). Audit events (logins, lockouts, admin actions) are at INFO/WARNING.
LOG_FORMAT text Log format: text (human-readable line) or json (one structured JSON object per line, for aggregators like Loki/ELK). Applies to both docker logs and LOG_FILE.
LOG_FILE Writes logs in addition to docker logs into this file (rotating). Recommended: /config/logs/audit.log with a mounted /config directory, to retain logs across container updates (see Logging & Audit Trail).
LOG_FILE_MAX_BYTES 10485760 Log file rotation size in bytes (default 10 MB).
LOG_FILE_BACKUPS 5 Number of rotated log files to retain.
DEFAULT_LOCALE de-DE Starting locale for new accounts (BCP-47: de-DE, de-AT, de-CH, en-GB, en-US). Each user can override it.
DEFAULT_CURRENCY EUR Starting currency for new accounts (ISO 4217: EUR, USD, GBP, CHF, JPY). Display only, overridable per user.
SESSION_COOKIE_SECURE auto Controls the Secure flag on session cookies. auto (default): set when the browser-facing transport is HTTPS, detected via X-Forwarded-Proto (honoured only from a trusted proxy, see TRUSTED_PROXIES) or the raw request scheme — works for direct HTTP (LAN / first-run) and HTTPS-proxy setups without any change. 1: always set (use when your proxy does not forward X-Forwarded-Proto). 0: never set.
TRUSTED_PROXIES (private ranges) Comma-separated IPs or CIDR ranges of trusted reverse proxies (e.g. 172.16.0.0/12,192.168.1.1), or * to trust all peers. Decides which sources may set X-Real-IP/X-Forwarded-For (audit log) and X-Forwarded-Proto (Secure-cookie detection). Empty (default) trusts the standard private/loopback ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1/128, fe80::/10), so a container on a private network behind a proxy works out of the box; an explicit list replaces those defaults.
LOGIN_IP_LOCKOUT_THRESHOLD 20 Failed logins/setup attempts from one source IP before the per-IP backoff starts. Deliberately above the per-user threshold — a household behind one NAT/proxy IP shares this budget.
LOGIN_IP_LOCKOUT_MAX_SECONDS 600 Cap for the per-IP backoff (doubles per failure beyond the threshold).
LOGIN_IP_FAILURE_WINDOW_SECONDS 900 Failures older than this no longer count toward the per-IP threshold.
SESSION_LIFETIME_HOURS 24 Session duration without "Stay logged in"
SESSION_REMEMBER_DAYS 30 Session duration with "Stay logged in"
SESSION_ABSOLUTE_DAYS 7 Maximum session duration (standard)
SESSION_REMEMBER_ABSOLUTE_DAYS 90 Maximum session duration (remember-me)

Reverse Proxy

PocketLog runs on port 8000 and ships with its own login — no upstream identity provider required. Any reverse proxy can sit in front of it (nginx, Caddy, Traefik …). Nginx example:

server {
    listen 443 ssl;
    server_name pocketlog.example.com;

    location / {
        proxy_pass         http://localhost:8000;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

With this setup PocketLog detects HTTPS automatically via X-Forwarded-Proto and sets the Secure cookie flag accordingly — no extra configuration needed, as long as the proxy sits on a private network (trusted by default).

X-Forwarded-Proto and X-Real-IP/X-Forwarded-For are only honoured when the proxy's IP is trusted (TRUSTED_PROXIES). The default already trusts the standard private/loopback ranges, so a proxy on a Docker bridge or LAN works out of the box. Override TRUSTED_PROXIES only to narrow it down to a specific proxy (e.g. -e TRUSTED_PROXIES=172.17.0.5), or set -e TRUSTED_PROXIES=* for a single-proxy setup where the container port is not directly reachable from untrusted networks.

Login & Security

  • Password policy: minimum 12 characters with upper/lowercase letters, a number, and a special character
  • Brute-force protection: after several failed attempts an automatic lockout period kicks in; admins can lift it via Reset Password
  • Per-IP throttle: failed logins (and setup probes) are additionally counted per source IP with an exponential backoff, so distributing guesses across many usernames doesn't dodge the lockout and a third party can't hammer a known account into a permanent lockout for free (tunable via LOGIN_IP_*, see Configuration)
  • Session: active for 24 hours by default, 30 days with "Stay logged in"; after an absolute maximum of 7 or 90 days a new login is required

API Access

Beyond the browser, PocketLog exposes its API for programmatic access via API keys (bearer tokens). The primary use case is automated CSV import — for example a bridge tool that converts your bank's export and uploads it on a schedule — without needing an interactive browser session.

Creating a key

Open Settings → API keys, enter a name, choose a scope, and create the key. The key (format plk_…) is shown only once — copy it right away; PocketLog stores only its hash and cannot recover the original. You can revoke a key at any time from the same screen.

Each key carries a single scope, in a hierarchy where the higher tier includes the lower ones:

Scope Grants
read all read endpoints (transactions, categories, tags, CSV export, JSON backup export)
import CSV import only (POST /api/import/csv)
write full data access — every create / update / delete plus import and read

write is the top data tier; there is intentionally no admin API scope. User management, the bulk-delete endpoints, and API-key management stay session-only — they are never reachable with a bearer token, so a leaked key can never take over the instance or wipe all data. Keys are bound to the user who created them and respect the same per-user data isolation as the web UI.

Importing a CSV via the API

Send the file as multipart form data with the key in the Authorization header:

curl -X POST https://pocketlog.example.com/api/import/csv \
  -H "Authorization: Bearer plk_your_key_here" \
  -F "file=@transactions.csv"

Response:

{ "imported": 12, "skipped": 0, "deduped": 0, "errors": [] }

The CSV uses a header row with the columns date, amount, type, description, category, and tags; the delimiter (;, ,, tab, or |) is auto-detected. A ready-made example is available under Settings → CSV import.

Deduplication: every imported row gets a fingerprint derived from its date, amount, description, and type. Re-importing an overlapping export skips rows that are already present instead of booking them twice — those are counted under deduped:

{ "imported": 0, "skipped": 0, "deduped": 12, "errors": [] }

Unlike browser requests, bearer-token calls do not require the CSRF header (a browser never sends the token automatically, so CSRF does not apply). All other security — per-user isolation, the 5 MB / row limits, and formula-injection guards — applies identically.

Logging & Audit Trail

PocketLog logs security-relevant events (logins including failed attempts, lockouts, password changes, admin user actions, deletion of all own data, and create / update / delete / catch-up events for recurring rules). Passwords, hashes, security tokens, or user-supplied free-text (rule names, descriptions, amounts) are never logged.

By default output goes to stdout/stderr, i.e. into docker logs. This survives container restarts, but not an update with docker rm. There are two approaches for a persistent audit trail:

Option A — Log file in the app directory (in-app, simple):

PocketLog follows the common self-hosting convention: a single app directory at /config inside the container, mounted to the host. It holds the SQLite database (/config/db/, unless an external MariaDB is used) and the audit trail (/config/logs/) — one mount covers the entire persistent app state.

docker run -d \
  --name pocketlog \
  -p 8000:8000 \
  -e DB_HOST=mariadb -e DB_NAME=pocketlog \
  -e DB_USER=pocketlog -e DB_PASSWORD=your-password \
  -e LOG_FILE=/config/logs/audit.log \
  -v /mnt/user/appdata/pocketlog:/config \
  ghcr.io/anym001/pocketlog:latest

Writes in addition to docker logs into the file (rotating; size/count via LOG_FILE_MAX_BYTES / LOG_FILE_BACKUPS). The missing directory is created automatically. If the file is not writable, the app continues and logs only to stderr (with a warning). The mounted /config folder persists across container updates.

Unraid: Map /config to /mnt/user/appdata/pocketlog in the template and set LOG_FILE=/config/logs/audit.log — the entire app state then lives under a single path in your appdata share.

Option B — Docker log driver (platform-side, "12-Factor"):

Let the app log to stderr unchanged and leave persistence to the host, e.g. via journald:

docker run -d --name pocketlog \
  --log-driver=journald \
  … (remaining options) …

Logs then land in the systemd journal (journalctl CONTAINER_NAME=pocketlog) and survive container updates without the app needing to manage files.

Option A is most convenient for single instances; Option B is cleaner when a centralised logging infrastructure (journald, syslog, Loki …) is already in place. Both can be combined.

Backup & Restore

Two complementary layers:

Per-account backup (in-app, JSON)

Every user can download a full backup of their own account under Settings → Import/Export → Backup: transactions, categories, tags, goals, budgets, recurring bookings, and settings in one versioned JSON file (GET /api/export/json, also available to read-scoped API keys for scheduled off-site pulls).

Restoring (POST /api/import/json) is a migration/recovery path, not a merge: it only runs against an account without ledger data. On a fresh instance — or after Delete all data — upload the file and the account comes back exactly as exported, including recurring-rule state, so no booking is created twice. Restore is session-only and never reachable with an API key.

Whole-database snapshot (operator, SQLite)

The SQLite database runs in WAL mode — a plain file copy of a running instance is not a valid backup. Use the CLI, which snapshots via SQLite's VACUUM INTO and is safe while the app keeps running:

docker exec pocketlog pocketlog backup
# → /config/db/backups/pocketlog-YYYYMMDD-HHMMSS.db

(The first pocketlog is the container name, the second the CLI launcher inside the image; python -m app.cli backup remains equivalent.)

--output <file-or-directory> picks a different target. A cron line on the host makes it nightly:

15 3 * * * docker exec pocketlog pocketlog backup

Restore by stopping the container and replacing /config/db/pocketlog.db with the snapshot (remove any leftover -wal/-shm files next to it).

With the MariaDB option, back up on the database side instead: mariadb-dump --single-transaction pocketlog.

Emergency Recovery

Forgot the admin password:

docker exec -it pocketlog pocketlog reset-admin-password

Resets the password and any lockout; a new password must be set on the next login. --username <name> resets a specific account instead of the default admin.

Image

Published to GHCR and Docker Hub — use whichever you prefer:

docker pull ghcr.io/anym001/pocketlog:<tag>
docker pull anym001/pocketlog:<tag>      # Docker Hub
Tag Source Purpose
:X.Y.Z Release tag on main Production — fixed version, update when ready
:latest Latest main state Tracks every main merge
:dev Latest dev state Maintainers only — pre-production/staging for testing

Recommendation: Pin production to a fixed :X.Y.Z tag (not :latest), so a new main merge does not silently update your instance — and rollback is trivial (point to the old tag). A staging instance can track :dev (or :latest) to review changes before production. The branching and release workflow is described in CONTRIBUTING.md.

License

PocketLog is released under the GNU Affero General Public License v3.0 (AGPL-3.0). You may use, redistribute, and modify the software — if you offer a (modified) version as a networked service, you must make the complete source code available (AGPL §13). The full license text is in LICENSE.

Copyright (C) 2026 anym001


Built with Claude Code