Self-Hosting

Docker Deployment

Deploy Retrospend on your own server using Docker Compose. This is the recommended and officially supported self-hosting method.

Prerequisites

  • Docker Engine 24+ and Docker Compose v2 installed
  • • A server or VPS with at least 1 GB RAM (2 GB+ recommended if using local Ollama)
  • • A domain name pointed at your server (for HTTPS via a reverse proxy)
  • • Ports 80 and 443 available (if using Caddy or Nginx for TLS)

AI import is optional

The bank statement import feature requires an LLM (local Ollama or OpenRouter). If you don't plan to use it, simply omit the LLM environment variables from the sidecar service.

Setup

Step 1: Create your .env file

Create a .env file in your deployment directory:

.env
# Generate with: openssl rand -base64 32
AUTH_SECRET=""

PUBLIC_URL="https://your-domain.com"

# Database
POSTGRES_PASSWORD="changeme"
POSTGRES_USER="postgres"
POSTGRES_DB_NAME="retrospend"

# Worker
WORKER_API_KEY="your-worker-api-key"

# AI Import - pick one:
# Option A: OpenRouter (external, recommended for most users)
# OPENROUTER_API_KEY=""
# OPENROUTER_MODEL="qwen/qwen-2.5-7b-instruct"

# Option B: Local Ollama (see docker-compose.local-ai.yml)
# LLM_MODEL="qwen2.5:7b"

# Optional: email notifications
# SMTP_HOST=""
# SMTP_PORT=587
# SMTP_USER=""
# SMTP_PASSWORD=""
# EMAIL_FROM="Retrospend <[email protected]>"
# UNSUBSCRIBE_SECRET=""  # Generate with: openssl rand -base64 32

Generate strong secrets

Run openssl rand -base64 32 for each secret value. Never reuse secrets across services.

Step 2: Choose your docker-compose.yml

Pick the variant that matches your setup:

Uses OpenRouter for AI-powered bank import. Requires an API key but no GPU. The free tier is sufficient for most personal use.

docker-compose.ymlyaml
services:
  retrospend:
    image: synzeit/retrospend:latest
    container_name: retrospend
    restart: unless-stopped
    environment:
      AUTH_SECRET: ${AUTH_SECRET}
      DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB_NAME}"
      PUBLIC_URL: "https://your-domain.com"
      WORKER_API_KEY: ${WORKER_API_KEY}
      SIDECAR_URL: "http://sidecar:8080"
    ports:
      - "1997:1997"
    volumes:
      - uploads:/data/uploads
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    container_name: retrospend-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  sidecar:
    image: synzeit/retrospend-sidecar:latest
    container_name: retrospend-sidecar
    restart: unless-stopped
    environment:
      DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB_NAME}"
      WORKER_API_KEY: ${WORKER_API_KEY}
      LOG_LEVEL: "info"
      BACKUP_DIR: "/backups"
      BACKUP_RETENTION_DAYS: "${BACKUP_RETENTION_DAYS:-30}"
      BACKUP_CRON: "${BACKUP_CRON:-0 3 * * *}"
      OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
      OPENROUTER_MODEL: ${OPENROUTER_MODEL:-qwen/qwen-2.5-7b-instruct}
      ENRICH_BATCH_SIZE: "${ENRICH_BATCH_SIZE:-20}"
      ENRICH_CONCURRENCY: "${ENRICH_CONCURRENCY:-3}"
      PDF_CONCURRENCY: "${PDF_CONCURRENCY:-3}"
    volumes:
      - backup_data:/backups
      - sidecar_data:/app/data
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
  uploads:
  sidecar_data:
  backup_data:

Step 3: Start the stack

bashbash
docker compose up -d

The first start will pull images and run database migrations automatically. Check logs with:

bashbash
docker compose logs -f retrospend

Step 4: Access your instance

Retrospend listens on port 1997. You can access it directly at http://your-server:1997 or put it behind a reverse proxy for HTTPS. See the Reverse Proxy guide for Nginx and Caddy configuration.

Services Overview

ServiceImagePurpose
retrospendsynzeit/retrospendMain web app (Next.js)
postgrespostgres:16-alpinePrimary database
sidecarsynzeit/retrospend-sidecarBackground jobs + AI bank import
ollamaollama/ollamaLocal LLM (optional)

Data Persistence

All data is stored in named Docker volumes: postgres_data for the database and uploads for uploaded files (avatars, project images, receipts). Back these up regularly, as they contain all your financial data.

bashbash
# Backup PostgreSQL
docker exec retrospend-postgres pg_dump -U postgres retrospend > backup.sql

# List volumes
docker volume ls | grep retrospend

First User & Registration

On a fresh instance, navigate to /signup to create your account. The first registered user can be promoted to admin from the admin panel at /admin.

Note

To disable public registration after setup, you can remove the signup route or restrict access at the reverse proxy level.