From 50dce9eaf72243af0a6f12b644327b783ed51626 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sat, 29 Aug 2026 06:52:32 +0000 Subject: [PATCH] fix(deploy): align runtime identity and ingress --- Dockerfile | 4 ++-- Plan.md | 2 +- README.md | 11 ++++++----- k8s/base/deployment.yaml | 4 ++++ k8s/base/ingress.yaml | 7 ------- k8s/base/worker-deployment.yaml | 4 ++++ k8s/migration/job.yaml | 4 ++++ tests/deployment-contract.test.ts | 9 ++++++--- 8 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index c8627ae..13c6463 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ COPY --chown=bun:bun db ./db COPY --chown=bun:bun drizzle ./drizzle COPY --chown=bun:bun scripts/migrate.ts ./scripts/migrate.ts COPY --chown=bun:bun tsconfig.json ./tsconfig.json -USER bun +USER 1000:1000 ENTRYPOINT ["bun", "scripts/migrate.ts"] FROM oven/bun:${BUN_VERSION} AS app @@ -52,6 +52,6 @@ COPY --from=next-builder --chown=bun:bun /app/.next/standalone ./ COPY --from=next-builder --chown=bun:bun /app/.next/static ./.next/static COPY --from=next-builder --chown=bun:bun /app/public ./public COPY --from=worker-builder --chown=bun:bun /app/dist/outbox-worker.js ./worker/outbox-worker.js -USER bun +USER 1000:1000 EXPOSE 3000 CMD ["bun", "server.js"] diff --git a/Plan.md b/Plan.md index 39614f2..ea3a5a0 100644 --- a/Plan.md +++ b/Plan.md @@ -148,7 +148,7 @@ flowchart LR - Two application replicas with rolling updates - ClusterIP service on port 3000 - Traefik ingress for `sheet.sudloh.com` - - Externally provisioned `sheet-sudloh-com-tls` secret + - Cloudflare edge TLS with the standard HTTP Traefik origin route - Readiness/liveness health endpoint - Requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB - HPA from 2–6 replicas at 70% CPU diff --git a/README.md b/README.md index 6081842..2dc528f 100644 --- a/README.md +++ b/README.md @@ -163,8 +163,9 @@ Without `REDIS_INTEGRATION_URL`, that external-service test is skipped. ## Containers The application image compiles Next.js in a Node builder stage, then runs the -standalone server and bundled outbox worker on Bun as the unprivileged `bun` -user. The migration image runs the committed Drizzle migrations on Bun. +standalone server and bundled outbox worker on Bun as the unprivileged numeric +UID/GID `1000:1000`. The migration image runs the committed Drizzle migrations +with the same identity. ```bash docker build \ @@ -190,7 +191,7 @@ Kustomize resources live in `k8s/` and define: - Namespace `buzz-sheet`. - Two rolling web replicas and one outbox worker. - ClusterIP port 3000 and Traefik ingress for `sheet.sudloh.com`. -- Externally provisioned `sheet-sudloh-com-tls`. +- Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route. - Startup, liveness, and dependency-aware readiness probes. - Web requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB. - HPA from 2 to 6 web replicas at 70% CPU and a PDB with one available. @@ -202,8 +203,8 @@ not create in-cluster data stores or credentials. ### External prerequisites -Before the first rollout, a cluster administrator must provision the TLS secret -and a `buzz-sheet-env` Secret in the `buzz-sheet` namespace containing: +Before the first rollout, a cluster administrator must provision a +`buzz-sheet-env` Secret in the `buzz-sheet` namespace containing: ```text DATABASE_URL diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml index 13780a8..f3acbe6 100644 --- a/k8s/base/deployment.yaml +++ b/k8s/base/deployment.yaml @@ -27,6 +27,10 @@ spec: terminationGracePeriodSeconds: 30 securityContext: runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault containers: diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml index cb3db73..c3940e1 100644 --- a/k8s/base/ingress.yaml +++ b/k8s/base/ingress.yaml @@ -4,15 +4,8 @@ metadata: name: buzz-sheet labels: app.kubernetes.io/name: buzz-sheet - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure - traefik.ingress.kubernetes.io/router.tls: "true" spec: ingressClassName: traefik - tls: - - hosts: - - sheet.sudloh.com - secretName: sheet-sudloh-com-tls rules: - host: sheet.sudloh.com http: diff --git a/k8s/base/worker-deployment.yaml b/k8s/base/worker-deployment.yaml index 1793297..826841b 100644 --- a/k8s/base/worker-deployment.yaml +++ b/k8s/base/worker-deployment.yaml @@ -27,6 +27,10 @@ spec: terminationGracePeriodSeconds: 35 securityContext: runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault containers: diff --git a/k8s/migration/job.yaml b/k8s/migration/job.yaml index 86f0e7a..25fec83 100644 --- a/k8s/migration/job.yaml +++ b/k8s/migration/job.yaml @@ -19,6 +19,10 @@ spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault containers: diff --git a/tests/deployment-contract.test.ts b/tests/deployment-contract.test.ts index d926434..8242e7c 100644 --- a/tests/deployment-contract.test.ts +++ b/tests/deployment-contract.test.ts @@ -19,11 +19,13 @@ describe("production deployment contract", () => { /requests:\s+cpu: 500m\s+memory: 512Mi\s+limits:\s+cpu: "1"\s+memory: 1Gi/u, ); expect(deployment).toContain("runAsNonRoot: true"); + expect(deployment).toContain("runAsUser: 1000"); + expect(deployment).toContain("runAsGroup: 1000"); expect(deployment).toContain("readOnlyRootFilesystem: true"); expect(deployment).toContain('drop: ["ALL"]'); }); - it("exposes only the app through the requested service and TLS host", async () => { + it("exposes only the app through the requested edge-TLS host", async () => { const [service, ingress] = await Promise.all([ repositoryFile("k8s/base/service.yaml"), repositoryFile("k8s/base/ingress.yaml"), @@ -34,7 +36,8 @@ describe("production deployment contract", () => { expect(service).toContain("targetPort: http"); expect(ingress).toContain("ingressClassName: traefik"); expect(ingress).toContain("host: sheet.sudloh.com"); - expect(ingress).toContain("secretName: sheet-sudloh-com-tls"); + expect(ingress).not.toContain("secretName:"); + expect(ingress).not.toContain("router.tls"); }); it("keeps availability and scaling bounds explicit", async () => { @@ -78,7 +81,7 @@ describe("production deployment contract", () => { expect(dockerfile).toContain("FROM dependencies AS migration"); expect(dockerfile).toContain("FROM oven/bun:${BUN_VERSION} AS app"); - expect(dockerfile.match(/^USER bun$/gmu)).toHaveLength(2); + expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2); expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]'); expect(dockerfile).toContain('CMD ["bun", "server.js"]'); });