Back to blog
7 min read

Self-Host Trout with Docker in 5 Minutes

Step-by-step guide to deploying Trout on your own server with Docker Compose. Covers installation, configuration, bank sync setup, backups, and troubleshooting.

self-hostingdockerguidesetup

Prefer not to self-host? Trout also offers a free hosted option with no setup required.

This guide gets Trout running on your own hardware with Docker Compose. You'll have a working envelope budgeting app in about 5 minutes, assuming Docker is already installed.

Prerequisites

You need two things:

  1. Docker and Docker Compose installed on your server. Any Linux VPS, macOS machine, Windows with WSL2, or Raspberry Pi 4+ will work. Trout runs four containers that together use roughly 500MB of RAM.
  2. Git to clone the repository.

If you don't have Docker, install it from docs.docker.com/get-docker. Docker Compose is included with Docker Desktop on macOS and Windows. On Linux, install the docker-compose-plugin package.

Step 1: Clone the repository

git clone https://github.com/SSardorf/betterbudget.git
cd betterbudget

Step 2: Configure environment variables

Copy the example environment file:

cp .env.example .env

Open .env in your editor. The only required change is generating an authentication secret:

# Generate a random secret
openssl rand -base64 32

Paste the output as your BETTER_AUTH_SECRET value:

BETTER_AUTH_SECRET=your-generated-secret-here

The default database credentials work out of the box for local use:

DATABASE_URL=postgresql://budget_user:budget_password@localhost:5432/betterbudget

If deploying to a public server, change the database password:

POSTGRES_USER=budget_user
POSTGRES_PASSWORD=a-strong-random-password
POSTGRES_DB=betterbudget

For now, skip the optional variables (GoCardless, OpenAI, etc.). You can add them later without restarting from scratch.

Step 3: Start the stack

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

This builds and starts four containers:

| Container | Purpose | Port | |-----------|---------|------| | betterbudget-postgres | PostgreSQL database | Internal only | | betterbudget-migrations | Runs database migrations, then exits | None | | betterbudget-api | Elysia API server | 3001 | | betterbudget-web | Next.js frontend | 3000 |

The first build takes 2-4 minutes depending on your hardware and internet speed. Subsequent starts are much faster since Docker caches the built images.

Watch the progress:

docker compose -f docker-compose.production.yml logs -f

Wait until you see the web container log something like Ready in Xs. Then open http://localhost:3000 (or your server's IP) in a browser.

Step 4: Create your account

Open the app and sign up. That's it. You're running a self-hosted envelope budgeting app.

Start by creating your accounts (checking, savings, credit cards), setting up category groups and categories, and entering your current balances. The budgeting workflow is the same as YNAB: income arrives, you allocate it across categories, spending deducts from those categories.

Optional: Set up bank sync

Trout connects to 2,500+ European and UK banks through GoCardless Open Banking. To enable it:

  1. Create a free account at bankaccountdata.gocardless.com
  2. Generate API credentials (Secret ID and Secret Key)
  3. Add them to your .env file:
GOCARDLESS_SECRET_ID=your-secret-id
GOCARDLESS_SECRET_KEY=your-secret-key
  1. Restart the API container:
docker compose -f docker-compose.production.yml restart api
  1. In the app, go to your accounts page and use the "Link Bank Account" button to connect your bank.

GoCardless free tier allows connections to a limited number of banks. Check their pricing page for details on premium tier coverage.

Note: Bank sync currently covers European and UK banks only. US bank sync is not yet available.

Optional: Set up the AI assistant

The AI assistant lets you ask questions about your finances in plain English. It requires an OpenAI API key:

  1. Get an API key from platform.openai.com
  2. Add it to .env:
OPENAI_API_KEY=sk-your-api-key
  1. Restart the API container:
docker compose -f docker-compose.production.yml restart api

API costs are minimal for personal use -- typically a few cents per month. The assistant uses function calling to query your database, so your financial data isn't sent as training data.

Backing up your data

Your financial data lives in a PostgreSQL database stored in a Docker volume called betterbudget_postgres_data. Back it up regularly.

Option A: pg_dump (recommended)

docker exec betterbudget-postgres pg_dump -U budget_user betterbudget > backup_$(date +%Y%m%d).sql

This creates a SQL dump file. To restore:

