API Deployment

The API runs on Node.js 24 in a Docker container. Build context is the repo root.

Historical note: the API previously ran on Cloudflare Workers. That architecture was retired — see planning/migration-steps/ for the migration record. This file now documents the current Docker-based deployment.


Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────┐
│   User Request  │────▶│  Container Host  │────▶│   Node.js   │
└─────────────────┘     └──────────────────┘     └─────────────┘
                                                            │
                        ┌──────────────────┐                 │
                        │  PostgreSQL 18   │◀────────────────┘
                        └──────────────────┘
  • Runtime: Node.js 24 (slim base image)
  • Framework: Hono with OpenAPI
  • Server: @hono/node-server
  • Process: single Node process, multi-stage Docker build (apps/api/Dockerfile)
  • Health check: GET /health — verifies DB connectivity + required env vars

Prerequisites

  • Docker (BuildKit-enabled)
  • A Postgres 18 instance (local Docker via compose.yml for dev, hoststack.dev for staging/prod)

Environments

Environment Host URL
Production hoststack.dev (managed PG) https://api.meister-bill.com (TBD)
Staging hoststack.dev (managed PG) https://api-staging.meister-bill.com

Configuration

Dockerfile: apps/api/Dockerfile

Multi-stage build: 1. builder — installs pnpm, builds schemas, builds API (prisma generate && tsc) 2. runtime — copies built artifacts, installs prod deps only, runs as non-root app user

Healthcheck hits /health every 30s.


Deployment

Build the image

docker build -f apps/api/Dockerfile -t meisterbill-api .

Run locally

docker run -d \
  --name meisterbill-api \
  -p 3001:3001 \
  -e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/meisterbill \
  -e PORT=3001 \
  -e APP_URL=http://localhost:3000 \
  meisterbill-api

Required environment variables

See apps/api/.env.example for the full list. Required at runtime: - DATABASE_URL — Postgres connection string - PORT — listen port (default 3001) - APP_URL — web origin (used for CORS + email redirects) - GOTENBERG_URL — PDF rendering service - Stripe keys (if Stripe features used) - SENTRY_DSN (optional — Sentry disabled if unset)

CI/CD

.gitea/workflows/deploy-api.yml builds the image, smoke-tests /health, and is wired for a future registry push (TODO: registry credentials not yet configured).

Triggers: push to main with changes in apps/api/** or schemas/**.


Rollback

The image is tagged meisterbill-api:<git-sha>. To roll back, run the previous SHA:

docker stop meisterbill-api && docker rm meisterbill-api
docker run -d --name meisterbill-api ... meisterbill-api:<previous-sha>

If the schema has migrated forward incompatibly, restore the database from the most recent backup first.


Troubleshooting

Container exits immediately: - Check DATABASE_URL is reachable from inside the container - docker logs meisterbill-api for stack trace

/health returns 503: - One or more required env vars missing — response body lists them - DB unreachable — check DATABASE_URL + Postgres running

PDF generation fails: - GOTENBERG_URL must point at a reachable gotenberg service (port 3003 in compose.yml)