chore(infra): production manifests + CI for scheduling apps
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
@@ -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"]
|
||||||
@@ -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 → <AX41 IP>
|
||||||
|
app.dezky.eu → <AX41 IP>
|
||||||
|
booking.dezky.eu → <AX41 IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
*.log
|
||||||
@@ -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"]
|
||||||
Reference in New Issue
Block a user