From 56e8fa5e1d8d3762a212a2e4c12a4a4ff8ae7a68 Mon Sep 17 00:00:00 2001 From: Abdikhani Abdirashid Date: Sat, 31 Oct 2026 15:22:34 +0100 Subject: [PATCH 1/4] infra: add the dashboard manifests for cdn.myrelay.tech --- k8s/dashboard/dashboard.yaml | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 k8s/dashboard/dashboard.yaml diff --git a/k8s/dashboard/dashboard.yaml b/k8s/dashboard/dashboard.yaml new file mode 100644 index 0000000..254c4e1 --- /dev/null +++ b/k8s/dashboard/dashboard.yaml @@ -0,0 +1,106 @@ +# The Relay dashboard on https://cdn.myrelay.tech: static files built by Vite and +# served by nginx. Deployment, Service and Ingress, like the API's own manifests in +# k8s/app/, with four differences: port 8080 instead of 80 (the unprivileged nginx +# image cannot bind a low port), probes so a rollout never sends traffic to a pod +# that is not serving yet, small resource requests because the VPS is shared with +# Postgres, the API and Grafana, and cdn.myrelay.tech instead of api.myrelay.tech. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dashboard +spec: + replicas: 2 # two pods, so a rollout always has one serving + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 # never take a pod down before its replacement is ready + maxSurge: 1 + selector: + matchLabels: + app: dashboard + template: + metadata: + labels: + app: dashboard + spec: + containers: + - name: nginx + # Tag is the git SHA, set by the pipeline. Never "latest": a rollback has + # to name an exact build, and "latest" means something different tomorrow. + image: ghcr.io/devinder-dev/relay-dashboard:PLACEHOLDER_SHA + ports: + - containerPort: 8080 + readinessProbe: + # Kubernetes sends no traffic here until this answers. Without it, a new + # pod receives requests while nginx is still starting. + httpGet: + path: /index.html + port: 8080 + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + # If this stops answering, the pod is restarted. + httpGet: + path: /index.html + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + # Serving static files needs almost nothing. The requests exist so the + # scheduler knows this pod is cheap and leaves room for the database. + requests: + cpu: 10m + memory: 24Mi + limits: + memory: 64Mi + securityContext: + # Namespace relay enforces Pod Security "restricted" (k8s/platform/deployer.yaml), + # which requires all four of these. runAsNonRoot and seccompProfile are not + # optional hardening here: without them the API server refuses the pod, even + # though the image already runs as uid 101. Kubernetes does not look inside the + # image to check, so being non-root has to be declared. + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + allowPrivilegeEscalation: false + # Not required by "restricted". Left false because nginx writes its cache and + # pid file under /var; making it read-only would mean emptyDir mounts for both. + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +--- +apiVersion: v1 +kind: Service +metadata: + name: dashboard +spec: + selector: + app: dashboard + ports: + - port: 80 # the Service listens on 80 inside the cluster... + targetPort: 8080 # ...and forwards to 8080 in the pod +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - cdn.myrelay.tech + secretName: cdn-myrelay-tech-tls + rules: + - host: cdn.myrelay.tech + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dashboard + port: + number: 80 From f1d46a0ceb73f7895a155518a132598adff11e67 Mon Sep 17 00:00:00 2001 From: Abdikhani Abdirashid Date: Tue, 3 Nov 2026 09:17:59 +0100 Subject: [PATCH 2/4] infra: move the dashboard manifest into k8s/app and fix its image tag --- k8s/{dashboard => app}/dashboard.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename k8s/{dashboard => app}/dashboard.yaml (93%) diff --git a/k8s/dashboard/dashboard.yaml b/k8s/app/dashboard.yaml similarity index 93% rename from k8s/dashboard/dashboard.yaml rename to k8s/app/dashboard.yaml index 254c4e1..9f8a380 100644 --- a/k8s/dashboard/dashboard.yaml +++ b/k8s/app/dashboard.yaml @@ -8,7 +8,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: dashboard + name: relay-dashboard spec: replicas: 2 # two pods, so a rollout always has one serving strategy: @@ -18,17 +18,17 @@ spec: maxSurge: 1 selector: matchLabels: - app: dashboard + app: relay-dashboard template: metadata: labels: - app: dashboard + app: relay-dashboard spec: containers: - name: nginx # Tag is the git SHA, set by the pipeline. Never "latest": a rollback has # to name an exact build, and "latest" means something different tomorrow. - image: ghcr.io/devinder-dev/relay-dashboard:PLACEHOLDER_SHA + image: ghcr.io/devinder-dev/relay-dashboard:__GIT_SHA__ ports: - containerPort: 8080 readinessProbe: @@ -73,10 +73,10 @@ spec: apiVersion: v1 kind: Service metadata: - name: dashboard + name: relay-dashboard spec: selector: - app: dashboard + app: relay-dashboard ports: - port: 80 # the Service listens on 80 inside the cluster... targetPort: 8080 # ...and forwards to 8080 in the pod @@ -84,7 +84,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: dashboard + name: relay-dashboard annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: @@ -101,6 +101,6 @@ spec: pathType: Prefix backend: service: - name: dashboard + name: relay-dashboard port: number: 80 From ede6fdfb54c9576d665ab403a3d37afda4c818e5 Mon Sep 17 00:00:00 2001 From: Abdikhani Abdirashid Date: Tue, 3 Nov 2026 21:59:22 +0100 Subject: [PATCH 3/4] infra: let Traefik stop routing before a dashboard pod shuts down --- k8s/app/dashboard.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/k8s/app/dashboard.yaml b/k8s/app/dashboard.yaml index 9f8a380..18a911f 100644 --- a/k8s/app/dashboard.yaml +++ b/k8s/app/dashboard.yaml @@ -46,6 +46,16 @@ spec: port: 8080 initialDelaySeconds: 10 periodSeconds: 20 + # Before SIGTERM, wait 5 s, so Traefik stops sending this pod traffic first. + # Removing a pod from the Service and telling it to stop happen at the same moment, + # but Traefik needs a moment to hear about the removal. Without this pause nginx is + # already shutting down while Traefik still routes here, and those requests fail. + # terminationGracePeriodSeconds stays at its default 30 s, which comfortably holds + # these 5 s: nginx serves static files, so it has nothing to drain afterwards. + lifecycle: + preStop: + sleep: + seconds: 5 resources: # Serving static files needs almost nothing. The requests exist so the # scheduler knows this pod is cheap and leaves room for the database. From 9583e41bca83b710b956eab715ec37069c6b144b Mon Sep 17 00:00:00 2001 From: Abdikhani Abdirashid Date: Tue, 3 Nov 2026 22:01:26 +0100 Subject: [PATCH 4/4] infra: label the dashboard like the API --- k8s/app/dashboard.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/k8s/app/dashboard.yaml b/k8s/app/dashboard.yaml index 18a911f..4bfe4d3 100644 --- a/k8s/app/dashboard.yaml +++ b/k8s/app/dashboard.yaml @@ -9,6 +9,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: relay-dashboard + labels: + app.kubernetes.io/name: relay-dashboard spec: replicas: 2 # two pods, so a rollout always has one serving strategy: @@ -18,11 +20,11 @@ spec: maxSurge: 1 selector: matchLabels: - app: relay-dashboard + app.kubernetes.io/name: relay-dashboard # manage the pods that carry this label... template: metadata: labels: - app: relay-dashboard + app.kubernetes.io/name: relay-dashboard # ...and give every new pod this label spec: containers: - name: nginx @@ -86,7 +88,7 @@ metadata: name: relay-dashboard spec: selector: - app: relay-dashboard + app.kubernetes.io/name: relay-dashboard # the label the Deployment gives its pods ports: - port: 80 # the Service listens on 80 inside the cluster... targetPort: 8080 # ...and forwards to 8080 in the pod