This guide covers deploying nib with Docker or bare metal.
The easiest way to run nib is with Docker Compose, which includes the app and a PostgreSQL database.
- Docker and Docker Compose
git clone https://github.com/thassiov/nib.git
cd nibCreate a .env file:
SESSION_SECRET=<generate-with-openssl-rand-hex-32>
DB_PASS=<postgres-password>
OIDC_ISSUER=https://your-oidc-provider.example.com
OIDC_CLIENT_ID=nib
OIDC_CLIENT_SECRET=<your-oidc-client-secret>
OIDC_REDIRECT_URI=http://localhost:3000/auth/callback
OIDC_POST_LOGOUT_URI=http://localhost:3000Generate a session secret:
openssl rand -hex 32docker compose up -dThe app will be available at http://localhost:3000. The PostgreSQL container runs migrations automatically via init scripts.
- nib app — Node.js 22 Alpine container, built from the Dockerfile (multi-stage: build + production)
- PostgreSQL 17 — Alpine container with a persistent volume (
pgdata) and health checks - Migrations — SQL files from
migrations/are mounted as init scripts and run on first database creation - Session store — The app auto-creates the
sessiontable in PostgreSQL on startup
The compose file supports these env vars (set in .env or environment):
| Variable | Required | Default | Description |
|---|---|---|---|
SESSION_SECRET |
Yes | - | Express session secret |
DB_PASS |
Yes | - | PostgreSQL password (shared by app and db containers) |
OIDC_ISSUER |
No* | - | OIDC provider URL |
OIDC_CLIENT_ID |
No | nib |
OIDC client ID |
OIDC_CLIENT_SECRET |
No* | - | OIDC client secret |
OIDC_REDIRECT_URI |
No | http://localhost:3000/auth/callback |
OIDC callback URL |
OIDC_POST_LOGOUT_URI |
No | http://localhost:3000 |
Post-logout redirect |
COOKIE_SECURE |
No | false |
Set to true behind TLS proxy |
ADMIN_SUBS |
No | - | Comma-separated OIDC subject IDs for admin role |
*OIDC variables are only required if you want authentication. The app works without them — you just can't log in.
- Node.js 22+
- PostgreSQL (external — nib does not include its own database server)
nib needs a PostgreSQL database to exist before it can start. The migration script creates tables, but the database and user must be created first.
If you already have a PostgreSQL server, create the database and user:
CREATE DATABASE nib;
CREATE USER nib WITH PASSWORD '<password>';
GRANT ALL PRIVILEGES ON DATABASE nib TO nib;
-- On PostgreSQL 15+, also grant schema permissions:
ALTER DATABASE nib OWNER TO nib;Then create the tables using either method:
Option A: Sequelize sync (uses the model definitions directly):
DB_HOST=<your-pg-host> DB_USER=nib DB_PASS=<password> npx tsx server/migrate.tsOption B: SQL migration files (the same ones Docker Compose uses):
psql -h <your-pg-host> -U nib -d nib -f migrations/001_initial.sql
psql -h <your-pg-host> -U nib -d nib -f migrations/002_add_user_role.sql
psql -h <your-pg-host> -U nib -d nib -f migrations/003_add_session_table.sqlThe session table is also auto-created by connect-pg-simple on first startup (createTableIfMissing: true), so migration file 003 is optional if you prefer to let the app handle it.
On subsequent deploys, if models have changed:
DB_HOST=<your-pg-host> DB_USER=nib DB_PASS=<password> npx tsx server/migrate.ts --alterThe --alter flag adds new columns and indexes without dropping data.
git clone https://github.com/thassiov/nib.git
cd nib
npm ci
npm run buildThis produces:
dist/client/— Static frontend files (HTML, JS, CSS)dist/server/— Compiled server JavaScript
Create a .env file or set environment variables:
NODE_ENV=production
PORT=3000
SESSION_SECRET=<generate-with-openssl-rand-hex-32>
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=nib
DB_USER=nib
DB_PASS=<database-password>
# OIDC (optional — app works without auth, you just can't log in)
OIDC_ISSUER=https://your-oidc-provider.example.com
OIDC_CLIENT_ID=nib
OIDC_CLIENT_SECRET=<oidc-client-secret>
OIDC_REDIRECT_URI=http://localhost:3000/auth/callback
OIDC_POST_LOGOUT_URI=http://localhost:3000
# Optional
COOKIE_SECURE=false
ADMIN_SUBS=oidc-subject-id-1,oidc-subject-id-2NODE_ENV=production node dist/server/main.jsIn production mode, NestJS serves the built client files as static assets via @nestjs/serve-static and handles all routes (API, auth, and SPA fallback) on a single port.
curl http://localhost:3000/api/healthReturns:
{ "status": "ok", "service": "nib", "db": "connected", "oidc": "reachable" }The health endpoint checks database connectivity via Sequelize.authenticate() and OIDC provider reachability. If the database is unreachable, it returns "db": "disconnected" but still responds with status "ok" (the server itself is running).
nib exposes a Prometheus-compatible /metrics endpoint that can be scraped by any Prometheus-compatible collector (Prometheus, Grafana Alloy, Victoria Metrics, etc.).
scrape_configs:
- job_name: nib
static_configs:
- targets: ['localhost:3000']
scrape_interval: 15s| Metric | Type | Labels | Description |
|---|---|---|---|
nib_drawings_total |
Gauge | visibility (public/private) |
Total drawings in database |
nib_users_total |
Gauge | - | Total registered users |
nib_sessions_active |
Gauge | type (authenticated/anonymous) |
Active sessions |
nib_drawings_created_total |
Counter | visibility (public/private) |
Drawings created since restart |
nib_drawings_deleted_total |
Counter | - | Drawings deleted since restart |
Default Node.js process metrics (CPU, memory, event loop, GC) are also exposed via prom-client.
A pre-built Grafana dashboard is included at grafana/dashboard.json. Import it via Grafana UI (Dashboards > Import > Upload JSON file) and select your Prometheus and Loki datasources when prompted.
The dashboard includes panels for application overview (drawings, users, sessions), activity timeseries (creation/deletion rates), process health (CPU, memory, event loop), and log aggregation (via Loki).
The PromQL queries filter by job="nib" — make sure your Prometheus scrape config uses job_name: nib (or adjust the queries after import). The Loki logs panel uses {job="nib"} which should match if your log collector labels nib logs with the same job name.
- The
/metricsendpoint skips session middleware to prevent scrape requests from creating anonymous sessions. - The endpoint is excluded from the SPA catch-all so it returns Prometheus text format, not the React app.
- Session counts use a raw SQL query (
sess::jsonb->>'userId') to classify sessions as authenticated or anonymous.
