Initiële opzet: Fastify + static, Docker, Postgres, Forgejo Actions
Some checks are pending
dev - build & deploy naar test / build-and-deploy (push) Waiting to run
Some checks are pending
dev - build & deploy naar test / build-and-deploy (push) Waiting to run
This commit is contained in:
commit
516a048ce7
14 changed files with 6453 additions and 0 deletions
14
.dockerignore
Normal file
14
.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
compose*.yaml
|
||||||
|
deploy
|
||||||
|
.forgejo
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
15
.env.example
Normal file
15
.env.example
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Kopieer naar .env voor lokaal draaien (nooit .env committen).
|
||||||
|
# Op test/prod komen deze waarden uit een .env naast de deploy-compose op de VM.
|
||||||
|
|
||||||
|
# App
|
||||||
|
PORT=3000
|
||||||
|
APP_VERSION=dev
|
||||||
|
|
||||||
|
# PostgreSQL (moet matchen met de db-service in compose)
|
||||||
|
POSTGRES_DB=teach
|
||||||
|
POSTGRES_USER=teach
|
||||||
|
POSTGRES_PASSWORD=change-me-locally
|
||||||
|
|
||||||
|
# Connectiestring die de app gebruikt.
|
||||||
|
# Hostnaam 'db' verwijst naar de postgres-service binnen het compose-netwerk.
|
||||||
|
DATABASE_URL=postgres://teach:change-me-locally@db:5432/teach
|
||||||
62
.forgejo/workflows/dev.yaml
Normal file
62
.forgejo/workflows/dev.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
name: dev - build & deploy naar test
|
||||||
|
|
||||||
|
# Elke push naar dev bouwt een image en zet die op de test-VM.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-test
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
|
||||||
|
env:
|
||||||
|
# git.jouwdomein.nl - host van je Forgejo (= container registry host)
|
||||||
|
REGISTRY: ${{ vars.REGISTRY }}
|
||||||
|
IMAGE_NAME: ${{ github.repository }} # bv. ramon/teach
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Image-tags bepalen
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "sha_tag=${IMAGE}:dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "dev_tag=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Login bij Forgejo registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build & push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=dev-${{ github.sha }}
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.sha_tag }}
|
||||||
|
${{ steps.meta.outputs.dev_tag }}
|
||||||
|
|
||||||
|
- name: Deploy naar test-VM
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.TEST_HOST }}
|
||||||
|
username: ${{ secrets.TEST_USER }}
|
||||||
|
key: ${{ secrets.TEST_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd ${{ secrets.TEST_DEPLOY_PATH }}
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
# Zet de te draaien image-tag in de env van de VM
|
||||||
|
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|" .env
|
||||||
|
docker compose -f compose.deploy.yaml pull
|
||||||
|
docker compose -f compose.deploy.yaml up -d
|
||||||
|
docker image prune -f
|
||||||
63
.forgejo/workflows/release.yaml
Normal file
63
.forgejo/workflows/release.yaml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
name: release - build & deploy naar productie
|
||||||
|
|
||||||
|
# Draait wanneer je in Forgejo een release publiceert.
|
||||||
|
# De release-tag (bv. v1.2.0) wordt de image-tag op productie.
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-prod
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
|
||||||
|
env:
|
||||||
|
REGISTRY: ${{ vars.REGISTRY }}
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Image-tags bepalen
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
VERSION="${GITHUB_REF_NAME}" # de release-/tagnaam, bv. v1.2.0
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version_tag=${IMAGE}:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "latest_tag=${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Login bij Forgejo registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build & push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ steps.meta.outputs.version }}
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.version_tag }}
|
||||||
|
${{ steps.meta.outputs.latest_tag }}
|
||||||
|
|
||||||
|
- name: Deploy naar productie-VM
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.PROD_HOST }}
|
||||||
|
username: ${{ secrets.PROD_USER }}
|
||||||
|
key: ${{ secrets.PROD_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|" .env
|
||||||
|
docker compose -f compose.deploy.yaml pull
|
||||||
|
docker compose -f compose.deploy.yaml up -d
|
||||||
|
docker image prune -f
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Lokale env-bestanden met secrets nooit committen
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Postgres data volume bij lokaal draaien
|
||||||
|
pgdata/
|
||||||
|
|
||||||
|
# OS / editor
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# --- Dependencies (alleen productie) ----------------------------------------
|
||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
# npm ci als er een lockfile is, anders install (eerste keer nog geen lock)
|
||||||
|
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
|
||||||
|
|
||||||
|
# --- Runtime ----------------------------------------------------------------
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Draai als non-root gebruiker
|
||||||
|
RUN addgroup -S app && adduser -S app -G app
|
||||||
|
|
||||||
|
COPY --chown=app:app --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --chown=app:app package.json ./
|
||||||
|
COPY --chown=app:app src ./src
|
||||||
|
COPY --chown=app:app public ./public
|
||||||
|
|
||||||
|
# Versie wordt tijdens de build meegegeven door CI
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ENV APP_VERSION=$APP_VERSION
|
||||||
|
|
||||||
|
USER app
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
|
||||||
|
CMD ["node", "src/server.js"]
|
||||||
131
README.md
Normal file
131
README.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Teach
|
||||||
|
|
||||||
|
Digibord-webapp (Fastify + static frontend) met PostgreSQL, gecontaineriseerd en
|
||||||
|
via Forgejo Actions automatisch uitgerold: `dev` → test-VM, release → productie-VM.
|
||||||
|
|
||||||
|
## Structuur
|
||||||
|
|
||||||
|
```
|
||||||
|
teach/
|
||||||
|
├─ public/index.html # de digibord-app (voorheen teach.html)
|
||||||
|
├─ src/server.js # Fastify: serveert de app + /api + DB-pool
|
||||||
|
├─ db/001_init.sql # initieel Postgres-schema
|
||||||
|
├─ Dockerfile # productie-image (non-root, healthcheck)
|
||||||
|
├─ compose.yaml # lokaal draaien (app + postgres)
|
||||||
|
├─ deploy/compose.deploy.yaml # test/prod: pullt image uit de registry
|
||||||
|
├─ .forgejo/workflows/
|
||||||
|
│ ├─ dev.yaml # push naar dev → build + deploy test
|
||||||
|
│ └─ release.yaml # release → build + deploy prod
|
||||||
|
└─ .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lokaal draaien
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # pas POSTGRES_PASSWORD en DATABASE_URL aan
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
App op http://localhost:3000, healthcheck op `/healthz`, DB-check op `/readyz`.
|
||||||
|
|
||||||
|
## Architectuurkeuzes
|
||||||
|
|
||||||
|
- **Container i.p.v. losse static site**: omdat er een database bijkomt, is een
|
||||||
|
backend nodig (browser praat niet rechtstreeks met Postgres). De Fastify-app
|
||||||
|
serveert de HTML én biedt de `/api`, en praat met de DB.
|
||||||
|
- **Forgejo container registry**: CI bouwt de image één keer en pusht die; beide
|
||||||
|
VM's pullen exact dezelfde geteste image. Geen build op de productie-VM.
|
||||||
|
- **Nginx blijft ervoor** als reverse proxy / TLS op elke VM; de app luistert
|
||||||
|
alleen op `127.0.0.1:<APP_PORT>`.
|
||||||
|
|
||||||
|
## Eenmalige setup
|
||||||
|
|
||||||
|
### 1. Repo aanmaken in Forgejo
|
||||||
|
Maak een leeg repo (bv. `ramon/teach`) en push (zie onderaan).
|
||||||
|
|
||||||
|
### 2. Registry / Actions variabelen en secrets
|
||||||
|
Onder **Settings → Actions → Variables** van het repo (of org):
|
||||||
|
|
||||||
|
| Variable | Voorbeeld | Uitleg |
|
||||||
|
|------------|--------------------------|---------------------------------|
|
||||||
|
| `REGISTRY` | `git.familiebesselink.nl`| Host van je Forgejo = registry |
|
||||||
|
|
||||||
|
Onder **Settings → Actions → Secrets**:
|
||||||
|
|
||||||
|
| Secret | Uitleg |
|
||||||
|
|--------------------|--------------------------------------------------------------|
|
||||||
|
| `REGISTRY_USER` | Forgejo-gebruiker met package-write rechten |
|
||||||
|
| `REGISTRY_TOKEN` | Token/wachtwoord voor die gebruiker (scope: packages) |
|
||||||
|
| `TEST_HOST` | IP/hostname van de test-VM |
|
||||||
|
| `TEST_USER` | SSH-gebruiker op de test-VM |
|
||||||
|
| `TEST_SSH_KEY` | Private SSH-key (deploy key) voor de test-VM |
|
||||||
|
| `TEST_DEPLOY_PATH` | Pad op de test-VM met `compose.deploy.yaml` + `.env` + `db/` |
|
||||||
|
| `PROD_HOST` | IP/hostname van de productie-VM |
|
||||||
|
| `PROD_USER` | SSH-gebruiker op de productie-VM |
|
||||||
|
| `PROD_SSH_KEY` | Private SSH-key voor de productie-VM |
|
||||||
|
| `PROD_DEPLOY_PATH` | Pad op de productie-VM met de deploy-bestanden |
|
||||||
|
|
||||||
|
> De runner moet Docker + buildx beschikbaar hebben en `actions/checkout`,
|
||||||
|
> `docker/*` en `appleboy/ssh-action` kunnen ophalen (standaard van github.com;
|
||||||
|
> instelbaar via de runner-config). Pas `runs-on` in de workflows aan naar het
|
||||||
|
> label van jouw runner als dat niet `ubuntu-latest` is.
|
||||||
|
|
||||||
|
### 3. Deploy-map op elke VM
|
||||||
|
Op zowel de test- als de productie-VM, in het pad dat je bij `*_DEPLOY_PATH`
|
||||||
|
opgeeft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p teach && cd teach
|
||||||
|
# kopieer deze twee uit de repo:
|
||||||
|
# deploy/compose.deploy.yaml -> compose.deploy.yaml
|
||||||
|
# db/ -> db/
|
||||||
|
# maak een .env aan:
|
||||||
|
cat > .env <<'EOF'
|
||||||
|
IMAGE=git.familiebesselink.nl/ramon/teach:dev # prod: laat CI dit op :vX.Y.Z zetten
|
||||||
|
POSTGRES_DB=teach
|
||||||
|
POSTGRES_USER=teach
|
||||||
|
POSTGRES_PASSWORD=<sterk-wachtwoord>
|
||||||
|
DATABASE_URL=postgres://teach:<sterk-wachtwoord>@db:5432/teach
|
||||||
|
APP_PORT=3000
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
CI werkt bij elke deploy de `IMAGE=`-regel bij, pullt en herstart.
|
||||||
|
|
||||||
|
### 4. Nginx reverse proxy (per VM)
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name digibord.familiebesselink.nl; # test-VM eigen subdomein
|
||||||
|
|
||||||
|
# ssl_certificate ... (bestaande config)
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow / branching
|
||||||
|
|
||||||
|
- Werk op feature-branches, merge naar **`dev`** → automatisch naar test.
|
||||||
|
- Tevreden? Maak een **release** (tag `vX.Y.Z`) → automatisch naar productie.
|
||||||
|
|
||||||
|
## Push naar Forgejo (eerste keer)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add origin https://git.familiebesselink.nl/ramon/teach.git
|
||||||
|
git push -u origin main
|
||||||
|
git push -u origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volgende stap: data uit localStorage naar de database
|
||||||
|
|
||||||
|
De frontend bewaart borden/gebruikers nu nog in `localStorage`. Om echt een
|
||||||
|
gedeelde database te gebruiken, bouwen we `/api`-endpoints (boards, folders,
|
||||||
|
users) in `src/server.js` en laten we de frontend die aanroepen i.p.v.
|
||||||
|
`localStorage`. Het schema in `db/001_init.sql` is daarvoor het startpunt.
|
||||||
41
compose.yaml
Normal file
41
compose.yaml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Lokaal ontwikkelen: bouwt de app zelf en start Postgres ernaast.
|
||||||
|
# cp .env.example .env (en pas het wachtwoord aan)
|
||||||
|
# docker compose up --build
|
||||||
|
# App op http://localhost:3000
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
APP_VERSION: dev
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
APP_VERSION: dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
# Init-scripts draaien alleen bij een lege database
|
||||||
|
- ./db:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
34
db/001_init.sql
Normal file
34
db/001_init.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- Initieel schema voor teach.
|
||||||
|
-- Dit bestand wordt bij een verse Postgres-container automatisch uitgevoerd
|
||||||
|
-- (docker-entrypoint-initdb.d). Bij bestaande databases draai je migraties apart.
|
||||||
|
--
|
||||||
|
-- Onderstaande tabellen zijn een startpunt: ze weerspiegelen wat nu in
|
||||||
|
-- localStorage zit (gebruikers en borden). Pas ze aan zodra de API-vorm vaststaat.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
role TEXT NOT NULL DEFAULT 'teacher',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS folders (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS boards (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
folder_id BIGINT REFERENCES folders(id) ON DELETE SET NULL,
|
||||||
|
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
-- De volledige bordinhoud (tekeningen, widgets, blokken) als JSON.
|
||||||
|
content JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_boards_owner ON boards(owner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_boards_folder ON boards(folder_id);
|
||||||
45
deploy/compose.deploy.yaml
Normal file
45
deploy/compose.deploy.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Deploy-compose voor test/prod. Draait op de VM.
|
||||||
|
# De app-image wordt NIET gebouwd maar uit de Forgejo registry gepulld.
|
||||||
|
# CI zet het juiste IMAGE (incl. tag) in een .env naast dit bestand en draait:
|
||||||
|
# docker compose -f compose.deploy.yaml pull
|
||||||
|
# docker compose -f compose.deploy.yaml up -d
|
||||||
|
#
|
||||||
|
# Verwachte env-variabelen op de VM (in ./.env):
|
||||||
|
# IMAGE=git.jouwdomein.nl/ramon/teach:dev (of :vX.Y.Z op prod)
|
||||||
|
# POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD
|
||||||
|
# DATABASE_URL=postgres://<user>:<pw>@db:5432/<db>
|
||||||
|
# APP_PORT=3000 (poort die nginx op deze VM naar de app proxyt)
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: ${IMAGE}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
APP_VERSION: ${IMAGE}
|
||||||
|
ports:
|
||||||
|
# Alleen op localhost van de VM; nginx zit ervoor als reverse proxy
|
||||||
|
- "127.0.0.1:${APP_PORT:-3000}:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./db:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
1187
package-lock.json
generated
Normal file
1187
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "teach",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Teach digibord - Fastify server met static frontend en PostgreSQL",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/server.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js",
|
||||||
|
"dev": "node --watch src/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/static": "^8.0.1",
|
||||||
|
"fastify": "^5.1.0",
|
||||||
|
"pg": "^8.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
4712
public/index.html
Normal file
4712
public/index.html
Normal file
File diff suppressed because it is too large
Load diff
81
src/server.js
Normal file
81
src/server.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import fastifyStatic from '@fastify/static';
|
||||||
|
import pg from 'pg';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT ?? 3000);
|
||||||
|
const HOST = process.env.HOST ?? '0.0.0.0';
|
||||||
|
|
||||||
|
const app = Fastify({
|
||||||
|
logger: true,
|
||||||
|
trustProxy: true, // achter nginx reverse proxy
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Database pool -----------------------------------------------------------
|
||||||
|
// Verbinding via DATABASE_URL, bv: postgres://teach:pw@db:5432/teach
|
||||||
|
const pool = new pg.Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
max: Number(process.env.PG_POOL_MAX ?? 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Maak de pool bereikbaar in routes via app.pg
|
||||||
|
app.decorate('pg', pool);
|
||||||
|
|
||||||
|
// --- Health checks (gebruikt door Docker + load balancer) --------------------
|
||||||
|
app.get('/healthz', async () => ({ status: 'ok' }));
|
||||||
|
|
||||||
|
app.get('/readyz', async (req, reply) => {
|
||||||
|
try {
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
return { status: 'ready' };
|
||||||
|
} catch (err) {
|
||||||
|
req.log.error({ err }, 'database not ready');
|
||||||
|
reply.code(503);
|
||||||
|
return { status: 'db_unavailable' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- API ---------------------------------------------------------------------
|
||||||
|
// Voorbeeld-endpoint. Hier komt straks de board/user-API die nu nog in
|
||||||
|
// localStorage zit. Prefix /api houdt het netjes gescheiden van de static app.
|
||||||
|
app.register(
|
||||||
|
async (api) => {
|
||||||
|
api.get('/version', async () => ({
|
||||||
|
name: 'teach',
|
||||||
|
version: process.env.APP_VERSION ?? 'dev',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
{ prefix: '/api' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Static frontend ---------------------------------------------------------
|
||||||
|
// Serveert public/index.html (de digibord-app) op /
|
||||||
|
app.register(fastifyStatic, {
|
||||||
|
root: join(__dirname, '..', 'public'),
|
||||||
|
index: ['index.html'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Start -------------------------------------------------------------------
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await app.listen({ port: PORT, host: HOST });
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nette shutdown zodat Docker de container snel kan stoppen
|
||||||
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||||
|
process.on(signal, async () => {
|
||||||
|
app.log.info(`${signal} ontvangen, afsluiten...`);
|
||||||
|
await app.close();
|
||||||
|
await pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
Loading…
Reference in a new issue