Back to self-hosting
Setup Guide

Self-host Coral Dash with Docker

A complete walkthrough — from licence key to first sync — for running Coral Dash on your own server. Roughly 15 minutes if your domain and Google Cloud project are ready.

Prerequisites

  • Docker and Docker Compose v2+
  • A server that can run Docker — VPS, Raspberry Pi 4/5, NAS, or any Linux/macOS host. Docker images are built for both amd64 and arm64.
  • A domain name with DNS pointed to your server (HTTPS is required for Google OAuth)
  • A Google Cloud project with OAuth credentials (for sign-in and Sheets sync)
  • A Coral Dash licence key — start a free trial or buy a permanent licence at coraldash.com/self-hosting

1. Get your licence key

Visit coraldash.com/self-hosting and either:

  • Start a 7-day free trial — instant, no card required
  • Purchase a permanent licence — £30 one-time via Stripe

Your key (a UUID) appears in your account settings. Copy it.

2. Download the configuration

Clone the public setup repository:

git clone https://github.com/coraldash/self-hosted.git coraldash
cd coraldash
cp .env.example .env

You should now have docker-compose.yml, .env.example, docker/kong.yml, and docker/generate-keys.sh.

3. Configure environment

First generate three secrets in your terminal and keep the output of each:

openssl rand -hex 24     # -> POSTGRES_PASSWORD
openssl rand -base64 32  # -> JWT_SECRET
openssl rand -hex 16     # -> CRON_SECRET

Then edit .env and paste them in alongside your other values:

# Your licence key (trial or permanent)
LICENSE_KEY=xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx

# Your domain (or http://your-server-ip:3000 for a LAN trial, v3.1.1+)
SITE_URL=https://coraldash.yourdomain.com

# Paste the secrets generated above
POSTGRES_PASSWORD=paste-hex-output-here
JWT_SECRET=paste-base64-output-here
CRON_SECRET=paste-hex-output-here

# Google OAuth (see the Google OAuth section below)
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret

POSTGRES_PASSWORD must contain only letters and digits — it is embedded in postgres:// connection URLs, so characters like / + @ : # ? break the stack (base64 output usually contains / or +). Note that .env files are read literally: $(...) commands are not executed there, which is why you generate the values in the terminal first.

4. Generate Supabase keys

The bundled script derives ANON_KEY and SERVICE_ROLE_KEY from the JWT_SECRET in your .env:

chmod +x docker/generate-keys.sh
./docker/generate-keys.sh

Copy the two output keys into .env. (You can also pass the secret directly: ./docker/generate-keys.sh YOUR_JWT_SECRET.)

5. Start the stack

docker compose up -d

This starts five services: PostgreSQL, GoTrue (Supabase auth), PostgREST, Kong (API gateway), and the Coral Dash app.

On first boot, the app activates your licence (one HTTPS call to coraldash.com), runs database migrations, and starts the web server plus cron scheduler. After activation, the container runs fully offline — no telemetry, no phone-home.

Just trying it out? From v3.1.1 you can skip the reverse proxy entirely: set SITE_URL=http://your-server-ip:3000 in .env, open that address, and create an account with email and password. The app routes the Supabase API internally. A domain, HTTPS, and the reverse proxy below are only needed for Google sign-in and production use.

6. Set up reverse proxy (for Google sign-in and production)

Optional for a LAN trial with email/password (see step 5, v3.1.1+), but Google sign-in and production use need HTTPS on a domain. Your reverse proxy must route both the Next.js app (port 3000) and the Supabase API gateway (port 8000) through the same domain — the browser reaches Supabase auth and REST APIs at /auth/v1/ and /rest/v1/ on your domain.

We recommend Caddyfor automatic Let's Encrypt certificate management:

sudo apt install -y caddy

