Deployn

Sure Finance selbst hosten: Finanz-App mit Docker, Valkey und Caddy

Sure, der Community-Fork von Maybe Finance, mit Docker Compose auf einem Debian-Server installieren: PostgreSQL, Valkey, HTTPS über Caddy, CSV-Import, Backup und Updates.

Sure Finance selbst hosten: Finanz-App mit Docker, Valkey und Caddy hero image
Inhalt
Getestet mit
Sure 0.7.6PostgreSQL 17Valkey 9Caddy 2Docker 29.9Debian 13
Zuletzt geprüft am

Sure ist eine Finanz-App für den eigenen Server. Du siehst Kontostände, Einnahmen, Ausgaben und Budgets in einem Dashboard, und alle Daten bleiben bei dir. Das Projekt ist ein Community-Fork von Maybe Finance. Das Maybe-Team hat die Entwicklung eingestellt, das Repository ist seit Juli 2025 archiviert. Sure führt den Code unter der AGPL-3.0 weiter.

In dieser Anleitung installiere ich Sure 0.7.6 mit Docker Compose auf einem Debian-Server, setze HTTPS mit Caddy davor, importiere Transaktionen per CSV und richte Backup und Updates ein. Docker setze ich als installiert voraus. Falls nicht, hilft Docker Grundlagen weiter.

Was Sure kann

  • Konten: Bankkonten, Investments, Krypto, Immobilien, Fahrzeuge, Kreditkarten und Darlehen.
  • Dashboard: Nettovermögen, Cashflow als Sankey-Diagramm und Ausgaben nach Kategorien.
  • Budgets: Monatsbudgets mit Kategorien.
  • Import: CSV für Transaktionen, außerdem Importvorlagen für Mint, Actual Budget, YNAB und Quicken (QIF).
  • Bankanbindung: Sure kann sich über Enable Banking, SimpleFIN, Lunch Flow, Mercury, Wise, Trade Republic und weitere Anbieter mit Konten verbinden.
  • KI-Assistent: Optional über OpenAI oder Anthropic. Ohne API-Key bleibt der Chat aus, der Rest läuft normal.

Das Projekt hat viele offene Issues, und bei sehr großen Datenmengen gibt es Performance-Probleme. Für ein privates Haushaltsbuch reicht es. Wenn dir ein schlankeres Budget-Tool reicht, schau dir Actual Budget mit Docker an.

Voraussetzungen

  • Ein Server mit Docker und dem Compose-Plugin. Ich nutze hier einen Debian-Server mit 4 GB RAM.
  • Eine Domain oder Subdomain, die per A-Record auf die Server-IP zeigt. Ich nutze sure.example.com.
  • Offene Ports 80 und 443 für Caddy.

Die Doku nennt rund 512 MB RAM als Untergrenze für kleine Hosts. Im Test lagen alle Container zusammen bei etwa 800 MiB.

Schritt 1: Ordner anlegen und Beispiel-Datei laden

Sure bringt eine Beispiel-Compose-Datei mit. Ich lege einen Ordner an und lade sie herunter:

Terminalbash
mkdir -p ~/docker-apps/sure
cd ~/docker-apps/sure
curl --fail --location --silent --show-error --output compose.yml https://raw.githubusercontent.com/we-promise/sure/main/compose.example.yml

Die Datei startet diese Dienste:

  • web: die Rails-App auf Port 3000
  • worker: dasselbe Image mit Sidekiq für Hintergrundjobs wie Syncs und Imports
  • db: PostgreSQL
  • redis: der Job-Speicher

Dazu kommen die Dienste backup und backup-offsite, die nur mit einem Profil starten.

Schritt 2: Geheimnisse in die .env schreiben

Zwei Werte brauchst du: SECRET_KEY_BASE für die Verschlüsselung und POSTGRES_PASSWORD für die Datenbank. Beide erzeuge ich direkt beim Schreiben der Datei:

Terminalbash
printf 'SECRET_KEY_BASE="%s"\nPOSTGRES_PASSWORD="%s"\n' "$(openssl rand -hex 64)" "$(openssl rand -hex 24)" > .env

