From 40bc80b50aafe2aebf52a9bc613d37b1165685f8 Mon Sep 17 00:00:00 2001 From: Zachary Smith Date: Wed, 21 Oct 2026 15:45:36 -0400 Subject: [PATCH 1/2] Add Dockerfile and Datum Cloud deploy pipeline - Add output: "standalone" to next.config.ts and a multi-stage Dockerfile (node:20-slim) -- verified end-to-end locally (build, run, headers/CSP still correct from the prior security-hardening work) - Add .github/workflows/deploy.yml: build+push to ghcr.io on push to main, then datumctl compute deploy to Datum Cloud Compute. NEXT_PUBLIC_API_URL is baked in at build time from the API_PUBLIC_URL repo variable, since Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time - Add DEPLOY.md documenting the one-time setup and what's still unverified Stacked on security-hardening (#1). Co-Authored-By: Claude Sonnet 5 --- .dockerignore | 6 +++ .github/workflows/deploy.yml | 92 ++++++++++++++++++++++++++++++++++++ DEPLOY.md | 52 ++++++++++++++++++++ Dockerfile | 24 ++++++++++ next.config.ts | 1 + 5 files changed, 175 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/deploy.yml create mode 100644 DEPLOY.md create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ada3b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.next +.git +.github +.env*.local +*.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..cb9bf8f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,92 @@ +name: Deploy + +# Builds the web image and deploys it to Datum Cloud Compute on every push to +# main. See DEPLOY.md for the one-time setup this depends on. +on: + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: deploy-web + cancel-in-progress: false + +env: + IMAGE: ghcr.io/altsmanager/web + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.image.outputs.image }} + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set image tag + id: image + run: echo "image=${IMAGE}:${{ github.sha }}" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + # NEXT_PUBLIC_API_URL is inlined into the client bundle at build time, so + # it has to be known here, not just at deploy time. It's set once, by + # hand, as the API_PUBLIC_URL repository variable, after the api + # workload's first deploy produces its public hostname (see DEPLOY.md) -- + # it shouldn't change afterwards, since that hostname is tied to the + # workload, not to any particular deploy. + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.image.outputs.image }} + build-args: | + NEXT_PUBLIC_API_URL=${{ vars.API_PUBLIC_URL }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + env: + DATUM_PROJECT: altsmanager + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Install datumctl + run: go install go.datum.net/datumctl@latest + + # Service-account login per https://www.datum.net/docs/datumctl/auth/logging-in. + # The service account must be scoped to exactly this one project -- + # datum-cloud/datumctl#324 is an open bug where --credentials login fails + # non-interactively if the account can see more than one context. + - name: Authenticate to Datum Cloud + env: + DATUM_SERVICE_ACCOUNT_KEY: ${{ secrets.DATUM_SERVICE_ACCOUNT_KEY }} + run: | + umask 077 + printf '%s' "$DATUM_SERVICE_ACCOUNT_KEY" > "$RUNNER_TEMP/datum-key.json" + datumctl login --credentials "$RUNNER_TEMP/datum-key.json" + rm -f "$RUNNER_TEMP/datum-key.json" + + # NOTE: unverified against a real deployment -- Compute access for this + # project is pending as of when this workflow was written (see DEPLOY.md). + - name: Deploy to Datum Compute + run: | + datumctl compute deploy altsmanager-web \ + --image="${{ needs.build-and-push.outputs.image }}" \ + --city=DFW \ + --min=1 \ + --http-port=3000 diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..ad2ea9e --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,52 @@ +# Deploying to Datum Cloud + +This repo deploys to [Datum Cloud](https://datum.net) Compute via +`.github/workflows/deploy.yml`, which runs on every push to `main`. See the +[api repo's DEPLOY.md](https://github.com/AltsManager/api/blob/main/DEPLOY.md) +for the shared infrastructure (Datum project, Compute access, service +account) -- this document only covers what's specific to the frontend. + +## Status + +- [ ] Everything in the api repo's DEPLOY.md status list, since this depends + on the same project and Compute access +- [ ] `altsmanager-api` deployed at least once, so its public hostname exists +- [ ] `API_PUBLIC_URL` repository variable set to that hostname + +## Why the build needs the API's URL upfront + +`NEXT_PUBLIC_API_URL` is inlined into the client-side JavaScript bundle at +`next build` time -- it can't be supplied later, at deploy or run time, the +way a server-side env var can. So the api workload has to be deployed first +(at least once) to get its public hostname, and only then can this repo's +image be built with that URL baked in. + +In steady state this isn't a problem: the api's public hostname is tied to +its Compute workload, not to any individual deploy, so it stays the same +across api releases. `API_PUBLIC_URL` only needs to be set once, not updated +on every deploy. + +## One-time setup + +1. Complete the api repo's setup through "First deploy", and note the + `altsmanager-api` workload's public hostname. +2. In this repo's GitHub settings, add a repository **variable** (not secret -- + it ends up in client-side JS, so it's already public) named + `API_PUBLIC_URL` set to that hostname, e.g. `https://altsmanager-api.<...>.datum.app`. +3. Add the same `DATUM_SERVICE_ACCOUNT_KEY` secret as the api repo (or a + separate service account scoped the same way -- either works, since both + deploy into the same `altsmanager` project). +4. Push to `main`, or run the `Deploy` workflow manually. + +## Verify once Compute access is granted + +Same open questions as the api repo's DEPLOY.md: whether repeated +`datumctl compute deploy` calls preserve prior settings, and the exact +non-interactive login behavior. + +## Required GitHub secrets/variables + +| Name | Type | Purpose | +|---|---|---| +| `DATUM_SERVICE_ACCOUNT_KEY` | secret | Service account credentials JSON for `datumctl login --credentials` | +| `API_PUBLIC_URL` | variable | The deployed api's public hostname, baked into the client bundle at build time | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..72c58c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-slim AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +RUN npm run build + +FROM node:20-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/public ./public +COPY --from=builder --chown=node:node /app/.next/standalone ./ +COPY --from=builder --chown=node:node /app/.next/static ./.next/static +USER node + +EXPOSE 3000 +ENV PORT=3000 HOSTNAME=0.0.0.0 +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index 0ba2c63..42f2f12 100644 --- a/next.config.ts +++ b/next.config.ts @@ -29,6 +29,7 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { + output: "standalone", async headers() { return [ { From 0dfe37828015be53797916c8a44b50e1cd82dfbb Mon Sep 17 00:00:00 2001 From: Zachary Smith Date: Thu, 22 Oct 2026 06:57:32 -0400 Subject: [PATCH 2/2] Bump Dockerfile base image from node:20 to node:22, track docker images in dependabot Node.js shipped a security release on 2026-10-21 covering the 22.x/24.x/26.x lines (CVE-2026-111497, a HIGH-severity Brotli decoder out-of-bounds write reachable via fetch()'s auto-decompression, among others) -- 20.x isn't in that list at all, since it reached end of life in April 2026 and gets no further patches. Next.js 16 only requires Node >=20.9 as a floor, so bumping to the 22.x LTS line (currently patched) is safe. Add a dependabot "docker" ecosystem entry so a stale/EOL base image doesn't go unnoticed like this again. Verified: image builds, container runs node v22.23.4 (the just-patched version), serves /login with the expected security headers intact. Co-Authored-By: Claude Sonnet 5 --- .github/dependabot.yml | 5 +++++ Dockerfile | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ba11d63..4757dea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,3 +17,8 @@ updates: directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" diff --git a/Dockerfile b/Dockerfile index 72c58c9..b491882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:20-slim AS deps +FROM node:22-slim AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci -FROM node:20-slim AS builder +FROM node:22-slim AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -11,7 +11,7 @@ ARG NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} RUN npm run build -FROM node:20-slim AS runner +FROM node:22-slim AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/public ./public