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.ymlbuildsDATABASE_URLfromPOSTGRES_PASSWORD. ADATABASE_URLline in.envis ignored by this stack.CRON_SECRETis 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:
cashlyticsapp container (cashlytics-app)postgresdatabase container (cashlytics-postgres, PostgreSQL 16)cashlytics-cronscheduler 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 orEMAIL_TRANSPORT=sendmail. See Emails. - Push reminders: set
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY, andVAPID_SUBJECT(all three).CRON_SECRETis already required by the base setup. - Schedules: adjust
NOTIFICATION_SCHEDULEandINSIGHTS_SCHEDULE(UTC)
Reverse proxy notes
- Keep
AUTH_TRUST_HOST=truewhen running behind a domain/proxy - Route public traffic to the app container on port
3000 - The Compose file publishes
3000:3000on all interfaces. If a reverse proxy runs on the same host, change the mapping to127.0.0.1:3000:3000or block the port in your firewall so the app is only reachable through the proxy. - Keep PostgreSQL private (no public
5432unless strictly required)
Updating Cashlytics
- Back up the database first (see below).
- 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 (
cashlyticsDB) - The
.envfile, stored securely (you needAUTH_SECRETto 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(volumedemo_postgres_data)app(ghcr.io/aaronjoeldev/cashlytics-ai:latest)demo-seeder, which waits for the migrations, loadsscripts/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.