Sichere die .env an einem zweiten Ort, zum Beispiel in deinem Passwortmanager. Ohne SECRET_KEY_BASE sind verschlüsselte Daten in der Datenbank nicht mehr lesbar. Das Datenbank-Passwort wirkt nur beim allerersten Start der Datenbank. Ändern kannst du es später nicht über die .env allein.

Schritt 3: compose.yml anpassen

Ich passe die Beispiel-Datei an. Unten steht sie komplett, die markierten Zeilen sind meine Änderungen. Kommentare habe ich gekürzt.

  • Geheimnisse: Die Beispiel-Datei hat Standardwerte für SECRET_KEY_BASE und das Datenbank-Passwort. Diese Werte sind öffentlich. Ich nehme sie raus, damit Sure ohne eigene Werte in der .env nicht startet.
  • PostgreSQL 17: Statt postgres:16 nutze ich postgres:17. Der backup-Dienst bringt pg_dump in Version 17 mit und kann keine neuere Datenbank sichern. Mit postgres:18 bricht er mit server version mismatch ab.
  • Valkey: Statt redis:latest nutze ich valkey/valkey:9, den freien Redis-Fork. Der Dienst heißt weiter redis, die REDIS_URL bleibt gültig. Der Healthcheck mit redis-cli ping läuft im Valkey-Image unverändert.
  • HTTPS: Caddy läuft als eigener Dienst im selben Projekt und holt das Zertifikat selbst. Port 3000 bindest du nur noch an localhost, und RAILS_ASSUME_SSL steht auf "true". Mehr zu Caddy steht in Caddy als Reverse Proxy.
  • KI-Key: ${OPENAI_ACCESS_TOKEN:-} verhindert die Warnung, dass die Variable fehlt.
compose.ymlyaml
x-db-env: &db_env
  POSTGRES_USER: ${POSTGRES_USER:-sure_user}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sure_password}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  POSTGRES_DB: ${POSTGRES_DB:-sure_production}

x-rails-env: &rails_env
  <<: *db_env
  SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13}
  SECRET_KEY_BASE: ${SECRET_KEY_BASE}
  SELF_HOSTED: "true"
  RAILS_FORCE_SSL: "false"
  RAILS_ASSUME_SSL: "false"
  RAILS_ASSUME_SSL: "true"
  DB_HOST: db
  DB_PORT: 5432
  REDIS_URL: redis://redis:6379/1
  OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN}
  OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN:-}
  OPENAI_MODEL: ${OPENAI_MODEL:-}
  OPENAI_URI_BASE: ${OPENAI_URI_BASE:-}
  LLM_CONTEXT_WINDOW: ${LLM_CONTEXT_WINDOW:-}
  OPENAI_REQUEST_TIMEOUT: ${OPENAI_REQUEST_TIMEOUT:-60}
  AI_HEALTH_PROBE_TIMEOUT: ${AI_HEALTH_PROBE_TIMEOUT:-}
  AI_HEALTH_PROBE_CACHE_TTL: ${AI_HEALTH_PROBE_CACHE_TTL:-}
  AI_RESPONSE_TIMEOUT: ${AI_RESPONSE_TIMEOUT:-90}
  ASSISTANT_MAX_TOOL_CALL_ITERATIONS: ${ASSISTANT_MAX_TOOL_CALL_ITERATIONS:-}

