Files
accounted/docs/DOCKER.md
T
Mattsson 2908a951ab chore: remove Dependabot (#1084)
Delete .github/dependabot.yml and update the two doc references that
pointed at it. Weekly grouped bumps were noise, and the #884 grouped
bump broke Bedrock streaming in prod; dependency updates are manual
and deliberate from now on. The @anthropic-ai/bedrock-sdk 0.29.1 exact
pin remains enforced by scripts/checks/no-new-antipatterns.mjs. Open
dependabot PRs #1083, #1082, #1012 closed alongside this change.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:55:23 +02:00

259 lines
7.7 KiB
Markdown

# Self-Hosting Accounted with Docker
## Prerequisites
- Docker and Docker Compose (v2)
- A [Supabase](https://supabase.com) 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
```bash
mkdir Accounted && cd Accounted
# 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
```bash
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 Accounted (e.g. `https://gnubok.example.com`) |
| `CRON_SECRET` | Any random string: `openssl rand -hex 32` works |
Once `.env` is filled in, **restrict its permissions** so other users on the host can't read your service-role key or cron secret:
```bash
chmod 600 .env
```
### 3. Start
```bash
docker compose up -d
```
The app is now reachable on **loopback only** at `http://127.0.0.1:3000`. This is intentional: direct internet exposure over HTTP is not safe for an accounting app. The next section enables HTTPS.
### 4. Verify
```bash
# Should return {"status":"healthy",...}
curl http://localhost:3000/api/health
```
---
## Enable HTTPS (recommended)
Ship a Caddy reverse proxy alongside the app: it auto-provisions Let's Encrypt certificates and renews them forever.
### 1. Point a domain at the host
`gnubok.example.com → <your-public-ip>` (A record). Ports 80 and 443 must be reachable from the internet (Let's Encrypt's HTTP-01 challenge uses port 80).
### 2. Set `DOMAIN` in `.env`
```env
DOMAIN=gnubok.example.com
NEXT_PUBLIC_APP_URL=https://gnubok.example.com
```
### 3. Download the overlay + Caddyfile
```bash
curl -fsSLO https://raw.githubusercontent.com/gnubok/gnubok/main/docker-compose.caddy.yml
mkdir -p docker
curl -fsSL -o docker/Caddyfile \
https://raw.githubusercontent.com/gnubok/gnubok/main/docker/Caddyfile
```
### 4. Start with the overlay
```bash
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
```
Caddy obtains a cert on first boot (takes ~10 s). Visit `https://gnubok.example.com`.
If you already have nginx / a managed load balancer / Cloudflare in front, skip Caddy and point your existing proxy at `127.0.0.1:3000`: set `NEXT_PUBLIC_APP_URL` to match the public URL.
---
## 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)
```env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
```
### Email (invoice sending, reminders)
```env
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=faktura@your-domain.com
RESEND_WEBHOOK_SECRET=whsec_...
```
### Push Notifications
```env
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
The default `IMAGE_TAG=latest` follows `main` and updates on every `docker compose pull`. For production, **pin to a specific release** so updates are deliberate:
```env
# .env
IMAGE_TAG=1.2.3
```
Browse available tags at https://github.com/erp-mafia/gnubok/pkgs/container/gnubok. For maximum integrity, pin by digest:
```env
IMAGE_TAG=1.2.3@sha256:abcdef...
```
Apply updates:
```bash
docker compose pull
docker compose up -d
```
The cron sidecar is a small Alpine image built locally: it rebuilds automatically on `up --build` if you re-download `docker/cron.Dockerfile`. Base-image digests (node, alpine, caddy) are pinned in source and bumped manually when upstream ships security updates.
---
## Building from Source
If you prefer to build locally instead of pulling the pre-built image:
```bash
# Clone the repo
git clone https://github.com/gnubok/gnubok.git
cd Accounted
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](https://github.com/aptible/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 image is built with placeholder values (e.g. `__NEXT_PUBLIC_SUPABASE_URL__`) baked into the JavaScript bundles. At container start, `docker-entrypoint.sh` runs as `root`, `sed`-substitutes the placeholders with your runtime env vars, then runs `chmod -R a-w /app/.next/static` and drops privileges with `su-exec nextjs:nodejs` before exec'ing Node. The served JS bundle is owned by `root` and read-only by the time the application starts: a runtime RCE in the Node process cannot rewrite what other users will receive.
---
## Ports
The app listens on port 3000 inside the container. The base compose binds it to `127.0.0.1:3000` on the host: change `PORT` in `.env` to remap. To expose on all interfaces (only do this if you're putting your own reverse proxy in front), override the port binding in a local `docker-compose.override.yml`:
```yaml
services:
app:
ports: !override
- "${PORT:-3000}:3000"
```
---
## Reverse Proxy
The preferred path is the bundled Caddy overlay: see [Enable HTTPS](#enable-https-recommended). If you already run nginx, Traefik, or sit behind Cloudflare, leave the app on `127.0.0.1:3000` and point your existing proxy at it. Set `NEXT_PUBLIC_APP_URL` to the public URL.
Example nginx upstream:
```nginx
server {
server_name gnubok.example.com;
listen 443 ssl http2;
# ssl_certificate / ssl_certificate_key / etc.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## Troubleshooting
**Container exits immediately**
```bash
docker compose logs app
```
Most common cause: missing required env vars. Check that all 5 required values in `.env` are set.
**Health check fails**
```bash
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**
```bash
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`