Skip to main content

Self-Hosting

This guide covers a clean production-style self-hosted setup with Docker Compose.

Prerequisites​

  • Docker and Docker Compose installed
  • A server or VPS with open ports for your reverse proxy
  • A domain pointing to your server

1) Prepare the project files​

mkdir cashlytics && cd cashlytics
curl -O https://raw.githubusercontent.com/aaronjoeldev/cashlytics-ai/main/docker-compose.selfhost.yml
curl -o .env https://raw.githubusercontent.com/aaronjoeldev/cashlytics-ai/main/.env.example

2) Configure required values in .env​

Set these before first start:

POSTGRES_PASSWORD=replace_with_long_random_password
AUTH_SECRET=replace_with_long_random_secret
NEXT_PUBLIC_APP_URL=https://your-cashlytics-domain.tld
CRON_SECRET=replace_with_long_random_secret

Optional, with Compose defaults:

AUTH_TRUST_HOST=true      # default true, keep it behind a domain or proxy
SINGLE_USER_MODE=true # default true: only the first account can register

Generation helpers:

openssl rand -base64 32   # AUTH_SECRET
openssl rand -hex 32 # CRON_SECRET

Notes:

  • docker-compose.selfhost.yml builds DATABASE_URL from POSTGRES_PASSWORD. A DATABASE_URL line in .env is ignored by this stack.
  • CRON_SECRET is required because the cron container always runs. See Scheduled Jobs.
  • With SINGLE_USER_MODE=true, the first visitor who opens the registration page creates the account, and registration closes afterwards. Register your account before you expose the instance publicly.

3) Start the stack​

docker compose -f docker-compose.selfhost.yml up -d

This starts:

  • cashlytics app container (cashlytics-app)
  • postgres database container (cashlytics-postgres, PostgreSQL 16)
  • cashlytics-cron scheduler container for payment reminders and the assistant's daily insights

4) Verify services​

docker compose -f docker-compose.selfhost.yml ps
docker compose -f docker-compose.selfhost.yml logs -f cashlytics

The app container has a health check (wget against http://127.0.0.1:3000, 30 s interval, 40 s start period). The cron container waits until the app is healthy, so it may appear later than the other containers while migrations run.

Then open your configured app URL.

5) Optional features​

  • AI Assistant, Receipt Scanner, AI CSV matching: set OPENAI_API_KEY
  • Email (password reset, welcome mails): set all four SMTP_* connection variables or EMAIL_TRANSPORT=sendmail. See Emails.
  • Push reminders: set VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT (all three). CRON_SECRET is already required by the base setup.
  • Schedules: adjust NOTIFICATION_SCHEDULE and INSIGHTS_SCHEDULE (UTC)

Reverse proxy notes​

  • Keep AUTH_TRUST_HOST=true when running behind a domain/proxy
  • Route public traffic to the app container on port 3000
  • The Compose file publishes 3000:3000 on all interfaces. If a reverse proxy runs on the same host, change the mapping to 127.0.0.1:3000:3000 or block the port in your firewall so the app is only reachable through the proxy.
  • Keep PostgreSQL private (no public 5432 unless strictly required)

Updating Cashlytics​

  1. Back up the database first (see below).
  2. Pull and restart:
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d

Database migrations run automatically when the app container starts. The entrypoint retries up to 5 times and exits with an error if migrations fail, in which case the app does not start; check docker compose logs cashlytics.

The Compose file uses the latest tag. To control when you upgrade, pin a release in docker-compose.selfhost.yml, for example ghcr.io/aaronjoeldev/cashlytics-ai:v0.10.0. Available tags are vX.Y.Z, latest, and the short commit SHA. Tags starting with dev- are pre-releases from the dev branch and are not meant for production.

Backup and restore​

Everything Cashlytics stores, including uploaded documents (stored as base64 in the documents table), lives in the PostgreSQL database. The only data volume is postgres_data (Compose prefixes it with the project name, usually the directory name, for example cashlytics_postgres_data).

Create a dump:

docker exec cashlytics-postgres pg_dump -U cashlytics cashlytics | gzip > cashlytics-$(date +%F).sql.gz

Restore into a fresh, empty database (stop the app first so nothing writes during the restore):

docker compose -f docker-compose.selfhost.yml stop cashlytics cashlytics-cron
gunzip -c cashlytics-2026-01-01.sql.gz | docker exec -i cashlytics-postgres psql -U cashlytics cashlytics
docker compose -f docker-compose.selfhost.yml start cashlytics cashlytics-cron

If the target database already contains tables, drop and recreate it first (dropdb / createdb via docker exec) or restore into a new volume.

Minimum backup target:

  • A database dump (cashlytics DB)
  • The .env file, stored securely (you need AUTH_SECRET to keep sessions valid and your secrets to restart the stack)

Store backups off-host and test a restore occasionally.

Demo mode​

The app repository includes docker-compose.demo.yml for a public demo with pre-loaded data. It is not meant for real data.

docker compose -f docker-compose.demo.yml up -d

It starts three services under the project name cashlytics-demo:

  • postgres (volume demo_postgres_data)
  • app (ghcr.io/aaronjoeldev/cashlytics-ai:latest)
  • demo-seeder, which waits for the migrations, loads scripts/seed-demo.sql, and resets the database to the demo state every day at midnight UTC

Log in with demo@cashlytics.online and the password admin123456789. Registration is closed (SINGLE_USER_MODE is fixed to true). There is no cron container in the demo stack.

Optional variables: APP_PORT (default 3000), APP_URL (public URL, passed to the app as NEXT_PUBLIC_APP_URL), POSTGRES_PASSWORD (default cashlytics_demo), AUTH_SECRET (default demo_secret_change_in_production), OPENAI_API_KEY.

Warning: the seed script deletes all data in the target database. Never run seed-demo.sql or the demo stack against a database that holds real data, and set your own AUTH_SECRET if the demo is reachable from the internet.