services:
  web:
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    volumes:
      - app-storage:/rails/storage
    ports:
      - ${PORT:-3000}:3000
      - 127.0.0.1:${PORT:-3000}:3000
    restart: unless-stopped
    environment:
      <<: *rails_env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    dns:
      - 8.8.8.8
      - 1.1.1.1
    networks:
      - sure_net

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
    networks:
      - sure_net

  worker:
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    command: bundle exec sidekiq
    volumes:
      - app-storage:/rails/storage
    restart: unless-stopped
    environment:
      <<: *rails_env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    dns:
      - 8.8.8.8
      - 1.1.1.1
    networks:
      - sure_net

  db:
    image: postgres:16
    image: postgres:17
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      <<: *db_env
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - sure_net

  backup:
    profiles:
      - backup
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    user: "0"
    entrypoint: ["bin/sure-backup"]
    restart: unless-stopped
    volumes:
      - ${BACKUP_DIR:-./backups}:/backups
      - app-storage:/rails/storage
      - .:/config:ro
    environment:
      <<: *db_env
      POSTGRES_HOST: db
      BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
      BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
      INSTANCE_ID: ${INSTANCE_ID:-default}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - sure_net

  backup-offsite:
    profiles:
      - backup-offsite
    image: rclone/rclone:1.71
    restart: unless-stopped
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        : "$${BACKUP_DESTINATION:?set BACKUP_DESTINATION in .env}"
        while true; do
          rclone copy /backups "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --exclude ".tmp-*/**" \
            && rclone delete "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --min-age "$${BACKUP_KEEP_DAYS}d" --include "/*/*-scheduled/**" \
            && rclone rmdirs "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --leave-root \
            || echo "backup-offsite: copy failed; retrying in an hour"
          sleep 3600
        done
    volumes:
      - ${BACKUP_DIR:-./backups}:/backups:ro
    environment:
      BACKUP_DESTINATION: ${BACKUP_DESTINATION:-}
      BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
      INSTANCE_ID: ${INSTANCE_ID:-default}
      RCLONE_CONFIG_S3_TYPE: ${RCLONE_CONFIG_S3_TYPE:-}
      RCLONE_CONFIG_S3_PROVIDER: ${RCLONE_CONFIG_S3_PROVIDER:-}
      RCLONE_CONFIG_S3_ACCESS_KEY_ID: ${RCLONE_CONFIG_S3_ACCESS_KEY_ID:-}
      RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: ${RCLONE_CONFIG_S3_SECRET_ACCESS_KEY:-}
      RCLONE_CONFIG_S3_ENDPOINT: ${RCLONE_CONFIG_S3_ENDPOINT:-}
    networks:
      - sure_net

  redis:
    image: redis:latest
    image: valkey/valkey:9
    restart: unless-stopped
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - sure_net

volumes:
  caddy-data:
  app-storage:
  postgres-data:
  redis-data:

networks:
  sure_net:
    driver: bridge

Schritt 4: Caddyfile anlegen

Lege die Caddyfile neben die compose.yml:

Caddyfilecaddy
sure.example.com {
    reverse_proxy web:3000
}

Ersetze sure.example.com durch deine Domain. Der A-Record muss auf den Server zeigen, und die Ports 80 und 443 müssen offen sein.

Schritt 5: Starten

Terminalbash
sudo docker compose up -d
sudo docker compose ps

Nach etwa 20 Sekunden sind web, worker, db, redis und caddy oben. Danach ist https://sure.example.com mit gültigem Zertifikat erreichbar. Klappt das nicht, prüfe mit sudo docker compose logs caddy, ob Caddy das Zertifikat bekommen hat. Das Image von Sure hat 1,45 GB, PostgreSQL 650 MB und Valkey 174 MB.

Anmeldeseite von Sure über HTTPS

Schritt 6: Konto anlegen und einrichten

Auf der Anmeldeseite wechselst du auf “Konto erstellen” und gibst E-Mail und Passwort ein. Der erste Benutzer wird Super-Admin der Instanz. Danach führt Sure durch drei Schritte: Profil, Einstellungen und Ziele.

Bei den Einstellungen wählst du Sprache, Währung und Datumsformat. Für Deutschland passen “German (de)”, “Euro (EUR)” und DD.MM.YYYY. Das Standardformat ist MM-DD-YYYY.

Einstellungen im Onboarding: Farbschema, Sprache, Währung und Datumsformat

Schließe danach die Registrierung, damit sich niemand sonst auf deiner Instanz anmeldet. Du findest die Option unter Einstellungen > Self-Hosting > Onboarding. Dort stehen “Offen”, “Geschlossen” und “Nur mit Einladung”. Einladungslinks verschickt Sure auf einer selbst gehosteten Instanz nicht per Mail. Du kopierst sie unter Einstellungen > Profilinformationen.

Konten und Transaktionen anlegen

Über “Konto hinzufügen” wählst du den Kontotyp. Bei einem Bankkonto kannst du den Kontostand manuell eingeben oder das Konto über einen Anbieter verknüpfen.

Dialog zum Hinzufügen eines Kontos mit Bankkonto, Investment, Krypto, Immobilie und mehr

Für den manuellen Weg gibst du Name, Kontostand, Eröffnungsdatum und Kontotyp an, zum Beispiel “Girokonto” mit 2.450 Euro.

CSV importieren

Der schnellste Weg zu echten Daten ist ein CSV-Export deiner Bank. Unter Transaktionen > Import > Rohdaten > “Transaktionen importieren” lädst du die Datei hoch oder fügst den Inhalt direkt ein. Das Trennzeichen stellst du auf Semikolon um, wenn deine Bank so exportiert. Sure erwartet im einfachsten Fall diese Spalten:

csv
date,amount,name,category
2026-11-01,2800.0,Gehalt,Einkommen
2026-11-02,-950.0,Miete,Wohnen
2026-11-04,-13.99,Netflix,Abos

Der Import läuft in fünf Schritten:

  1. Hochladen: CSV auswählen oder einfügen, Konto wählen.
  2. Konfigurieren: Spalten für Datum, Betrag, Name und Kategorie zuordnen. Passe das Datumsformat an (hier YYYY-MM-DD) und das Zahlenformat, wenn deine Bank 1.234,56 schreibt.
  3. Bereinigen: Die Tabelle zeigt ungültige Zeilen rot an.
  4. Zuordnen: Kategorien und Tags aus der Datei den Kategorien in Sure zuweisen oder neu anlegen.
  5. Bestätigen: “Import veröffentlichen” legt die Transaktionen an.

Bei meinen 31 Beispielbuchungen waren die Kategorien nach dem Import sofort im Dashboard.

Transaktionsliste mit Kategorien und Beträgen

Dashboard und Budgets

Auf der Startseite siehst du das Nettovermögen und darunter den Cashflow als Sankey-Diagramm. Einnahmen fließen links hinein, rechts verteilen sie sich auf Wohnen, Lebensmittel, Abos und den Überschuss.

Dashboard mit Cashflow-Diagramm, Einnahmen und Ausgaben nach Kategorien

Unter Budgets siehst du pro Monat Einnahmen und Ausgaben, aufgeteilt nach Kategorien. Mit “Neues Budget” legst du Grenzen pro Kategorie fest.

Budget-Seite für November mit Einnahmen, Ausgaben und Kategorien

KI-Assistent aktivieren

Der Chat und die automatische Kategorisierung brauchen einen API-Key. Trage ihn in die .env ein:

Terminalbash
OPENAI_ACCESS_TOKEN="sk-..."

Dann übernimmst du ihn mit sudo docker compose up -d. Unter Einstellungen > Self-Hosting wählst du den KI-Anbieter, OpenAI oder Anthropic (Claude). Jede Anfrage kostet Geld bei deinem Anbieter, also setze dort ein Ausgabenlimit. Die Doku nennt mindestens 1 GB RAM für die KI-Funktionen.

Self-Hosting-Einstellungen mit KI-Assistent und Anbieterauswahl

Backup

Der backup-Dienst aus der Beispiel-Datei sichert Datenbank, hochgeladene Dateien, .env und compose.yml. Er nutzt das Sure-Image, du lädst also nichts nach. Ein Backup auf Zuruf:

Terminalbash
sudo docker compose run --rm backup create

Das Ergebnis liegt unter backups/<Version>/<Datum>-manual/. Der Ordner gehört root, du liest ihn also mit sudo. Ein tägliches Backup um 02:00 UTC, das 7 Tage aufbewahrt wird, startest du so:

Terminalbash
sudo docker compose --profile backup up -d backup

Die Caddyfile ist nicht im Backup. Sie steht in Schritt 4 und ist schnell neu angelegt.

Weil die Backups die .env mit dem SECRET_KEY_BASE enthalten, gehören sie an einen zweiten Ort. Das erledigt der Dienst backup-offsite. Er kopiert den Backup-Ordner mit rclone stündlich zu S3, Backblaze, SFTP oder einem anderen rclone-Ziel und löscht dort geplante Backups nach 7 Tagen. Die Zugangsdaten trägst du in die .env ein, hier beispielhaft für Cloudflare R2:

.env
BACKUP_DESTINATION="s3:mein-bucket"
RCLONE_CONFIG_S3_TYPE=s3
RCLONE_CONFIG_S3_PROVIDER=Cloudflare
RCLONE_CONFIG_S3_ACCESS_KEY_ID=…
RCLONE_CONFIG_S3_SECRET_ACCESS_KEY=…
RCLONE_CONFIG_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com

Dann startest du den Dienst:

Terminalbash
sudo docker compose --profile backup-offsite up -d backup-offsite

Die Kopien liegen unverschlüsselt im Ziel, auch die .env. Lege den Bucket deshalb privat an. Für einen anderen Anbieter als S3 ersetzt du die RCLONE_CONFIG_S3_*-Variablen im Dienst durch die passenden rclone-Variablen.

Zurückspielen:

Terminalbash
sudo docker compose stop web worker
sudo docker compose run --rm backup restore latest
sudo docker compose up -d

Backup mit PostgreSQL 18

Willst du PostgreSQL 18 nutzen, ändere in der compose.yml das Image auf postgres:18 und das Volume auf postgres-data:/var/lib/postgresql, denn ab Version 18 liegen die Daten nicht mehr unter /var/lib/postgresql/data. Den backup-Dienst kannst du dann nicht verwenden, sein pg_dump ist zu alt. Stattdessen sichert ein kleines Skript Datenbank, hochgeladene Dateien, .env, compose.yml und Caddyfile. Das Volume heißt sure_app-storage, weil der Projektordner sure heißt. Heißt dein Ordner anders, passt du den Namen an.

backup.shbash
#!/bin/sh
set -e
cd "$(dirname "$0")"
dir="backups/$(date +%F_%H%M%S)"
mkdir -p "$dir"
docker compose exec -T db sh -c 'pg_dump -Fc -U "$POSTGRES_USER" "$POSTGRES_DB"' > "$dir/db.dump"
docker run --rm -v sure_app-storage:/data:ro -v "$PWD/$dir":/backup alpine tar czf /backup/storage.tar.gz -C /data .
cp .env compose.yml Caddyfile "$dir/"
chmod -R go-rwx "$dir"
find backups -maxdepth 1 -mindepth 1 -type d -mtime +7 -exec rm -rf {} +

Mach es ausführbar (chmod +x backup.sh) und starte es mit sudo ./backup.sh. Es löscht Backups, die älter als 7 Tage sind. Für ein tägliches Backup um 02:00 Uhr trägst du es in die Crontab von root ein (sudo crontab -e):

0 2 * * * /home/deployn/docker-apps/sure/backup.sh

Zurückspielen:

Terminalbash
sudo docker compose stop web worker
sudo cat backups/<Datum>/db.dump | sudo docker compose exec -T db sh -c 'pg_restore --clean --if-exists --no-owner -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
sudo docker run --rm -v sure_app-storage:/data -v "$PWD/backups/<Datum>":/backup:ro alpine tar xzf /backup/storage.tar.gz -C /data
sudo docker compose up -d

Der Dienst backup-offsite kopiert den Ordner backups auch mit diesem Skript. Beim Update ersetzt sudo ./backup.sh den Befehl sudo docker compose run --rm backup create.

Updates

Sure aktualisiert sich nicht von selbst. Das Image ghcr.io/we-promise/sure:stable folgt dem jeweils letzten Release, latest dem Alpha-Stand. Ich bleibe bei stable. Vor jedem Update sichere ich:

Terminalbash
cd ~/docker-apps/sure
sudo docker compose run --rm backup create
sudo docker compose pull web worker
sudo docker compose up -d

Migrationen laufen beim Start automatisch. Eine feste Version legst du mit SURE_IMAGE_TAG=0.7.6 in der .env fest. Variablen aus der .env kommen nur an, wenn sie in der compose.yml stehen. SURE_IMAGE_TAG und PORT sind dort schon eingebaut.

Ressourcenverbrauch

Container RAM
web 369 MiB
worker 359 MiB
db (PostgreSQL 17) 55 MiB
redis (Valkey 9) 10 MiB
caddy 19 MiB

Das sind zusammen rund 810 MiB direkt nach dem Start mit leerer Datenbank. Der Demo-Daten-Generator läuft im Worker und braucht laut Doku mehr als 512 MB.

Typische Probleme

ActiveRecord::DatabaseConnectionError beim ersten Start. Meist liegt ein altes Postgres-Volume mit anderen Zugangsdaten herum. Wenn du noch keine Daten hast, setzt du es zurück. Das löscht alle Daten der Datenbank:

Terminalbash
sudo docker compose down
sudo docker volume rm sure_postgres-data
sudo docker compose up -d

Variablen aus der .env kommen nicht an. Die .env wird nicht komplett in die Container gereicht. Nur Variablen, die im Block x-rails-env der compose.yml stehen, kommen an. Das gilt zum Beispiel für SMTP: SMTP_ADDRESS, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_TLS_ENABLED, EMAIL_SENDER und APP_DOMAIN ergänzt du dort. Nach der Änderung startest du mit sudo docker compose up -d neu, denn restart behält die alte Umgebung.

Der Login klappt über HTTPS nicht. Prüfe, ob RAILS_ASSUME_SSL auf "true" steht und ob Caddy das Zertifikat bekommen hat. Das siehst du mit sudo docker compose logs caddy.

Sidekiq-Oberfläche. Unter /sidekiq siehst du Hintergrundjobs. Sie ist nur für angemeldete Super-Admins sichtbar.

Fertige Dateien

So sehen die drei Dateien am Ende aus. Die .env enthält deine erzeugten Werte, dazu optional den KI-Key:

.envbash
SECRET_KEY_BASE="…"
POSTGRES_PASSWORD="…"
Caddyfilecaddy
sure.example.com {
    reverse_proxy web:3000
}
compose.ymlyaml
x-db-env: &db_env
  POSTGRES_USER: ${POSTGRES_USER:-sure_user}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  POSTGRES_DB: ${POSTGRES_DB:-sure_production}

x-rails-env: &rails_env
  <<: *db_env
  SECRET_KEY_BASE: ${SECRET_KEY_BASE}
  SELF_HOSTED: "true"
  RAILS_FORCE_SSL: "false"
  RAILS_ASSUME_SSL: "true"
  DB_HOST: db
  DB_PORT: 5432
  REDIS_URL: redis://redis:6379/1
  OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN:-}
  OPENAI_MODEL: ${OPENAI_MODEL:-}
  OPENAI_URI_BASE: ${OPENAI_URI_BASE:-}
  LLM_CONTEXT_WINDOW: ${LLM_CONTEXT_WINDOW:-}
  OPENAI_REQUEST_TIMEOUT: ${OPENAI_REQUEST_TIMEOUT:-60}
  AI_HEALTH_PROBE_TIMEOUT: ${AI_HEALTH_PROBE_TIMEOUT:-}
  AI_HEALTH_PROBE_CACHE_TTL: ${AI_HEALTH_PROBE_CACHE_TTL:-}
  AI_RESPONSE_TIMEOUT: ${AI_RESPONSE_TIMEOUT:-90}
  ASSISTANT_MAX_TOOL_CALL_ITERATIONS: ${ASSISTANT_MAX_TOOL_CALL_ITERATIONS:-}

services:
  web:
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    volumes:
      - app-storage:/rails/storage
    ports:
      - 127.0.0.1:${PORT:-3000}:3000
    restart: unless-stopped
    environment:
      <<: *rails_env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    dns:
      - 8.8.8.8
      - 1.1.1.1
    networks:
      - sure_net

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
    networks:
      - sure_net

  worker:
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    command: bundle exec sidekiq
    volumes:
      - app-storage:/rails/storage
    restart: unless-stopped
    environment:
      <<: *rails_env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    dns:
      - 8.8.8.8
      - 1.1.1.1
    networks:
      - sure_net

  db:
    image: postgres:17
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      <<: *db_env
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - sure_net

  backup:
    profiles:
      - backup
    image: ghcr.io/we-promise/sure:${SURE_IMAGE_TAG:-stable}
    user: "0"
    entrypoint: ["bin/sure-backup"]
    restart: unless-stopped
    volumes:
      - ${BACKUP_DIR:-./backups}:/backups
      - app-storage:/rails/storage
      - .:/config:ro
    environment:
      <<: *db_env
      POSTGRES_HOST: db
      BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *}
      BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
      INSTANCE_ID: ${INSTANCE_ID:-default}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - sure_net

  backup-offsite:
    profiles:
      - backup-offsite
    image: rclone/rclone:1.71
    restart: unless-stopped
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        : "$${BACKUP_DESTINATION:?set BACKUP_DESTINATION in .env}"
        while true; do
          rclone copy /backups "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --exclude ".tmp-*/**" \
            && rclone delete "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --min-age "$${BACKUP_KEEP_DAYS}d" --include "/*/*-scheduled/**" \
            && rclone rmdirs "$${BACKUP_DESTINATION%/}/$${INSTANCE_ID}" --leave-root \
            || echo "backup-offsite: copy failed; retrying in an hour"
          sleep 3600
        done
    volumes:
      - ${BACKUP_DIR:-./backups}:/backups:ro
    environment:
      BACKUP_DESTINATION: ${BACKUP_DESTINATION:-}
      BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7}
      INSTANCE_ID: ${INSTANCE_ID:-default}
      RCLONE_CONFIG_S3_TYPE: ${RCLONE_CONFIG_S3_TYPE:-}
      RCLONE_CONFIG_S3_PROVIDER: ${RCLONE_CONFIG_S3_PROVIDER:-}
      RCLONE_CONFIG_S3_ACCESS_KEY_ID: ${RCLONE_CONFIG_S3_ACCESS_KEY_ID:-}
      RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: ${RCLONE_CONFIG_S3_SECRET_ACCESS_KEY:-}
      RCLONE_CONFIG_S3_ENDPOINT: ${RCLONE_CONFIG_S3_ENDPOINT:-}
    networks:
      - sure_net

  redis:
    image: valkey/valkey:9
    restart: unless-stopped
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - sure_net

