From 008ba44df311dcdc99f5827738ad9c5bd67d335f Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 8 Jul 2026 02:39:43 +1000 Subject: [PATCH 1/2] ci: containerize webapp and deploy to Oracle/Dokploy via GHCR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the mploy webapp off its current host to a Docker container on the Oracle Cloud VM, deployed by Dokploy from an image built in GitHub Actions and pushed to GHCR. Mirrors the monmap setup, adapted to this repo's stack (Next.js 16 + npm, single-package frontend/, not a pnpm workspace). - frontend/Dockerfile: multi-stage (deps → build → runner), node:22-slim, non-root nextjs user, runs the Next.js standalone server. No build-args — the app has no NEXT_PUBLIC_* vars and `next build` doesn't touch MongoDB, so all secrets are runtime env in Dokploy. - frontend/next.config.ts: pin outputFileTracingRoot to the app dir so the standalone entrypoint lands at .next/standalone/server.js; keep serverExternalPackages. - frontend/.dockerignore: keep the build context lean/deterministic. - .github/workflows/deploy.yml: build linux/arm64 on ubuntu-24.04-arm, push ghcr.io/monashcoding/mploy:latest + sha tag with gha cache, then POST the Dokploy webhook (self-skips until DOKPLOY_DEPLOY_WEBHOOK is set). Triggers on push to production. - docs/deploy.md: Dokploy runbook (Docker provider, runtime env, port 3000, jobs.monashcoding.com, Cloudflare grey-cloud-first for the cert). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/deploy.yml | 77 ++++++++++++++++++++++++++++ docs/deploy.md | 99 ++++++++++++++++++++++++++++++++++++ frontend/.dockerignore | 14 +++++ frontend/Dockerfile | 77 ++++++++++++++-------------- frontend/next.config.ts | 8 +++ 5 files changed, 237 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 docs/deploy.md create mode 100644 frontend/.dockerignore diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..9d20cbc6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,77 @@ +name: Build & publish image + +# Build the mploy webapp Docker image OFF the Oracle box (GitHub's runners do +# the heavy build) and push it to GHCR. Dokploy then just *pulls* the prebuilt +# image and runs it — so several apps can share one Oracle box without their +# builds fighting over RAM/CPU. See docs/deploy.md. + +on: + push: + branches: [production] + # Only rebuild when something that affects the image changes. + paths: + - "frontend/**" + - ".github/workflows/deploy.yml" + workflow_dispatch: {} # allow manual "Run workflow" + +env: + # NOTE: not ${{ github.repository }} — that's monashcoding/mploy-app. The + # published image name is deliberately just "mploy". + IMAGE: ghcr.io/monashcoding/mploy + +jobs: + build: + # Native ARM64 runner — Oracle Ampere is arm64, and this is free on public + # repos. Avoids ~5x slower QEMU cross-compilation. + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write # push to GHCR + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata (tags) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest + type=sha,format=long + + - name: Build & push (linux/arm64) + uses: docker/build-push-action@v6 + with: + # Build context is frontend/ — the webapp is a self-contained npm app + # (not a workspace), so npm ci resolves from frontend/package-lock.json. + context: ./frontend + file: ./frontend/Dockerfile + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # No build-args: mploy has no NEXT_PUBLIC_* build-time vars, and + # `next build` doesn't touch the DB. All secrets are runtime env in + # Dokploy. + + # Tell Dokploy to pull the new image and redeploy. Create the app in + # Dokploy with provider "Docker" pointing at ghcr.io/monashcoding/mploy:latest, + # then copy its deploy webhook URL into the repo secret + # DOKPLOY_DEPLOY_WEBHOOK. Skipped automatically until that secret exists. + - name: Trigger Dokploy redeploy + env: + # Secrets can't be used directly in `if:`, so hoist into env first. + DOKPLOY_DEPLOY_WEBHOOK: ${{ secrets.DOKPLOY_DEPLOY_WEBHOOK }} + if: ${{ env.DOKPLOY_DEPLOY_WEBHOOK != '' }} + run: curl -fsSL -X POST "$DOKPLOY_DEPLOY_WEBHOOK" diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 00000000..6d39cecb --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,99 @@ +# Deploying mploy (Oracle Cloud + Dokploy) + +mploy (the MAC Jobs Board webapp) runs as a Docker container on an Oracle +Cloud VM, fronted by [Dokploy](https://dokploy.com). This mirrors the setup +used for our sibling repo `monmap`. + +## Architecture: build off-box, run on-box + +The Oracle VM is shared by several apps (monmap, mploy, monashcoding). A +Docker build peaks at 2–4 GB RAM and pegs the CPU; several racing on one box +is how you OOM production. So **we never build on the Oracle box**: + +``` +push to production ──▶ GitHub Actions (ubuntu-24.04-arm) + │ builds linux/arm64 image + ▼ + GHCR: ghcr.io/monashcoding/mploy:latest + │ Dokploy pulls (webhook-triggered) + ▼ + Oracle VM: `node server.js` (~200–400 MB idle) +``` + +The box only ever runs the finished containers. + +## This app's shape (why the config looks the way it does) + +- The webapp is `frontend/` — a **self-contained npm app** (not a pnpm + workspace), so the Docker **build context is `frontend/`** and deps install + with `npm ci` from `frontend/package-lock.json`. The Spring Boot `backend/` + is a separate service, not part of this image. +- Next.js is built with `output: "standalone"`; the runtime just runs + `node server.js`. `outputFileTracingRoot` is pinned to `frontend/` so the + standalone entrypoint always lands at `.next/standalone/server.js`. +- **No build-time vars.** There are no `NEXT_PUBLIC_*` values (the Google + Analytics id is hardcoded), and `next build` does **not** touch MongoDB — + every DB-backed route is `force-dynamic` or reads `searchParams`, so nothing + is prerendered against the database. Everything below is therefore a + **runtime** env var set in Dokploy; nothing is baked into the image. + +## One-time GitHub setup + +1. **Repo Secret** (Settings → Secrets and variables → Actions → _Secrets_): + - `DOKPLOY_DEPLOY_WEBHOOK` = the deploy webhook URL Dokploy generates for + the app (added after the Dokploy step below). Until it exists, the + workflow builds/pushes the image but skips the redeploy trigger. +2. **Make the GHCR package public** (or give Dokploy a read token) so the VM + can pull without auth: after the first push, open the package at + `github.com/orgs/monashcoding/packages` → Package settings → change + visibility to Public. + +There are no repo _Variables_ to set — the build takes no build-args. + +## One-time Dokploy setup + +1. **Create Application** → Provider: **Docker**. + - Image: `ghcr.io/monashcoding/mploy:latest` + - (If you kept the package private: add GHCR registry credentials — a + GitHub PAT with `read:packages`.) +2. **Environment** (runtime vars): + ``` + MONGODB_URI=mongodb+srv://:@/ + MONGODB_DATABASE=default + NEXTAUTH_SECRET= + NEXTAUTH_URL=https://jobs.monashcoding.com + GOOGLE_CLIENT_ID= + GOOGLE_CLIENT_SECRET= + NOTION_API_KEY= + NOTION_DATABASE_ID= + ``` + MongoDB is hosted externally (Atlas), so `MONGODB_URI` is a normal + `mongodb+srv://` connection string — no on-box private-IP caveat. Make sure + the Atlas cluster's IP access list allows the Oracle VM's egress IP. +3. **Port**: container listens on `3000`. +4. **Domains**: add `jobs.monashcoding.com` → container port `3000` → enable + HTTPS (Let's Encrypt). + - DNS is on Cloudflare. **Grey-cloud (DNS-only)** the record first so + Dokploy/Traefik can complete the Let's Encrypt HTTP-01 challenge and + issue the cert, then switch it back to **orange-cloud (proxied)** + afterwards. +5. **Deploy webhook**: copy the app's deploy webhook URL into the GitHub repo + secret `DOKPLOY_DEPLOY_WEBHOOK` (GitHub setup step 1) so each pushed image + auto-redeploys. + +## Deploying a change + +Push to `production`. GitHub Actions builds + pushes the image, then hits the +Dokploy webhook, which pulls and restarts the container. Watch the run under +the repo's Actions tab; watch the pull/restart in Dokploy. + +To deploy manually: Actions → **Build & publish image** → _Run workflow_, then +hit **Deploy** in Dokploy. + +## Notes + +- `next build` does **not** touch MongoDB, so `MONGODB_URI` is a runtime-only + var. There are no build-args at all. +- The container runs as a non-root user (`nextjs`, uid 1001). +- The image is `linux/arm64` only — it runs on the Ampere A1 box and won't run + on an x86 host without emulation. diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..a5ad00af --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,14 @@ +# Build context = frontend/. Keep the image lean and deterministic: never ship +# local deps/build output/secrets into the build. +node_modules +.next +npm-debug.log* +.env +.env.* +.git +.gitignore +Dockerfile +.dockerignore +README.md +tsconfig.tsbuildinfo +.DS_Store diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 890ef68d..1f4e07ae 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,52 +1,53 @@ -# ------------------------- -# Base -# ------------------------- -FROM node:20-alpine AS base +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# mploy webapp — self-hosted image (Oracle Cloud + Dokploy). +# +# Build context is frontend/ (this directory). mploy's frontend is a +# self-contained npm app — NOT a pnpm workspace — so npm ci resolves +# everything from frontend/package-lock.json. The Spring Boot backend is a +# separate service and is not part of this image. +# +# There are NO build-time vars: the app has no NEXT_PUBLIC_* values (the GA id +# is hardcoded), and `next build` does NOT touch MongoDB (every DB route is +# force-dynamic or reads searchParams — nothing is prerendered against the DB). +# So everything secret (MONGODB_URI, NEXTAUTH_SECRET, OAuth creds, Notion keys) +# is a *runtime* env var, set in Dokploy — never baked into the image. +# --------------------------------------------------------------------------- + +FROM node:22-slim AS base WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 -# ------------------------- -# Dependencies -# ------------------------- +# ---- deps --------------------------------------------------------------- FROM base AS deps -COPY package.json package-lock.json* ./ +COPY package.json package-lock.json ./ RUN npm ci -# ------------------------- -# Builder -# ------------------------- -FROM base AS builder +# ---- build -------------------------------------------------------------- +FROM base AS build COPY --from=deps /app/node_modules ./node_modules -COPY package.json package-lock.json* ./ -COPY tsconfig.json next.config.ts ./ -COPY postcss.config.mjs tailwind.config.ts ./ -COPY public ./public -COPY src ./src +COPY . . RUN npm run build -# ------------------------- -# Runner -# ------------------------- -FROM base AS runner +# ---- runtime ------------------------------------------------------------ +FROM node:22-slim AS runner WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 -# Default: prod, can override with `-e NODE_ENV=development` -ENV NODE_ENV=production +# Run as a non-root user. +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs -# Copy runtime deps -COPY --from=deps /app/node_modules ./node_modules - -# Copy built assets -COPY --from=builder --chown=1001:1001 /app/.next/standalone ./ -COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static -COPY --from=builder --chown=1001:1001 /app/public ./public +# `output: "standalone"` bundles server.js + its traced node_modules. Static +# assets and public/ aren't traced, so copy them alongside. +COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=build --chown=nextjs:nodejs /app/public ./public -# Create non-root user -RUN addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 nextjs USER nextjs - EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -CMD ["npm", "start"] +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 68a6c64d..23387108 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from "next"; +import path from "path"; const nextConfig: NextConfig = { + // Emit a self-contained server bundle for the Docker runtime image + // (.next/standalone/server.js). See ../docs/deploy.md. output: "standalone", + // Pin file tracing to this app dir. Without it, the empty root + // package-lock.json makes Next infer the repo root as the tracing root and + // nest the standalone output under frontend/, breaking the Dockerfile CMD. + outputFileTracingRoot: path.join(__dirname), + serverExternalPackages: ["mongodb", "pino", "pino-pretty"], }; export default nextConfig; From e71d1904f3c7994b51dd5d71e6e2cf20760a2e01 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 8 Jul 2026 02:39:43 +1000 Subject: [PATCH 2/2] ci: containerize webapp and deploy to Oracle/Dokploy via GHCR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the mploy webapp off its current host to a Docker container on the Oracle Cloud VM, deployed by Dokploy from an image built in GitHub Actions and pushed to GHCR. Mirrors the monmap setup, adapted to this repo's stack (Next.js 16 + npm, single-package frontend/, not a pnpm workspace). - frontend/Dockerfile: multi-stage (deps → build → runner), node:22-slim, non-root nextjs user, runs the Next.js standalone server. No build-args — the app has no NEXT_PUBLIC_* vars and `next build` doesn't touch MongoDB, so all secrets are runtime env in Dokploy. - frontend/next.config.ts: pin outputFileTracingRoot to the app dir so the standalone entrypoint lands at .next/standalone/server.js; keep serverExternalPackages. - frontend/.dockerignore: keep the build context lean/deterministic. - .github/workflows/deploy.yml: build linux/arm64 on ubuntu-24.04-arm, push ghcr.io/monashcoding/mploy:latest + sha tag with gha cache, then POST the Dokploy webhook (self-skips until DOKPLOY_DEPLOY_WEBHOOK is set). Triggers on push to production. - docs/deploy.md: Dokploy runbook (Docker provider, runtime env, port 3000, jobs.monashcoding.com, Cloudflare grey-cloud-first for the cert). Co-Authored-By: Claude Opus 4.8 --- .github/dependabot.yml | 2 +- .github/workflows/deploy.yml | 77 +++++++++++++++++++++++ .github/workflows/lint-checker.yml | 10 +-- docs/deploy.md | 99 ++++++++++++++++++++++++++++++ frontend/.dockerignore | 14 +++++ frontend/Dockerfile | 77 +++++++++++------------ frontend/next.config.ts | 8 +++ frontend/package-lock.json | 1 - 8 files changed, 243 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 docs/deploy.md create mode 100644 frontend/.dockerignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4bf823eb..d89ad8bc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,4 +22,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "monthly" \ No newline at end of file + interval: "monthly" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..9d20cbc6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,77 @@ +name: Build & publish image + +# Build the mploy webapp Docker image OFF the Oracle box (GitHub's runners do +# the heavy build) and push it to GHCR. Dokploy then just *pulls* the prebuilt +# image and runs it — so several apps can share one Oracle box without their +# builds fighting over RAM/CPU. See docs/deploy.md. + +on: + push: + branches: [production] + # Only rebuild when something that affects the image changes. + paths: + - "frontend/**" + - ".github/workflows/deploy.yml" + workflow_dispatch: {} # allow manual "Run workflow" + +env: + # NOTE: not ${{ github.repository }} — that's monashcoding/mploy-app. The + # published image name is deliberately just "mploy". + IMAGE: ghcr.io/monashcoding/mploy + +jobs: + build: + # Native ARM64 runner — Oracle Ampere is arm64, and this is free on public + # repos. Avoids ~5x slower QEMU cross-compilation. + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write # push to GHCR + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata (tags) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest + type=sha,format=long + + - name: Build & push (linux/arm64) + uses: docker/build-push-action@v6 + with: + # Build context is frontend/ — the webapp is a self-contained npm app + # (not a workspace), so npm ci resolves from frontend/package-lock.json. + context: ./frontend + file: ./frontend/Dockerfile + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # No build-args: mploy has no NEXT_PUBLIC_* build-time vars, and + # `next build` doesn't touch the DB. All secrets are runtime env in + # Dokploy. + + # Tell Dokploy to pull the new image and redeploy. Create the app in + # Dokploy with provider "Docker" pointing at ghcr.io/monashcoding/mploy:latest, + # then copy its deploy webhook URL into the repo secret + # DOKPLOY_DEPLOY_WEBHOOK. Skipped automatically until that secret exists. + - name: Trigger Dokploy redeploy + env: + # Secrets can't be used directly in `if:`, so hoist into env first. + DOKPLOY_DEPLOY_WEBHOOK: ${{ secrets.DOKPLOY_DEPLOY_WEBHOOK }} + if: ${{ env.DOKPLOY_DEPLOY_WEBHOOK != '' }} + run: curl -fsSL -X POST "$DOKPLOY_DEPLOY_WEBHOOK" diff --git a/.github/workflows/lint-checker.yml b/.github/workflows/lint-checker.yml index 9ea83d6c..43e7199c 100644 --- a/.github/workflows/lint-checker.yml +++ b/.github/workflows/lint-checker.yml @@ -3,7 +3,7 @@ name: Frontend Lint Checker on: pull_request: paths: - - 'frontend/**' + - "frontend/**" jobs: verify: @@ -18,9 +18,9 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' - cache-dependency-path: './frontend/package-lock.json' + node-version: "20" + cache: "npm" + cache-dependency-path: "./frontend/package-lock.json" - name: Install dependencies run: npm ci @@ -35,4 +35,4 @@ jobs: run: npx prettier --check . - name: Build - run: npm run build \ No newline at end of file + run: npm run build diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 00000000..6d39cecb --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,99 @@ +# Deploying mploy (Oracle Cloud + Dokploy) + +mploy (the MAC Jobs Board webapp) runs as a Docker container on an Oracle +Cloud VM, fronted by [Dokploy](https://dokploy.com). This mirrors the setup +used for our sibling repo `monmap`. + +## Architecture: build off-box, run on-box + +The Oracle VM is shared by several apps (monmap, mploy, monashcoding). A +Docker build peaks at 2–4 GB RAM and pegs the CPU; several racing on one box +is how you OOM production. So **we never build on the Oracle box**: + +``` +push to production ──▶ GitHub Actions (ubuntu-24.04-arm) + │ builds linux/arm64 image + ▼ + GHCR: ghcr.io/monashcoding/mploy:latest + │ Dokploy pulls (webhook-triggered) + ▼ + Oracle VM: `node server.js` (~200–400 MB idle) +``` + +The box only ever runs the finished containers. + +## This app's shape (why the config looks the way it does) + +- The webapp is `frontend/` — a **self-contained npm app** (not a pnpm + workspace), so the Docker **build context is `frontend/`** and deps install + with `npm ci` from `frontend/package-lock.json`. The Spring Boot `backend/` + is a separate service, not part of this image. +- Next.js is built with `output: "standalone"`; the runtime just runs + `node server.js`. `outputFileTracingRoot` is pinned to `frontend/` so the + standalone entrypoint always lands at `.next/standalone/server.js`. +- **No build-time vars.** There are no `NEXT_PUBLIC_*` values (the Google + Analytics id is hardcoded), and `next build` does **not** touch MongoDB — + every DB-backed route is `force-dynamic` or reads `searchParams`, so nothing + is prerendered against the database. Everything below is therefore a + **runtime** env var set in Dokploy; nothing is baked into the image. + +## One-time GitHub setup + +1. **Repo Secret** (Settings → Secrets and variables → Actions → _Secrets_): + - `DOKPLOY_DEPLOY_WEBHOOK` = the deploy webhook URL Dokploy generates for + the app (added after the Dokploy step below). Until it exists, the + workflow builds/pushes the image but skips the redeploy trigger. +2. **Make the GHCR package public** (or give Dokploy a read token) so the VM + can pull without auth: after the first push, open the package at + `github.com/orgs/monashcoding/packages` → Package settings → change + visibility to Public. + +There are no repo _Variables_ to set — the build takes no build-args. + +## One-time Dokploy setup + +1. **Create Application** → Provider: **Docker**. + - Image: `ghcr.io/monashcoding/mploy:latest` + - (If you kept the package private: add GHCR registry credentials — a + GitHub PAT with `read:packages`.) +2. **Environment** (runtime vars): + ``` + MONGODB_URI=mongodb+srv://:@/ + MONGODB_DATABASE=default + NEXTAUTH_SECRET= + NEXTAUTH_URL=https://jobs.monashcoding.com + GOOGLE_CLIENT_ID= + GOOGLE_CLIENT_SECRET= + NOTION_API_KEY= + NOTION_DATABASE_ID= + ``` + MongoDB is hosted externally (Atlas), so `MONGODB_URI` is a normal + `mongodb+srv://` connection string — no on-box private-IP caveat. Make sure + the Atlas cluster's IP access list allows the Oracle VM's egress IP. +3. **Port**: container listens on `3000`. +4. **Domains**: add `jobs.monashcoding.com` → container port `3000` → enable + HTTPS (Let's Encrypt). + - DNS is on Cloudflare. **Grey-cloud (DNS-only)** the record first so + Dokploy/Traefik can complete the Let's Encrypt HTTP-01 challenge and + issue the cert, then switch it back to **orange-cloud (proxied)** + afterwards. +5. **Deploy webhook**: copy the app's deploy webhook URL into the GitHub repo + secret `DOKPLOY_DEPLOY_WEBHOOK` (GitHub setup step 1) so each pushed image + auto-redeploys. + +## Deploying a change + +Push to `production`. GitHub Actions builds + pushes the image, then hits the +Dokploy webhook, which pulls and restarts the container. Watch the run under +the repo's Actions tab; watch the pull/restart in Dokploy. + +To deploy manually: Actions → **Build & publish image** → _Run workflow_, then +hit **Deploy** in Dokploy. + +## Notes + +- `next build` does **not** touch MongoDB, so `MONGODB_URI` is a runtime-only + var. There are no build-args at all. +- The container runs as a non-root user (`nextjs`, uid 1001). +- The image is `linux/arm64` only — it runs on the Ampere A1 box and won't run + on an x86 host without emulation. diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..a5ad00af --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,14 @@ +# Build context = frontend/. Keep the image lean and deterministic: never ship +# local deps/build output/secrets into the build. +node_modules +.next +npm-debug.log* +.env +.env.* +.git +.gitignore +Dockerfile +.dockerignore +README.md +tsconfig.tsbuildinfo +.DS_Store diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 890ef68d..1f4e07ae 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,52 +1,53 @@ -# ------------------------- -# Base -# ------------------------- -FROM node:20-alpine AS base +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# mploy webapp — self-hosted image (Oracle Cloud + Dokploy). +# +# Build context is frontend/ (this directory). mploy's frontend is a +# self-contained npm app — NOT a pnpm workspace — so npm ci resolves +# everything from frontend/package-lock.json. The Spring Boot backend is a +# separate service and is not part of this image. +# +# There are NO build-time vars: the app has no NEXT_PUBLIC_* values (the GA id +# is hardcoded), and `next build` does NOT touch MongoDB (every DB route is +# force-dynamic or reads searchParams — nothing is prerendered against the DB). +# So everything secret (MONGODB_URI, NEXTAUTH_SECRET, OAuth creds, Notion keys) +# is a *runtime* env var, set in Dokploy — never baked into the image. +# --------------------------------------------------------------------------- + +FROM node:22-slim AS base WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 -# ------------------------- -# Dependencies -# ------------------------- +# ---- deps --------------------------------------------------------------- FROM base AS deps -COPY package.json package-lock.json* ./ +COPY package.json package-lock.json ./ RUN npm ci -# ------------------------- -# Builder -# ------------------------- -FROM base AS builder +# ---- build -------------------------------------------------------------- +FROM base AS build COPY --from=deps /app/node_modules ./node_modules -COPY package.json package-lock.json* ./ -COPY tsconfig.json next.config.ts ./ -COPY postcss.config.mjs tailwind.config.ts ./ -COPY public ./public -COPY src ./src +COPY . . RUN npm run build -# ------------------------- -# Runner -# ------------------------- -FROM base AS runner +# ---- runtime ------------------------------------------------------------ +FROM node:22-slim AS runner WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 -# Default: prod, can override with `-e NODE_ENV=development` -ENV NODE_ENV=production +# Run as a non-root user. +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs -# Copy runtime deps -COPY --from=deps /app/node_modules ./node_modules - -# Copy built assets -COPY --from=builder --chown=1001:1001 /app/.next/standalone ./ -COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static -COPY --from=builder --chown=1001:1001 /app/public ./public +# `output: "standalone"` bundles server.js + its traced node_modules. Static +# assets and public/ aren't traced, so copy them alongside. +COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=build --chown=nextjs:nodejs /app/public ./public -# Create non-root user -RUN addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 nextjs USER nextjs - EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -CMD ["npm", "start"] +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 68a6c64d..23387108 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from "next"; +import path from "path"; const nextConfig: NextConfig = { + // Emit a self-contained server bundle for the Docker runtime image + // (.next/standalone/server.js). See ../docs/deploy.md. output: "standalone", + // Pin file tracing to this app dir. Without it, the empty root + // package-lock.json makes Next infer the repo root as the tracing root and + // nest the standalone output under frontend/, breaking the Dockerfile CMD. + outputFileTracingRoot: path.join(__dirname), + serverExternalPackages: ["mongodb", "pino", "pino-pretty"], }; export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dbf255c7..24c5cc40 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,7 +20,6 @@ "isomorphic-dompurify": "^2.22.0", "jsdom": "^26.0.0", "lru-cache": "^11.2.2", - "mongodb": "^6.14.2", "next": "15.1.7", "pino": "^9.11.0",