From 35bc7b6c31d187c7e6e7d8e0fb182b890d6be229 Mon Sep 17 00:00:00 2001 From: Ronni Baslund Date: Sun, 7 Jun 2026 09:27:44 +0200 Subject: [PATCH] chore(infra): production manifests + CI for scheduling apps --- .gitea/workflows/ci.yml | 58 +++++++++ .github/workflows/ci.yml | 58 +++++++++ apps/portal/.dockerignore | 6 + apps/portal/Dockerfile | 24 ++++ infrastructure/production/fleet/README.md | 118 ++++++++++++++++++ .../production/fleet/apps/booking.yaml | 98 +++++++++++++++ .../production/fleet/apps/kustomization.yaml | 12 ++ .../production/fleet/apps/namespace.yaml | 9 ++ .../fleet/apps/platform-api-config.yaml | 17 +++ .../production/fleet/apps/platform-api.yaml | 102 +++++++++++++++ .../production/fleet/apps/portal.yaml | 101 +++++++++++++++ .../fleet/apps/secrets.example.yaml | 52 ++++++++ services/platform-api/.dockerignore | 4 + services/platform-api/Dockerfile | 30 +++++ 14 files changed, 689 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .github/workflows/ci.yml create mode 100644 apps/portal/.dockerignore create mode 100644 apps/portal/Dockerfile create mode 100644 infrastructure/production/fleet/README.md create mode 100644 infrastructure/production/fleet/apps/booking.yaml create mode 100644 infrastructure/production/fleet/apps/kustomization.yaml create mode 100644 infrastructure/production/fleet/apps/namespace.yaml create mode 100644 infrastructure/production/fleet/apps/platform-api-config.yaml create mode 100644 infrastructure/production/fleet/apps/platform-api.yaml create mode 100644 infrastructure/production/fleet/apps/portal.yaml create mode 100644 infrastructure/production/fleet/apps/secrets.example.yaml create mode 100644 services/platform-api/.dockerignore create mode 100644 services/platform-api/Dockerfile diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..8cda0e8 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,58 @@ +# CI for the dezky monorepo (Gitea Actions). Installs deps and typechecks each +# app/service independently — the repo is NOT a single pnpm workspace yet, so +# every app has its own lockfile and is built from its own directory. +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + typecheck: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: + - { name: platform-api, dir: services/platform-api } + - { name: portal, dir: apps/portal } + - { name: booking, dir: apps/booking } + - { name: website, dir: apps/website } + defaults: + run: + working-directory: ${{ matrix.target.dir }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ matrix.target.dir }}/pnpm-lock.yaml + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/platform-api + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: services/platform-api/pnpm-lock.yaml + - name: Install + run: pnpm install --frozen-lockfile + - name: Test + run: pnpm test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8cda0e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +# CI for the dezky monorepo (Gitea Actions). Installs deps and typechecks each +# app/service independently — the repo is NOT a single pnpm workspace yet, so +# every app has its own lockfile and is built from its own directory. +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + typecheck: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: + - { name: platform-api, dir: services/platform-api } + - { name: portal, dir: apps/portal } + - { name: booking, dir: apps/booking } + - { name: website, dir: apps/website } + defaults: + run: + working-directory: ${{ matrix.target.dir }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ${{ matrix.target.dir }}/pnpm-lock.yaml + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/platform-api + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: services/platform-api/pnpm-lock.yaml + - name: Install + run: pnpm install --frozen-lockfile + - name: Test + run: pnpm test diff --git a/apps/portal/.dockerignore b/apps/portal/.dockerignore new file mode 100644 index 0000000..7730896 --- /dev/null +++ b/apps/portal/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.nuxt +.output +.git +dist +*.log diff --git a/apps/portal/Dockerfile b/apps/portal/Dockerfile new file mode 100644 index 0000000..92249b5 --- /dev/null +++ b/apps/portal/Dockerfile @@ -0,0 +1,24 @@ +# Production image for the dezky customer portal (Nuxt 4 SSR). +# Build context = this directory (apps/portal). +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +WORKDIR /app +RUN corepack enable +# Install deps first for layer caching (pnpm version comes from packageManager). +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +RUN pnpm build + +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV NUXT_PUBLIC_PORTAL_URL=https://app.dezky.eu +# OIDC, NUXT_PUBLIC_AUTH_URL and PLATFORM_API_INTERNAL_URL are injected at +# deploy time (see infrastructure/production/fleet/README.md). +COPY --from=build /app/.output ./.output +EXPOSE 3000 +CMD ["node", ".output/server/index.mjs"] diff --git a/infrastructure/production/fleet/README.md b/infrastructure/production/fleet/README.md new file mode 100644 index 0000000..8be6269 --- /dev/null +++ b/infrastructure/production/fleet/README.md @@ -0,0 +1,118 @@ +# dezky production — fleet (k3s app tier) + +k3s manifests for the dezky **application tier** that runs in-cluster on the +Hetzner AX41 node (see `../host/README.md` for the host layer). This layer +deploys the three first-party apps: + +| App | Image | Public host | Internal Service | +|-----|-------|-------------|------------------| +| platform-api | `git.lastcloud.io/ronnibaslund/dezky/platform-api` | `api.dezky.eu` | `platform-api.dezky-apps:3001` | +| portal | `git.lastcloud.io/ronnibaslund/dezky/portal` | `app.dezky.eu` | `portal.dezky-apps:3000` | +| booking | `git.lastcloud.io/ronnibaslund/dezky/booking` | `booking.dezky.eu` | `booking.dezky-apps:3000` | + +All three live in the `dezky-apps` namespace. The data tier (Postgres/Mongo/ +Redis), Authentik and OCIS are added by other parts of the fleet layer and live +in their own namespaces; these manifests reference them by cluster DNS only. + +## Files + +``` +apps/ +├── kustomization.yaml # bundles the non-secret resources +├── namespace.yaml # dezky-apps namespace +├── platform-api.yaml # Deployment + Service + Ingress (api.dezky.eu) +├── platform-api-config.yaml # non-secret ConfigMap (Stalwart URL, toggles) +├── portal.yaml # Deployment + Service + Ingress (app.dezky.eu) +├── booking.yaml # Deployment + Service + Ingress (booking.dezky.eu) +└── secrets.example.yaml # SECRET TEMPLATE — never commit real values +``` + +## Prerequisites (other fleet layers) + +These manifests assume the cluster already has: + +- **Traefik** ingress controller (ships with k3s) — `ingressClassName: traefik`. +- **cert-manager** with a `ClusterIssuer` named **`letsencrypt-prod`** (HTTP-01). + The Ingresses request TLS certs via the `cert-manager.io/cluster-issuer` + annotation; cert-manager fills the named `*-tls` Secrets. +- **MongoDB** reachable at `mongo.dezky-data.svc.cluster.local:27017`. +- **Authentik** reachable publicly at `https://auth.dezky.eu` with OIDC clients + provisioned for the portal. +- **Stalwart** on the host, reachable from the pod CIDR at its node-internal IP + on `:8080` (JMAP management). Update the placeholder IP in + `platform-api-config.yaml`. + +## DNS + +Point these A/AAAA records at the AX41 public IP: + +``` +api.dezky.eu → +app.dezky.eu → +booking.dezky.eu → +``` + +## Deploy + +```bash +# 1) Apply real Secrets out-of-band (NOT from git). Copy the template, +# fill in values, apply — or render SealedSecrets from it. +cp apps/secrets.example.yaml /tmp/dezky-secrets.yaml +$EDITOR /tmp/dezky-secrets.yaml # fill every REPLACE / PASSWORD +kubectl apply -f /tmp/dezky-secrets.yaml +rm /tmp/dezky-secrets.yaml + +# 2) Apply the app tier. +kubectl apply -k apps/ + +# 3) Watch rollout + cert issuance. +kubectl -n dezky-apps rollout status deploy/platform-api +kubectl -n dezky-apps get ingress,certificate +``` + +Images are pushed by CI (`.gitea/workflows/ci.yml`) to the Gitea registry. The +manifests reference `:latest` for convenience; for real releases, set the image +tag to the commit SHA and bump it per deploy (or wire Fleet/ArgoCD to do it). + +## Required env / secrets + +Non-secret config lives in `platform-api-config.yaml` (ConfigMap) and inline +`env:` in each Deployment. Secrets are defined in `secrets.example.yaml` and +**must be supplied at deploy time**: + +### platform-api (`platform-api-secrets`) + +| Key | Purpose | How to get it | +|-----|---------|---------------| +| `MONGODB_URI` | Portal/app database connection | From the in-cluster Mongo credentials | +| `SCHEDULING_CREDENTIAL_KEY` | AES key encrypting stored scheduling creds | `openssl rand -hex 32` | +| `STALWART_ADMIN_PASSWORD` | JMAP management auth | **Same value** as the host `config.env` | +| `STALWART_WEBHOOK_SECRET` | Audit webhook HMAC | **Same value** as the host `config.env` | + +ConfigMap (`platform-api-config`): `STALWART_API_URL`, `STALWART_ADMIN_USER`, +`STALWART_PROVISIONING_ENABLED`. `PORT` and `DEZKY_ENV` are set inline. + +### portal (`portal-secrets`) + +| Key | Purpose | +|-----|---------| +| `NUXT_OIDC_CLIENT_ID` / `NUXT_OIDC_CLIENT_SECRET` | Authentik OIDC client | +| `NUXT_OIDC_REDIRECT_URI` | `https://app.dezky.eu/auth/callback` | +| `NUXT_OIDC_SESSION_SECRET` | Session encryption (`openssl rand -hex 32`) | +| `NUXT_PUBLIC_AUTH_URL` | Public Authentik URL (login + full sign-out) | + +`PLATFORM_API_INTERNAL_URL` / `NUXT_API_BASE` / `NUXT_PUBLIC_PORTAL_URL` are set +inline in `portal.yaml`. + +### booking (`booking-secrets`) + +| Key | Purpose | +|-----|---------| +| `NUXT_PUBLIC_TURNSTILE_SITE_KEY` | Cloudflare Turnstile site key (public, env-injected) | + +`PLATFORM_API_INTERNAL_URL` / `NUXT_PUBLIC_SITE_URL` are set inline in +`booking.yaml`. + +> **Never commit real secret values.** `secrets.example.yaml` is a template and +> is deliberately excluded from `kustomization.yaml`. Manage real secrets via +> sealed-secrets / SOPS / the Rancher secret store. diff --git a/infrastructure/production/fleet/apps/booking.yaml b/infrastructure/production/fleet/apps/booking.yaml new file mode 100644 index 0000000..5406e28 --- /dev/null +++ b/infrastructure/production/fleet/apps/booking.yaml @@ -0,0 +1,98 @@ +# booking — Nuxt 4 SSR public booking app on booking.dezky.eu. Fully public +# (no OIDC); only calls platform-api's /api/v1/public/* through its nitro proxy. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: booking + namespace: dezky-apps + labels: + app.kubernetes.io/name: booking + app.kubernetes.io/part-of: dezky +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: booking + template: + metadata: + labels: + app.kubernetes.io/name: booking + spec: + containers: + - name: booking + image: git.lastcloud.io/ronnibaslund/dezky/booking:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 3000 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "3000" + - name: NUXT_PUBLIC_SITE_URL + value: https://booking.dezky.eu + - name: PLATFORM_API_INTERNAL_URL + value: http://platform-api.dezky-apps.svc.cluster.local:3001 + # NUXT_PUBLIC_TURNSTILE_SITE_KEY is public but env-injected so the key + # can rotate without a rebuild; lives in the Secret. See README.md. + envFrom: + - secretRef: + name: booking-secrets + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + memory: 512Mi + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: booking + namespace: dezky-apps + labels: + app.kubernetes.io/name: booking +spec: + selector: + app.kubernetes.io/name: booking + ports: + - name: http + port: 3000 + targetPort: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: booking + namespace: dezky-apps + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + tls: + - hosts: + - booking.dezky.eu + secretName: booking-dezky-eu-tls + rules: + - host: booking.dezky.eu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: booking + port: + number: 3000 diff --git a/infrastructure/production/fleet/apps/kustomization.yaml b/infrastructure/production/fleet/apps/kustomization.yaml new file mode 100644 index 0000000..02c62c5 --- /dev/null +++ b/infrastructure/production/fleet/apps/kustomization.yaml @@ -0,0 +1,12 @@ +# Kustomization for the dezky application tier. Real Secrets are applied +# out-of-band (sealed-secrets / SOPS), so secrets.example.yaml is intentionally +# NOT listed here — it is a template only. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: dezky-apps +resources: + - namespace.yaml + - platform-api-config.yaml + - platform-api.yaml + - portal.yaml + - booking.yaml diff --git a/infrastructure/production/fleet/apps/namespace.yaml b/infrastructure/production/fleet/apps/namespace.yaml new file mode 100644 index 0000000..d7ed09e --- /dev/null +++ b/infrastructure/production/fleet/apps/namespace.yaml @@ -0,0 +1,9 @@ +# Namespace for the dezky application tier (portal, booking, platform-api). +# The data tier (Postgres/Mongo/Redis), Authentik and OCIS live in their own +# namespaces added elsewhere in the fleet layer. +apiVersion: v1 +kind: Namespace +metadata: + name: dezky-apps + labels: + app.kubernetes.io/part-of: dezky diff --git a/infrastructure/production/fleet/apps/platform-api-config.yaml b/infrastructure/production/fleet/apps/platform-api-config.yaml new file mode 100644 index 0000000..ec3e9b2 --- /dev/null +++ b/infrastructure/production/fleet/apps/platform-api-config.yaml @@ -0,0 +1,17 @@ +# Non-secret runtime config for platform-api. Cluster-internal service +# addresses and integration toggles. Secrets (Mongo URI, credential key, +# Stalwart password, webhook secret) live in the platform-api-secrets Secret. +apiVersion: v1 +kind: ConfigMap +metadata: + name: platform-api-config + namespace: dezky-apps + labels: + app.kubernetes.io/name: platform-api +data: + # Stalwart runs on the HOST (not k3s). Reach it on the node-internal IP at the + # JMAP management port; the firewall lets the pod CIDR through. Override the + # placeholder IP to match the host's actual internal address. + STALWART_API_URL: "http://10.0.0.1:8080" + STALWART_ADMIN_USER: "admin" + STALWART_PROVISIONING_ENABLED: "true" diff --git a/infrastructure/production/fleet/apps/platform-api.yaml b/infrastructure/production/fleet/apps/platform-api.yaml new file mode 100644 index 0000000..36c885c --- /dev/null +++ b/infrastructure/production/fleet/apps/platform-api.yaml @@ -0,0 +1,102 @@ +# platform-api — NestJS control plane (tenants, partners, users, scheduling, +# provisioning). Internal-only Service on :3001 plus a public Ingress for +# api.dezky.eu (consumed by booking's nitro proxy and the Stalwart webhook). +apiVersion: apps/v1 +kind: Deployment +metadata: + name: platform-api + namespace: dezky-apps + labels: + app.kubernetes.io/name: platform-api + app.kubernetes.io/part-of: dezky +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: platform-api + template: + metadata: + labels: + app.kubernetes.io/name: platform-api + spec: + containers: + - name: platform-api + # Pinned by CI to the commit SHA on release; :latest is dev-only. + image: git.lastcloud.io/ronnibaslund/dezky/platform-api:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 3001 + env: + - name: PORT + value: "3001" + - name: DEZKY_ENV + value: production + # Non-secret config (Stalwart URL, feature toggles, etc.) comes from a + # ConfigMap; secrets (Mongo URI, credential key, Stalwart password, + # webhook secret) come from the Secret. See README.md. + envFrom: + - configMapRef: + name: platform-api-config + - secretRef: + name: platform-api-secrets + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + memory: 512Mi + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: platform-api + namespace: dezky-apps + labels: + app.kubernetes.io/name: platform-api +spec: + selector: + app.kubernetes.io/name: platform-api + ports: + - name: http + port: 3001 + targetPort: http +--- +# Public ingress for api.dezky.eu. TLS via cert-manager (HTTP-01) + Traefik. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: platform-api + namespace: dezky-apps + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + tls: + - hosts: + - api.dezky.eu + secretName: api-dezky-eu-tls + rules: + - host: api.dezky.eu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: platform-api + port: + number: 3001 diff --git a/infrastructure/production/fleet/apps/portal.yaml b/infrastructure/production/fleet/apps/portal.yaml new file mode 100644 index 0000000..75e8a39 --- /dev/null +++ b/infrastructure/production/fleet/apps/portal.yaml @@ -0,0 +1,101 @@ +# portal — Nuxt 4 SSR customer portal on app.dezky.eu. Talks to platform-api +# over the cluster network via its nitro proxy (PLATFORM_API_INTERNAL_URL) and +# authenticates users through Authentik (OIDC). +apiVersion: apps/v1 +kind: Deployment +metadata: + name: portal + namespace: dezky-apps + labels: + app.kubernetes.io/name: portal + app.kubernetes.io/part-of: dezky +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: portal + template: + metadata: + labels: + app.kubernetes.io/name: portal + spec: + containers: + - name: portal + image: git.lastcloud.io/ronnibaslund/dezky/portal:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 3000 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "3000" + - name: NUXT_PUBLIC_PORTAL_URL + value: https://app.dezky.eu + # Cluster-internal address of platform-api for the nitro proxy. + - name: PLATFORM_API_INTERNAL_URL + value: http://platform-api.dezky-apps.svc.cluster.local:3001 + - name: NUXT_API_BASE + value: http://platform-api.dezky-apps.svc.cluster.local:3001 + # OIDC client id/secret + Authentik public URL come from the Secret. + envFrom: + - secretRef: + name: portal-secrets + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + memory: 512Mi + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: portal + namespace: dezky-apps + labels: + app.kubernetes.io/name: portal +spec: + selector: + app.kubernetes.io/name: portal + ports: + - name: http + port: 3000 + targetPort: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: portal + namespace: dezky-apps + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + tls: + - hosts: + - app.dezky.eu + secretName: app-dezky-eu-tls + rules: + - host: app.dezky.eu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: portal + port: + number: 3000 diff --git a/infrastructure/production/fleet/apps/secrets.example.yaml b/infrastructure/production/fleet/apps/secrets.example.yaml new file mode 100644 index 0000000..bab8527 --- /dev/null +++ b/infrastructure/production/fleet/apps/secrets.example.yaml @@ -0,0 +1,52 @@ +# ───────────────────────────────────────────────────────────────────────── +# Secret TEMPLATE — DO NOT COMMIT REAL VALUES. +# +# These are placeholders. In production, manage the real Secrets out-of-band +# (sealed-secrets / SOPS / Rancher secret store), NOT in git. Copy this file, +# fill in real values, and apply it separately — or render SealedSecrets from +# it. The Deployments reference these Secrets by name via envFrom. +# +# Generate strong values with: openssl rand -hex 32 +# ───────────────────────────────────────────────────────────────────────── +apiVersion: v1 +kind: Secret +metadata: + name: platform-api-secrets + namespace: dezky-apps +type: Opaque +stringData: + # Mongo connection string for the in-cluster MongoDB (data tier namespace). + MONGODB_URI: "mongodb://USER:PASSWORD@mongo.dezky-data.svc.cluster.local:27017/dezky?authSource=admin" + # AES key used to encrypt stored scheduling credentials (e.g. CalDAV creds). + SCHEDULING_CREDENTIAL_KEY: "REPLACE_WITH_openssl_rand_hex_32" + # MUST equal the host's STALWART_ADMIN_PASSWORD (config.env on the AX41). + STALWART_ADMIN_PASSWORD: "REPLACE_WITH_SAME_AS_HOST" + # MUST equal the host's STALWART_WEBHOOK_SECRET (audit webhook HMAC). + STALWART_WEBHOOK_SECRET: "REPLACE_WITH_SAME_AS_HOST" +--- +apiVersion: v1 +kind: Secret +metadata: + name: portal-secrets + namespace: dezky-apps +type: Opaque +stringData: + # Authentik OIDC client provisioned for the portal. + NUXT_OIDC_CLIENT_ID: "REPLACE" + NUXT_OIDC_CLIENT_SECRET: "REPLACE" + NUXT_OIDC_REDIRECT_URI: "https://app.dezky.eu/auth/callback" + # Public base URL of Authentik (used for login redirects + full sign-out). + NUXT_PUBLIC_AUTH_URL: "https://auth.dezky.eu" + # nuxt-oidc-auth session encryption secret (openssl rand -hex 32). + NUXT_OIDC_SESSION_SECRET: "REPLACE_WITH_openssl_rand_hex_32" +--- +apiVersion: v1 +kind: Secret +metadata: + name: booking-secrets + namespace: dezky-apps +type: Opaque +stringData: + # Cloudflare Turnstile site key for the public booking form (public value, + # env-injected so it can rotate without a rebuild). + NUXT_PUBLIC_TURNSTILE_SITE_KEY: "REPLACE" diff --git a/services/platform-api/.dockerignore b/services/platform-api/.dockerignore new file mode 100644 index 0000000..3be2309 --- /dev/null +++ b/services/platform-api/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.git +*.log diff --git a/services/platform-api/Dockerfile b/services/platform-api/Dockerfile new file mode 100644 index 0000000..be64fc1 --- /dev/null +++ b/services/platform-api/Dockerfile @@ -0,0 +1,30 @@ +# Production image for the dezky platform-api (NestJS, ESM + NodeNext). +# Build context = this directory (services/platform-api). +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +WORKDIR /app +RUN corepack enable +# Install deps first for layer caching (pnpm version comes from packageManager). +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +RUN pnpm build + +FROM node:22-alpine AS deps +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml ./ +# Production-only deps for the runtime layer (no dev/build tooling). +RUN pnpm install --frozen-lockfile --prod + +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3001 +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY package.json ./ +EXPOSE 3001 +# main.ts compiles to dist/main.js; NodeNext ESM output is run directly by node. +CMD ["node", "dist/main.js"]