Files
accounted/DOCKER.md
T
Jakob Wennberg b5b004509d feat: add Docker support with self-hosted and hosted presets
Multi-stage Dockerfile (Node 22 Alpine), docker-compose for self-hosted
and hosted deployments, CI workflow for GHCR publishing, runtime env var
substitution, and cron sidecar with supercronic.

Aligns hosted extension preset with dev config (enable-banking,
ai-categorization, ai-chat, email).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:40:33 +01:00

5.2 KiB

Self-Hosting gnubok with Docker

Prerequisites

  • Docker and Docker Compose (v2)
  • A Supabase project (free tier works)

You do not need Node.js, npm, or anything else installed locally. The pre-built image has everything.


Quick Start

1. Download the required files

mkdir gnubok && cd gnubok

# Compose file + env template
curl -fsSLO https://raw.githubusercontent.com/gnubok/gnubok/main/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/gnubok/gnubok/main/.env.docker.example

# Cron sidecar (Dockerfile + schedule)
mkdir -p docker
curl -fsSL -o docker/cron.Dockerfile \
  https://raw.githubusercontent.com/gnubok/gnubok/main/docker/cron.Dockerfile
curl -fsSL -o docker/crontab.self-hosted \
  https://raw.githubusercontent.com/gnubok/gnubok/main/docker/crontab.self-hosted

2. Configure your environment

cp .env.docker.example .env

Open .env and fill in the required values:

Variable Where to find it
NEXT_PUBLIC_SUPABASE_URL Supabase dashboard → Settings → API → Project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY Supabase dashboard → Settings → API → anon public key
SUPABASE_SERVICE_ROLE_KEY Supabase dashboard → Settings → API → service_role key
NEXT_PUBLIC_APP_URL The URL where you'll access gnubok (e.g. https://gnubok.example.com)
CRON_SECRET Any random string — openssl rand -hex 32 works

3. Start

docker compose up -d

That's it. The app is now running at http://localhost:3000 (or whatever port you set with PORT).

4. Verify

# Should return {"status":"healthy",...}
curl http://localhost:3000/api/health

Optional Extensions

The self-hosted image ships with all extensions enabled (except Enable Banking, which requires private PSD2 credentials). Each extension activates when you provide its env vars — without them, the app works normally and the feature is simply unavailable.

AI Features (ai-categorization, ai-chat, receipt-ocr, invoice-inbox)

ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

Email (invoice sending, reminders)

RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=faktura@your-domain.com
RESEND_WEBHOOK_SECRET=whsec_...

Push Notifications

NEXT_PUBLIC_VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_SUBJECT=mailto:you@example.com

Generate VAPID keys with: npx web-push generate-vapid-keys

Calendar

No env vars needed — always available.


Updating

docker compose pull        # pulls latest app image from GHCR
docker compose up -d       # recreates containers if image changed

The latest tag always points to the newest build from main. The cron sidecar is a small Alpine image built locally — it updates automatically on up if you re-download docker/cron.Dockerfile.


Building from Source

If you prefer to build locally instead of pulling the pre-built image:

# Clone the repo
git clone https://github.com/gnubok/gnubok.git
cd gnubok
cp .env.docker.example .env
# Fill in .env

# Build and start
docker compose -f docker-compose.yml -f docker-compose.build.yml up --build -d

Architecture

The compose setup runs two containers:

Container What it does
app Next.js application server
cron Lightweight Alpine sidecar that runs scheduled jobs (deadline checks, invoice reminders, tax deadline sync, document verification) via supercronic

The cron container waits for the app's healthcheck to pass before starting. It calls the app's cron API endpoints over the internal Docker network.

How NEXT_PUBLIC_* injection works

The Docker image is built with placeholder values (e.g. __NEXT_PUBLIC_SUPABASE_URL__) baked into the JavaScript bundles. When the container starts, docker-entrypoint.sh replaces those placeholders with your actual env vars via sed. This means the same image works for any Supabase project — no rebuilding needed.


Ports

The app listens on port 3000 inside the container. To map it to a different host port:

PORT=8080

Then access at http://localhost:8080.


Reverse Proxy

For production, put the app behind a reverse proxy (nginx, Caddy, Traefik) that handles TLS. Example with Caddy:

gnubok.example.com {
    reverse_proxy localhost:3000
}

Make sure NEXT_PUBLIC_APP_URL matches the public URL (e.g. https://gnubok.example.com).


Troubleshooting

Container exits immediately

docker compose logs app

Most common cause: missing required env vars. Check that all 5 required values in .env are set.

Health check fails

curl -v http://localhost:3000/api/health

The health endpoint tests database connectivity. If it returns unhealthy, verify your Supabase URL and service role key are correct.

Cron container keeps restarting

docker compose logs cron

The cron container depends on the app being healthy first. If the app never becomes healthy, the cron container will wait indefinitely.

Port already in use Set a different port: PORT=8080 docker compose up -d