docker exec -i betterbudget-postgres psql -U budget_user betterbudget < backup_20260208.sql

Option B: Volume backup

docker compose -f docker-compose.production.yml stop
docker run --rm -v betterbudget_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup_$(date +%Y%m%d).tar.gz /data
docker compose -f docker-compose.production.yml up -d

Set up a cron job to run backups daily:

# Add to crontab (crontab -e)
0 3 * * * docker exec betterbudget-postgres pg_dump -U budget_user betterbudget > /backups/betterbudget_$(date +\%Y\%m\%d).sql

Updating to a new version

When a new version is released:

cd betterbudget
git pull
docker compose -f docker-compose.production.yml build
docker compose -f docker-compose.production.yml up -d

The migrations container runs automatically on startup, applying any new database migrations. Your data is preserved across updates.

Back up your database before updating. Schema migrations are tested, but a backup protects you against the unexpected.

Exposing to the internet

If you want to access Trout from outside your local network, you need a reverse proxy with HTTPS. Do not expose ports 3000 or 3001 directly to the internet without TLS.

Common options:

  • Caddy -- simplest setup, automatic HTTPS via Let's Encrypt
  • Traefik -- good if you already use it for other Docker services
  • nginx -- most flexible, more configuration

Example Caddy configuration:

budget.yourdomain.com {
    reverse_proxy localhost:3000
}

api.budget.yourdomain.com {
    reverse_proxy localhost:3001
}

After setting up your reverse proxy, update .env:

NEXT_PUBLIC_API_URL=https://api.budget.yourdomain.com
BETTER_AUTH_URL=https://api.budget.yourdomain.com
ALLOWED_ORIGINS=https://budget.yourdomain.com

Then rebuild and restart:

docker compose -f docker-compose.production.yml up -d --build

Troubleshooting

"Cannot connect to database"

Check if PostgreSQL is healthy:

docker compose -f docker-compose.production.yml ps

The postgres container should show "healthy" status. If it's restarting, check logs:

docker compose -f docker-compose.production.yml logs postgres

Common cause: insufficient disk space or incorrect POSTGRES_PASSWORD in .env.

"Migrations failed"

Check the migrations container logs:

docker compose -f docker-compose.production.yml logs migrations

Migrations run once on startup. If they fail, the API won't start (it depends on successful migration completion). Fix the issue and restart:

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

"Web container starts but shows blank page"

Check that NEXT_PUBLIC_API_URL in .env points to a URL the browser (not the container) can reach. If you're accessing via http://192.168.1.50:3000, then NEXT_PUBLIC_API_URL should be http://192.168.1.50:3001, not http://localhost:3001.

After changing NEXT_PUBLIC_API_URL, rebuild the web container since it's baked in at build time:

docker compose -f docker-compose.production.yml up -d --build web

"Bank sync not working"

  • Verify GoCardless credentials are correct in .env
  • Check the API logs: docker compose -f docker-compose.production.yml logs api
  • GoCardless has rate limits. If you've hit them, wait until the next day.
  • Bank connections expire after 90 days. Re-authorize if expired.

High memory usage

Trout runs four containers. Expected memory usage:

| Container | RAM | |-----------|-----| | PostgreSQL | ~100-200MB | | API | ~100-150MB | | Web | ~150-200MB | | Migrations | Exits after running |

Total: roughly 350-550MB. If you're running on a machine with 1GB or less of RAM, this may be tight. Consider adding swap space.

What this guide doesn't cover

Automated backups to cloud storage. You'll want to send your pg_dump files somewhere off-server. Rclone, rsync to another machine, or S3-compatible storage are common approaches. This is your responsibility as a self-hoster.

High availability. This is a single-server setup. If the server goes down, your budget app goes down. For personal use, this is fine. If you need uptime guarantees, you'd need to run PostgreSQL in a replication setup and add load balancing, which is well beyond the scope of budgeting.

Monitoring. There's no built-in health dashboard. Docker's healthchecks will restart containers that crash, but if you want alerts when things break, you'll need to add monitoring yourself (Uptime Kuma, Healthchecks.io, or similar).

Self-hosting means taking responsibility for your infrastructure. The trade-off is clear: more control and privacy, more maintenance work. For most self-hosters, this is a trade-off they've already made with other services, and Trout is just one more Docker Compose service in the stack.