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
amd64andarm64. - 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 .envYou 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_SECRETThen 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-secretPOSTGRES_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.shCopy 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 -dThis 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 caddyFor 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
- Open the Google Cloud Console → Credentials page.
- Create a new OAuth 2.0 Client ID (type: Web application).
- Add both authorised redirect URIs:
https://coraldash.yourdomain.com/auth/v1/callback(Google sign-in) andhttps://coraldash.yourdomain.com/api/auth/google-sheets/callback(Sheets connection). - 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.
- Copy the Client ID and Client Secret into your
.env. - Create an API key on the same Credentials page (restrict it to the Google Picker API) and set it as
GOOGLE_PICKER_API_KEYin.env; setGOOGLE_APP_IDto 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.sqlDaily 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.gzUpdating
cd /path/to/coraldash
git pull # compose/config fixes
docker compose pull coraldash # latest app image
docker compose up -dIf 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
| Variable | Required | Description |
|---|---|---|
LICENSE_KEY | Yes | Your licence key (UUID format) |
SITE_URL | Yes | Public URL — must match your DNS and reverse proxy host |
POSTGRES_PASSWORD | Yes | PostgreSQL password (letters and digits only) |
JWT_SECRET | Yes | JWT signing secret (min 32 chars) |
ANON_KEY | Yes | Supabase anonymous key (from generate-keys.sh) |
SERVICE_ROLE_KEY | Yes | Supabase service role key (from generate-keys.sh) |
GOOGLE_OAUTH_CLIENT_ID | Yes | Google OAuth client ID |
GOOGLE_OAUTH_CLIENT_SECRET | Yes | Google OAuth client secret |
CRON_SECRET | Yes | Secret for internal cron job authentication |
GOOGLE_PICKER_API_KEY | No | API key for the spreadsheet picker (restrict to the Picker API) |
GOOGLE_APP_ID | No | Google Cloud project number (for the spreadsheet picker) |
SYNC_SCHEDULE | No | Cron expression for sync schedule (default: 0 6 * * *) |
TRADING212_ENCRYPTION_KEY | No | Required to connect Trading 212 — 64-char hex (openssl rand -hex 32) |
METALPRICE_API_KEY | No | metalpriceapi.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 100db, 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.