Self-hosting rollnote
rollnote is three pieces: a PostgreSQL database, the API (rollnote-api,
NestJS), and the frontend (rollnote-front, Angular served by nginx). This
guide gets all three running with Docker. For the full list of settings, see the configuration reference.
Requirements
- A host with Docker and the Docker Compose plugin.
- PostgreSQL 18 (the compose file below runs one for you).
- Two DNS names you control, for example
app.example.com(frontend) andapi.example.com(API). Auth cookies are shared across them, so both should sit under the same parent domain. - An SMTP account for transactional email (sign-up verification, password reset, email OTP). This is required in production.
Get the code
Both repositories live on Forgejo:
git clone https://forgejo.eliottbidault.fr/RollNote/rollnote-api.git
git clone https://forgejo.eliottbidault.fr/RollNote/rollnote-front.git Configure the environment
Create a .env file next to your compose file. The minimum for a production
boot:
# PostgreSQL
POSTGRES_USER=rollnote
POSTGRES_PASSWORD=change-me
POSTGRES_DB=rollnote
DATABASE_URL=postgres://rollnote:change-me@postgres:5432/rollnote
# Better-Auth
BETTER_AUTH_SECRET=... # >= 16 chars, openssl rand -base64 24
BETTER_AUTH_URL=https://api.example.com
ENCRYPTION_KEY=... # base64-encoded 32 bytes, openssl rand -base64 32
# URLs
API_URL=https://api.example.com
FRONT_URL=https://app.example.com
# SMTP (required in production)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=postmaster@example.com
SMTP_PASS=change-me
EMAIL_FROM=no-reply@example.com # a bare address, no display name NODE_ENV=production is already set inside the API image, so it does not need to
be in this file.
The API validates this on boot and exits loudly if anything required is
missing or malformed (for example, an ENCRYPTION_KEY that does not decode to
exactly 32 bytes). See configuration for how to generate
each secret and what every variable does.
The frontend bakes its API URL in at build time, not from this
.env. Set it inrollnote-front/src/environments/environment.tsbefore building (see the frontend’s API URL). The frontend’s Content-Security-Policy inrollnote-front/nginx-security-headers.confalso nameshttps://api.roll-note.cominconnect-src: replace it with your API URL too.
A reference compose file
This builds the API and frontend from the cloned source and wires them to Postgres. Adjust paths to match where you cloned the repos.
services:
postgres:
# Same image as rollnote-api's own compose.yml.
image: postgres:18-trixie
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
# PostgreSQL 18 images keep their data in a versioned subdirectory
# (/var/lib/postgresql/18/docker): mount the parent directory, not
# /var/lib/postgresql/data.
- rollnote-db:/var/lib/postgresql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 10
api:
build: ./rollnote-api
restart: unless-stopped
# Minimal init as PID 1: forwards SIGTERM to node and reaps zombies.
init: true
env_file: .env
depends_on:
postgres:
condition: service_healthy
# /health checks database connectivity. start_period covers the
# migrations and seed that run before the server starts listening.
healthcheck:
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/health']
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
ports:
- '3000:3000'
front:
build: ./rollnote-front
restart: unless-stopped
depends_on:
api:
condition: service_healthy
ports:
- '8080:80'
volumes:
rollnote-db: Bring it up:
docker compose up --build -d On first boot the API runs its migrations and seeds the global reference data (film stocks, formats, and the changelog) automatically, so a fresh database catches itself up with no extra step.
Put it behind HTTPS
The API and frontend listen on plain HTTP inside Docker. In production, run a reverse proxy that terminates TLS and routes each hostname to the right container. A minimal Caddy example:
api.example.com {
reverse_proxy localhost:3000
}
app.example.com {
reverse_proxy localhost:8080
} Caddy provisions certificates automatically. nginx or Traefik work just as well -
the only requirements are TLS and forwarding the original Host header so
auth cookies and CORS line up with your FRONT_URL / API_URL.
Built-in analytics
The frontend ships a small Umami analytics snippet. On any production build
(environment.production === true), rollnote-front/src/main.ts injects a script,
once the page has loaded, that loads Umami from the project’s own instance under
a fixed website ID:
function loadAnalytics(): void {
const umamiScript = document.createElement('script');
umamiScript.src = 'https://umami.eliottbidault.fr/script.js';
umamiScript.setAttribute('data-website-id', '409a7799-df72-486c-a9ff-326b04ba30ae');
document.head.appendChild(umamiScript);
}
if (environment.production) {
window.addEventListener('load', loadAnalytics, { once: true });
} On a self-hosted build this reports your visitors’ page views to that instance, not yours. There is no environment variable for it - the frontend is a static bundle, so change the source before you build:
- To disable analytics entirely, delete the
if (environment.production)block fromsrc/main.ts. - To use your own Umami, replace the
srchost anddata-website-idwith the values from your instance, and swap the Umami host in the CSP ofnginx-security-headers.conf(script-srcandconnect-src).
Become an admin
Register your account in the app as normal. Admin-only areas (such as the
changelog editor) are gated on an is_admin flag on your user row. Email and
username are stored encrypted, so you cannot match on them in SQL: find your
user by creation date instead, then flip the flag by id:
docker compose exec postgres psql -U rollnote -d rollnote SELECT id, created_at FROM "user" ORDER BY created_at DESC LIMIT 5;
UPDATE "user" SET is_admin = true WHERE id = '<your user id>'; Next steps
- Configuration reference - every variable, secret generation, and backups.
- Architecture - how the pieces fit together.