sudo tee /etc/caddy/Caddyfile << 'EOF'
coraldash.yourdomain.com {
    # Supabase Auth API
    handle /auth/v1/* {
        reverse_proxy localhost:8000
    }

    # Supabase REST API
    handle /rest/v1/* {
        reverse_proxy localhost:8000
    }

    # Next.js application
    handle {
        reverse_proxy localhost:3000
    }
}
EOF

sudo systemctl reload caddy

For Nginx or Traefik, see docker/nginx.conf.example in the setup repository — the rule is the same: /auth/v1/* and /rest/v1/* must proxy to Kong (port 8000), everything else to Next.js (port 3000).

7. Sign in and connect your Google Sheet

Visit https://coraldash.yourdomain.com, sign in with Google, and follow the onboarding flow to pick the Google Sheet holding your Monzo export. The first sync runs immediately, then daily at 06:00 UTC by default (configurable via SYNC_SCHEDULE).

Google OAuth setup

  1. Open the Google Cloud Console → Credentials page.
  2. Create a new OAuth 2.0 Client ID (type: Web application).
  3. Add both authorised redirect URIs: https://coraldash.yourdomain.com/auth/v1/callback (Google sign-in) and https://coraldash.yourdomain.com/api/auth/google-sheets/callback (Sheets connection).
  4. Enable three APIs (APIs & Services → Library): the Google Sheets API (reads your transactions during sync), the Google Drive API, and the Google Picker API (both used when picking your spreadsheet). If the Sheets API is missing, the connection succeeds but every sync fails.
  5. Copy the Client ID and Client Secret into your .env.
  6. Create an API key on the same Credentials page (restrict it to the Google Picker API) and set it as GOOGLE_PICKER_API_KEY in .env; set GOOGLE_APP_ID to your Google Cloud project number (shown on the Cloud Console home page). Requires v3.1.6+.

On the consent screen (Data access), the only scope Coral Dash needs is .../auth/drive.file— it requires no Google verification, and you don't specify any file in the console: the scope grants access solely to the spreadsheet you pick inside Coral Dash during onboarding. Then publish the consent screen to Production (Audience page) — with only the non-sensitive drive.file scope this needs no review, and Testing mode would expire the Google connection every 7 days, silently breaking the daily sync.

Lock down signups (single-user instances)

By default anyone who can reach your URL can create an account. For a single-user instance, create your own account first, then disable new signups by adding this under the auth service's environment: in docker-compose.yml and running docker compose up -d:

GOTRUE_DISABLE_SIGNUP: "true"

Existing users can still log in; no new accounts (Google or email) can be created.

Backups

Back up the bundled PostgreSQL database with pg_dump:

# Create a backup
docker compose exec db pg_dump -U postgres postgres > backup_$(date +%Y%m%d).sql

# Restore from backup
docker compose exec -T db psql -U postgres postgres < backup_20260315.sql

Daily automated backups via cron:

0 2 * * * cd /path/to/coraldash && docker compose exec -T db pg_dump -U postgres postgres | gzip > /backups/coraldash_$(date +\%Y\%m\%d).sql.gz

Updating

cd /path/to/coraldash
git pull                        # compose/config fixes
docker compose pull coraldash   # latest app image
docker compose up -d

If you have edited docker-compose.yml locally (SMTP, signup lockdown), stash your changes first: git stash && git pull && git stash pop. Migrations run automatically on startup. Always back up your database before updating. The self-hosted edition receives the same updates as the cloud version — track upcoming releases on the roadmap.

Environment variables

VariableRequiredDescription
LICENSE_KEYYesYour licence key (UUID format)
SITE_URLYesPublic URL — must match your DNS and reverse proxy host
POSTGRES_PASSWORDYesPostgreSQL password (letters and digits only)
JWT_SECRETYesJWT signing secret (min 32 chars)
ANON_KEYYesSupabase anonymous key (from generate-keys.sh)
SERVICE_ROLE_KEYYesSupabase service role key (from generate-keys.sh)
GOOGLE_OAUTH_CLIENT_IDYesGoogle OAuth client ID
GOOGLE_OAUTH_CLIENT_SECRETYesGoogle OAuth client secret
CRON_SECRETYesSecret for internal cron job authentication
GOOGLE_PICKER_API_KEYNoAPI key for the spreadsheet picker (restrict to the Picker API)
GOOGLE_APP_IDNoGoogle Cloud project number (for the spreadsheet picker)
SYNC_SCHEDULENoCron expression for sync schedule (default: 0 6 * * *)
TRADING212_ENCRYPTION_KEYNoRequired to connect Trading 212 — 64-char hex (openssl rand -hex 32)
METALPRICE_API_KEYNometalpriceapi.com key for precious-metal valuations

Troubleshooting

Container won't start — licence error

Verify LICENSE_KEY in .env is a valid UUID (xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx). Get one at coraldash.com/self-hosting.

Container won't start — trial expired

Your 7-day trial has ended. Purchase a permanent licence, update LICENSE_KEY in .env, and run docker compose restart coraldash.

Container won't start — key already activated

Each licence key activates on a single instance. Deactivate the old one in your account settings, then restart.

"Method Not Allowed" when signing in or creating an account

Seen on versions before v3.1.1 when auth requests reached the app instead of the API gateway. Update the image (docker compose pull coraldash then docker compose up -d) — from v3.1.1 the app routes /auth/v1/* and /rest/v1/* itself, so no reverse proxy is needed for email/password sign-in. Also make sure SITE_URL exactly matches the address in your browser's address bar.

Auth or API requests fail in the browser

If you use a reverse proxy, ensure it routes /auth/v1/* and /rest/v1/* to Kong on port 8000 (not directly to the Next.js app on port 3000), and that SITE_URLmatches the address in the browser's address bar. Fixed in v2.3.3+; pull the latest image if you're on an older version. If it still misbehaves after an update, do a one-time Clear site data(DevTools → Application) — the service worker may be serving a stale cached build.

Google OAuth redirect error

Check the redirect URI in Google Cloud Console exactly matches https://your-domain/auth/v1/callback. Note the /auth/v1/ prefix — it points at GoTrue, not Next.js.

Sync not running

Inspect the cron worker logs:

docker compose logs coraldash | grep Cron

"Sync failed" after connecting Google Sheets

From v3.1.8 the sync screen shows the exact underlying error — update the image if you only see a generic message. The most common cause is the Google Sheets APInot being enabled in your Google Cloud project (it is a separate switch from the Drive and Picker APIs): enable it under APIs & Services → Library, wait a minute, and sync again. The full error is also recorded in the app logs (keep the -A 6 — database errors span several lines):

docker compose logs coraldash | grep -A 6 -i "sync failed"

"dependency failed to start: container ...-rest-1 is unhealthy"

A bug in compose files downloaded before 22 July 2026: the PostgREST health probe needed a shell that doesn't exist inside the image on amd64 servers, so the container could never report healthy and startup aborted. Run git pull in your setup folder to get the fixed docker-compose.yml, then docker compose up -d. To patch by hand instead: delete the healthcheck: block under rest:, and under the kong: service's depends_on change the rest: condition from service_healthy to service_started.

"password authentication failed" or auth restart-looping

The database volume keeps the password it was first created with. If you changed POSTGRES_PASSWORD after an earlier docker compose up, the new value no longer matches. If setup never completed there is no data to lose: run docker compose down -v (deletes the database volume), then docker compose up -d. Also check the password contains only letters and digits — / + @ : break the connection URLs.

Check container health

docker compose ps
docker compose logs coraldash --tail 100

db, auth and kong should report healthy; rest shows plain Up by design (its image has no shell to run a probe). When something fails, the reason is in its logs: docker compose logs rest, docker compose logs auth, docker compose logs kong.

Full reference on GitHub

This page covers the most common path. The canonical setup repository on GitHub contains every supported configuration, migration notes, and the full troubleshooting matrix.