volumes:
  caddy-data:
  app-storage:
  postgres-data:
  redis-data:

networks:
  sure_net:
    driver: bridge

FAQ

Häufige Fragen

Was ist Sure Finance?

Sure ist ein Community-Fork von Maybe Finance, einer Open-Source-App für persönliche Finanzen. Sie zeigt Konten, Cashflow, Ausgaben und Budgets in einem Dashboard und läuft auf deinem eigenen Server.

Was kostet Sure beim Selbst-Hosten?

Die Software ist kostenlos (AGPL-3.0). Du zahlst nur für den Server. Ein Debian-Server mit 2 bis 4 GB RAM reicht. Kosten für KI-Funktionen kommen von OpenAI oder Anthropic.

Wie viel RAM braucht Sure?

Die Doku nennt rund 512 MB als Untergrenze. Im Test lagen web, worker, PostgreSQL, Valkey und Caddy zusammen bei etwa 800 MiB.

Kann ich Sure mit Valkey statt Redis betreiben?

Ja. Ersetze in der compose.yml das Image redis:latest durch valkey/valkey:9. Der Dienstname redis und die REDIS_URL bleiben gleich.

Wie importiere ich Kontoauszüge?

Über Transaktionen > Import > Rohdaten > Transaktionen importieren. Du lädst eine CSV-Datei hoch oder fügst den Text ein und ordnest die Spalten zu. Für Mint, Actual Budget, YNAB und Quicken gibt es fertige Vorlagen.

Wie sichere ich Sure?

Mit dem Backup-Dienst aus der Beispiel-Datei: sudo docker compose run --rm backup create sichert Datenbank, hochgeladene Dateien, .env und compose.yml. Er braucht PostgreSQL 17 oder älter, weil sein pg_dump Version 17 hat. Kopiere die Backups verschlüsselt an einen zweiten Ort.

Wie aktualisiere ich Sure?

Erst mit sudo docker compose run --rm backup create sichern, dann sudo docker compose pull web worker und sudo docker compose up -d ausführen. Migrationen laufen automatisch.