Stap-voor-stap-gids om het GRC-platform productie-rijp te draaien bij een gemeente of publieke organisatie. Gericht op IT-beheerders met basiskennis van Linux en Docker.
Verwante documenten:
configuratie.md·deployment-caddy.md·backup.md·monitoring.md
- Voorbereiden — server, DNS, firewall
- Installeren — repo clonen,
.envinvullen, containers starten - Initialiseren — migraties, eerste beheerder, normenkaders
- Verifiëren — smoke-test, security headers
- Onderhouden — backup-cron, monitoring, updates
Verwachte tijd: half tot heel werkdag voor een ervaren beheerder, langer bij eerste keer.
| Vereiste | Minimaal | Aanbevolen |
|---|---|---|
| OS | Ubuntu 22.04 LTS of Debian 12 | Ubuntu 24.04 LTS |
| CPU | 2 vCPU | 4 vCPU |
| Geheugen | 4 GB RAM | 8 GB RAM |
| Schijf | 20 GB SSD | 50 GB SSD |
| Docker | 24.0+ met Compose plugin v2 | 28.0+ |
Wijs één van deze configuraties aan naar je server-IP:
- Variant A (één domein):
grc.jouwdomein.nl→ IP - Variant B (subdomeinen):
grc.jouwdomein.nl+api.grc.jouwdomein.nl→ IP
Wacht tot DNS-propagatie afgerond is (verifieer met dig grc.jouwdomein.nl) voor je verder gaat. Zonder werkende DNS kan Let's Encrypt geen certificaat uitgeven.
Open vanuit het internet alleen:
- TCP 80 — voor Let's Encrypt ACME HTTP-01 challenge en HTTP → HTTPS redirect
- TCP 443 — voor HTTPS
- UDP 443 — voor HTTP/3 (optioneel maar aanbevolen)
- TCP 22 — SSH alleen vanuit beheernetwerken, idealiter met key-only auth
Niet open: 5432 (PostgreSQL), 8000 (API), 3000 (frontend). Die zijn alleen intern bereikbaar.
# Op een verse Ubuntu server
sudo apt update && sudo apt install -y git docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER # log opnieuw in voor groep-wijzigingsudo mkdir -p /opt && sudo chown $USER /opt
cd /opt
git clone https://github.com/security-commons-nl/grc-platform.git
cd grc-platformcp .env.example .envVerplichte aanpassingen — alle waarden moeten gewijzigd zijn voor productie:
# Database — kies een sterk uniek wachtwoord
POSTGRES_PASSWORD=<openssl rand -base64 24>
# JWT — minimaal 64 tekens
JWT_SECRET_KEY=<openssl rand -hex 32>
# Environment — schakelt /docs en /auth/dev-token uit
ENVIRONMENT=production
# CORS — vul in het frontend-domein
ALLOWED_ORIGINS=https://grc.jouwdomein.nl
# Rate limiting — defaults zijn meestal goed
RATE_LIMIT_ENABLED=true
RATE_LIMIT_DEFAULT=100/minute
RATE_LIMIT_AUTH=10/minute
# AI — voor EU-conforme deployment (Mistral)
AI_API_BASE=https://api.mistral.ai/v1
AI_API_KEY=<mistral-api-key>
AI_MODEL_NAME=mistral-small-latest
AI_EMBEDDING_MODEL=mistral-embedHoud de .env uit version control en bewaar een kopie veilig (password manager, encrypted backup).
Volg deployment-caddy.md: kies Variant A of B, kopieer het Caddyfile-voorbeeld uit examples/caddy/, vervang het domein.
docker compose -f docker-compose.yml -f examples/caddy/docker-compose.prod.yml up -d --buildEerste keer duurt 3–5 minuten (containers bouwen + Let's Encrypt certificaat ophalen).
Verifieer dat alles draait:
docker compose ps
# verwacht: db, api, frontend, caddy — allemaal Updocker-compose.yml start de frontend in Next.js-dev-modus (Turbopack, hot-reload, hoog geheugengebruik). Voor een productiedeployment naast bestaande sites is er docker-compose.prod.yml in de repo:
- Frontend wordt gebouwd via
frontend/Dockerfile.prod(multi-stagenext build+next start) — geheugen ~80 MB i.p.v. >1 GB - Container-namen:
grc-db,grc-api,grc-frontend(zodat ze niet botsen met andere stacks op dezelfde host) - Ports: API op
127.0.0.1:8010, frontend op127.0.0.1:3010(niet 8000/3000) — bedoeld om achter een gedeelde reverse-proxy te draaien - Health-checks op alle drie containers met
depends_on: condition: service_healthy - Eigen volume
grc_postgres_data(geen botsing met dev-volume)
Gebruik:
sudo docker compose -f docker-compose.prod.yml build
sudo docker compose -f docker-compose.prod.yml up -d
sleep 10
sudo docker exec grc-api alembic upgrade headNEXT_PUBLIC_API_URL wordt tijdens de build geïnlined. Default is relatief (/api/v1) wat werkt als API en frontend onder hetzelfde domein draaien achter dezelfde reverse-proxy. Voor een absolute URL (of een aparte API-host) zet je het via env-var:
NEXT_PUBLIC_API_URL=https://grc.example.org/api/v1 \
sudo docker compose -f docker-compose.prod.yml builddocker compose exec api alembic upgrade head17 migraties draaien, in volgorde:
- 001 — Initiële schema (35 tabellen, 67 indexen)
- 002 — Row Level Security policies op 21 tenant-tabellen + speciale policy voor
ims_knowledge_chunks - 003–008 — Seed reference data (IMS-stappen, normenkaders, step-output-definities, agent-conversaties)
- 010 — AI-systemenregister (M4)
- 011 — NIST AI RMF 1.0 als zesde normenkader
- 012–013 — AI Conformity Assessment + HITL-checkpoints (M4)
- 014–015 — Financiële range + Monte Carlo simulatie-historie (M5)
- 016 — Custom-attributes JSONB +
ims_custom_field_definitions(RFC 0001) - 017 — Organizational units met parent-self-FK (RFC 0002)
Eindstand: 41 tabellen, 27 RLS-policies.
Standaard wordt een admin@example.com beheerder aangemaakt. Wijzig direct het e-mailadres en wachtwoord via de UI bij eerste login, of vooraf via SQL als je dat liever scriptbaar doet.
In de UI: Beheer → Organisaties → Nieuwe organisatie — vul de naam van jullie gemeente in. Voeg gebruikers en rollen toe (zie gebruik.md voor de rolverdeling).
./scripts/smoke-test-deployment.sh https://grc.jouwdomein.nlControleert HTTPS, security headers, dat /api/docs en /auth/dev-token uit staan in productie, dat /health en /health/details antwoorden.
Exit-code 0 = alles in orde. Bij fouten: zie output voor welke check faalde.
- Inloggen via
https://grc.jouwdomein.nl/loginwerkt https://grc.jouwdomein.nl/api/v1/health/detailstoont"status": "ok","environment": "production"- Browser-console heeft geen CORS-errors
- Een nieuwe risk aanmaken slaagt en is na refresh nog zichtbaar
crontab -eVoeg toe:
# Dagelijks 03:00 — backup met retentie (7 daily + 4 weekly)
0 3 * * * /opt/grc-platform/scripts/backup-postgres.sh >> /var/log/grc-backup.log 2>&1
# Wekelijks maandag 04:00 — verifieer backup/restore pipeline
0 4 * * 1 /opt/grc-platform/scripts/test-backup-restore.sh >> /var/log/grc-backup-test.log 2>&1Volledige strategie: backup.md. Off-site backups: zie de relevante sectie aldaar.
Stel een externe uptime-monitor in die polled:
GET /api/v1/healthelke 1–5 minuten (basic)GET /api/v1/health/detailselke 1–5 minuten (latency, status)
Alerting-drempels en log-aggregatie: monitoring.md.
Bij een nieuwe release:
cd /opt/grc-platform
git fetch origin
git log --oneline HEAD..origin/main # bekijk wat er verandert
# Maak eerst een handmatige backup
./scripts/backup-postgres.sh
# Pull en herstart
git pull
docker compose -f docker-compose.yml -f examples/caddy/docker-compose.prod.yml up -d --build
docker compose exec api alembic upgrade head
# Verifieer
./scripts/smoke-test-deployment.sh https://grc.jouwdomein.nlRoll-back bij problemen:
git checkout <vorige-commit-hash>
docker compose up -d --build
./scripts/restore-postgres.sh backups/daily/grc-<timestamp>.sql.gz
docker compose exec api alembic upgrade head| Symptoom | Oorzaak | Oplossing |
|---|---|---|
Caddy: tls handshake error: no certificate available |
DNS niet correct of poort 80 dicht | dig <domein> + nc -zv <domein> 80 |
| Browser: CORS-error in console | ALLOWED_ORIGINS in .env mist het frontend-domein, of staat zonder https:// |
corrigeer .env, docker compose restart api |
/api/docs is bereikbaar in productie |
ENVIRONMENT staat nog op development |
zet ENVIRONMENT=production, docker compose restart api |
Login werkt niet, 403 |
Geen seed-data of admin niet aangemaakt | check docker compose exec api alembic current — moet head zijn |
| Backup-script: "POSTGRES_USER not set" | .env ontbreekt of staat op verkeerde locatie |
zet ENV_FILE=/pad/naar/.env of run uit repo-root |
| API geeft 429 vanaf één client (bv. integratie) | Rate limit te streng voor die use case | verhoog RATE_LIMIT_DEFAULT in .env of overweeg agent-token met aparte limiet |
| Disk vol door backups | Retentie te ruim of dump te groot | pas BACKUP_DAILY_RETENTION aan, of off-site backup naar NAS |
- SSH: disable password-auth, alleen public-key
- Unattended-upgrades: automatische security updates voor Ubuntu/Debian
- Fail2ban: rate-limiting op SSH-niveau
- AppArmor/SELinux: profiel voor Docker-daemon
- Audit logging: systemd-journald output streamen naar een log-aggregator
- Backup-encryptie: off-site backups met
gpg --symmetricof restic - VPN-only beheer: sta SSH alleen toe vanuit een VPN-netwerk
Een uitgebreidere checklist staat in security-hardening.md (10 categorieën met statussen ✅ ingebouwd / 🛠️ zelf doen / 💡 aanbevolen extra) + geautomatiseerd verifieerbaar via ./scripts/security-check.sh.
- Logs:
docker compose logs --tail 200 <service>waarbij service ∈ {api, frontend, caddy, db} - Architectuur-vragen:
architectuur.md - GitHub issues: https://github.com/security-commons-nl/grc-platform/issues
- Discussions: https://github.com/security-commons-nl/grc-platform/discussions