feat(deploy): gör 0pt körbar utanför Vercel — standalone + bevisad Coolify-image #40
@@ -8,6 +8,25 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- `feat(deploy)`: appen är nu faktiskt byggbar och körbar utanför Vercel.
|
||||
`next.config.mjs` sätter `output: "standalone"`; Dockerfilen är omskriven
|
||||
till samma form som `sax3l/siax-io` (som redan körs självhostat på flottan):
|
||||
fyra steg, `node:22-alpine` (matchar CI:s nodversion, var 24), `libc6-compat`,
|
||||
icke-root, HEALTHCHECK mot `/api/health`, npm/npx borttaget ur runtime-imagen.
|
||||
Uppmätt: image 1,33 GB -> 423 MB. `--ignore-workspace` borttaget (dess
|
||||
motivering var inaktuell sedan P1-21) så containerinstallationen är samma
|
||||
kommando som CI kör; `--prod=false` tillagt eftersom Coolify sätter
|
||||
NODE_ENV=production vid byggtid och pnpm då hoppar över devDependencies.
|
||||
- `fix(images)`: `images.unoptimized: true` borttaget — det var v0-generatorns
|
||||
standardvärde, inte ett val (`next.config.mjs` var orörd sedan initial commit).
|
||||
Självhostad bildoptimering kräver att `sharp` faktiskt finns i runtime-imagen:
|
||||
`next build` spårar den inte in i `.next/standalone`, och
|
||||
`outputFileTracingIncludes` får Turbopack att panika på pnpms symlänkar. Sharp
|
||||
levereras därför i ett eget npm-steg i Dockerfile, pinnat till versionen i
|
||||
`pnpm-lock.yaml`. Verifierat i containern: `/_next/image` svarar 200 med
|
||||
`image/avif` (1,46 MB PNG -> 6,7 kB).
|
||||
- `chore(deploy)`: `poweredByHeader: false` (ingen `X-Powered-By: Next.js`).
|
||||
|
||||
- fix(security): security headers (CSP/HSTS/X-Frame-Options/etc.) + CI
|
||||
security gates actually enforce (Trivy `--exit-code 1`, gitleaks) instead
|
||||
of advisory-only. Actions pinned to commit SHA. SBOM + license inventory
|
||||
|
||||
+134
-44
@@ -1,71 +1,161 @@
|
||||
# =============================================================================
|
||||
# 0pt web console -- Dockerfile for Coolify (SIAX Cloud)
|
||||
# 0pt web console — Dockerfile för Coolify (SIAX Cloud)
|
||||
# =============================================================================
|
||||
# This repo's deployable service is the Next.js 16 app at the repo root
|
||||
# (package.json `start` == `next start`, liveness at GET /api/health). Unlike
|
||||
# the tsx backend services elsewhere in the fleet (aud0/n0d/api0), a Next app
|
||||
# needs a real build step, so this is a multi-stage build: install+build, then
|
||||
# a slim runtime that runs the built output as the non-root `node` user.
|
||||
# Referensimplementation: sax3l/siax-io, som redan körs självhostat på flottan
|
||||
# (Hetzner + Coolify + Traefik). Samma form återanvänds här i stället för att
|
||||
# uppfinna ett eget mönster: flerstegsbygge, node:22-alpine, libc6-compat för
|
||||
# sharp, icke-root, HEALTHCHECK, och npm/npx borttaget ur runtime-imagen.
|
||||
#
|
||||
# node:24-alpine matches the tag already proven in ../aud0 and ../api0. `wget`
|
||||
# is added to the runtime stage so Coolify's container healthcheck has a binary
|
||||
# to hit /api/health (bare node:*-alpine ships neither curl nor wget).
|
||||
# Den deployerbara tjänsten i det här repot är Next.js 16-appen i repo-roten.
|
||||
# Liveness: GET /api/health (svarar alltid 200 så länge processen lever).
|
||||
# Readiness: GET /api/ready (503 tills DATABASE_URL pekar på en nåbar Postgres)
|
||||
# — peka Coolifys readiness-probe dit, INTE HEALTHCHECK nedan.
|
||||
#
|
||||
# next.config.mjs does NOT set `output: "standalone"` (that would be a source
|
||||
# change, out of Wave-1 scope), so the runtime stage carries node_modules + the
|
||||
# .next build output and runs `next start` directly.
|
||||
# VAD SOM ÄNDRADES MOT FÖREGÅENDE VERSION AV DEN HÄR FILEN, och varför:
|
||||
#
|
||||
# 1. next.config.mjs sätter nu `output: "standalone"`. Runtime-steget bär
|
||||
# därför .next/standalone (server.js + spårade moduler) i stället för hela
|
||||
# node_modules. Mätt på det här repot: den gamla imagen var 1,33 GB på disk
|
||||
# / 315 MB komprimerat innehåll.
|
||||
# 2. node:24-alpine -> node:22-alpine. Båda CI-workflowsen (.gitea/ och
|
||||
# .github/workflows/ci.yml) kör `next build` på node 22; siax-io kör 22 i
|
||||
# produktion. Att bygga och köra på samma major som grinden testar tar bort
|
||||
# en version-skew utan uppsida.
|
||||
# 3. `--ignore-workspace` borttaget. Kommentaren som motiverade flaggan
|
||||
# ("pnpm-workspace.yaml ... carries only an `allowBuilds:` block and NO
|
||||
# `packages:` field") är inaktuell: filen har ett giltigt `packages:`-fält
|
||||
# och packages/schemas är en riktig workspace-medlem med egen package.json
|
||||
# sedan BACKLOG P1-21. Utan flaggan blir installationen här BIT FÖR BIT
|
||||
# samma kommando som CI kör (`pnpm install --frozen-lockfile`).
|
||||
# 4. `--prod=false` tillagt. Coolify injicerar NODE_ENV=production vid byggtid,
|
||||
# vilket får pnpm att hoppa över devDependencies (typescript, tailwindcss)
|
||||
# som `next build` kräver. Samma fallgrop dokumenterad i siax-io:s
|
||||
# Dockerfile för npm's `--include=dev`.
|
||||
# 5. Eget `sharp`-steg, se nedan.
|
||||
# 6. npm/npx strippat ur runtime-imagen (SIO-019-mönstret från siax-io).
|
||||
# =============================================================================
|
||||
|
||||
# ---- build stage ------------------------------------------------------------
|
||||
FROM node:24-alpine AS builder
|
||||
# ---- beroenden ---------------------------------------------------------------
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Enable corepack (pnpm@9 pinned below to match pnpm-lock.yaml lockfileVersion 9.0).
|
||||
RUN corepack enable
|
||||
# sharps förbyggda binär och libvips kräver libc-kompatibilitet på Alpine.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# Install deps against the frozen lockfile first for layer caching.
|
||||
# --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact that
|
||||
# carries only an `allowBuilds:` block and NO `packages:` field, which makes pnpm
|
||||
# abort with "packages field missing or empty". There are no real workspace
|
||||
# members (only the root package.json), so ignoring the workspace file is
|
||||
# semantically a no-op and lets the frozen install resolve the root deps cleanly.
|
||||
COPY package.json pnpm-lock.yaml .npmrc ./
|
||||
RUN corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile --ignore-workspace
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# Build the app. The lib/* clients (db, stripe, ...) are lazy + env-guarded, so
|
||||
# `next build` succeeds without any runtime secrets present.
|
||||
# --ignore-workspace again here: `COPY . .` reintroduces the v0 pnpm-workspace.yaml,
|
||||
# and `pnpm run` re-parses it (same "packages field missing" abort) unless ignored.
|
||||
COPY . .
|
||||
RUN pnpm --ignore-workspace run build
|
||||
# Manifesten först, för lagercache. pnpm-workspace.yaml OCH
|
||||
# packages/schemas/package.json måste med: pnpm-lock.yaml har två importers
|
||||
# (`.` och `packages/schemas`), och --frozen-lockfile vägrar om medlemmens
|
||||
# manifest saknas på disk.
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||
COPY packages/schemas/package.json ./packages/schemas/package.json
|
||||
|
||||
# ---- runtime stage ----------------------------------------------------------
|
||||
FROM node:24-alpine AS runtime
|
||||
# --prod=false: se punkt 4 ovan. Utan den dör `next build` på saknad typescript
|
||||
# så fort byggmiljön råkar ha NODE_ENV=production satt.
|
||||
RUN pnpm install --frozen-lockfile --prod=false
|
||||
|
||||
# ---- sharp (bildoptimering) --------------------------------------------------
|
||||
# Nexts bildoptimerare laddar `sharp` med en runtime-`require`, så `next build`
|
||||
# spårar den INTE in i .next/standalone. Verifierat i det här repot: ett bygge
|
||||
# med enbart `output: "standalone"` gav ett .next/standalone/node_modules som
|
||||
# bara innehöll next/, react/ och react-dom/.
|
||||
#
|
||||
# `outputFileTracingIncludes` går inte att använda som lösning: globben
|
||||
# "./node_modules/@img/**/*" får Turbopack att panika på pnpms katalogsymlänkar
|
||||
# i node_modules/.pnpm ("Is a directory (os error 21)") och bygget faller med
|
||||
# exit 1. Reproducerat, inte antaget.
|
||||
#
|
||||
# Därför ett eget steg med npm i stället för pnpm: npm lägger ett PLATT,
|
||||
# symlänkfritt node_modules, vilket är precis vad runtime-steget kan kopiera rakt
|
||||
# av. Versionen läses ur pnpm-lock.yaml så den aldrig kan glida ifrån vad resten
|
||||
# av bygget löste ut, och steget faller hårt om utläsningen inte ger något.
|
||||
# Eftersom det körs i en Alpine-image blir det musl-binärerna (@img/*-linuxmusl-*)
|
||||
# som hamnar där, inte värdmaskinens.
|
||||
FROM node:22-alpine AS sharp
|
||||
WORKDIR /sharp
|
||||
RUN apk add --no-cache libc6-compat
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN set -eu; \
|
||||
SHARP_VERSION="$(sed -n 's/^ sharp@\([0-9][^(:]*\).*/\1/p' pnpm-lock.yaml | sort -u | head -1)"; \
|
||||
if [ -z "$SHARP_VERSION" ]; then \
|
||||
echo "FEL: hittade ingen sharp-version i pnpm-lock.yaml — vägrar bygga en image vars bildoptimering tyst skulle vara trasig." >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "sharp pinnad från pnpm-lock.yaml: ${SHARP_VERSION}"; \
|
||||
npm install --no-save --no-audit --no-fund "sharp@${SHARP_VERSION}"; \
|
||||
node -e "require('sharp'); console.log('sharp laddar OK i byggsteget')"
|
||||
|
||||
# ---- bygge -------------------------------------------------------------------
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# Endast rot-projektets node_modules kopieras. packages/schemas har noll
|
||||
# beroenden (den paketerar bara JSON Schema-filer som konsumeras via sökväg),
|
||||
# så pnpm skapar aldrig något packages/schemas/node_modules att kopiera —
|
||||
# verifierat med `ls` efter en frozen install.
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
# Måste vara production. devDependencies behövs inte som NODE_ENV här — de ligger
|
||||
# redan i node_modules från deps-steget. NODE_ENV=development skulle dessutom
|
||||
# dra in Reacts utvecklingsruntime under prerendering (se siax-io:s Dockerfile,
|
||||
# där det är uppmätt åt båda hållen).
|
||||
ENV NODE_ENV=production
|
||||
# Bygget får inte kräva riktiga secrets: lib/db.ts och lib/stripe.ts är lata och
|
||||
# env-skyddade, och middleware.ts:s startgrind (assertAuthConfigSafe) klagar bara
|
||||
# om AUTH_ENFORCE uttryckligen sätts till "false" i produktion. Verifierat: hela
|
||||
# bygget går igenom utan en enda env-variabel satt.
|
||||
RUN pnpm run build
|
||||
|
||||
# ---- runtime -----------------------------------------------------------------
|
||||
FROM node:22-alpine AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# `apk upgrade` hämtar Alpines senaste patchade paket (t.ex. libssl3/libcrypto3 —
|
||||
# bastaggen släpar efter uppströms säkerhetsfixar) utan att röra den pinnade
|
||||
# Node-versionen. wget behövs för HEALTHCHECK: bara node:*-alpine har varken
|
||||
# curl eller wget.
|
||||
RUN apk add --no-cache libc6-compat wget && apk upgrade --no-cache
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
RUN apk add --no-cache wget
|
||||
# standalone-utdatan kopierar varken public/ eller .next/static — de måste med
|
||||
# explicit, annars serveras appen utan statiska assets.
|
||||
COPY --from=build --chown=node:node /app/public ./public
|
||||
COPY --from=build --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=build --chown=node:node /app/.next/static ./.next/static
|
||||
|
||||
# Built artifacts + resolved dependencies from the build stage. .next is chowned
|
||||
# so `next start` can write its runtime cache as the non-root user.
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=node:node /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/next.config.mjs ./next.config.mjs
|
||||
# sharp läggs BREDVID standalone-utdatans egna node_modules (next/react/react-dom).
|
||||
# Docker slår ihop katalogerna, så inget skrivs över. `require('sharp')` från
|
||||
# next/dist/... löses ut genom att vandra uppåt till /app/node_modules/sharp.
|
||||
COPY --from=sharp --chown=node:node /sharp/node_modules ./node_modules
|
||||
|
||||
# Bas-imagen node:22-alpine skeppar npms egen globala installation
|
||||
# (/usr/local/lib/node_modules/npm) med ett eget beroendeträd som regelbundet
|
||||
# bär kända HIGH/CRITICAL-CVE:er, helt utan koppling till den här appens kod.
|
||||
# Runtime-steget kör `node server.js` — varken RUN, CMD eller HEALTHCHECK här
|
||||
# anropar npm eller npx — så trädet kan tas bort i stället för att accepteras.
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx /usr/local/share/man/man1/npm*
|
||||
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
# LIVENESS. /api/health svarar 200 så länge processen kan svara — rätt signal för
|
||||
# "starta om containern". Peka INTE hit en readiness-probe: /api/ready svarar 503
|
||||
# tills DATABASE_URL är satt och Postgres är nåbar, och en HEALTHCHECK mot den
|
||||
# skulle få Coolify att döda en container som fungerar precis som avsett.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=25s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:3000/api/health >/dev/null 2>&1 || exit 1
|
||||
|
||||
# Invoke the next binary directly (pnpm/corepack are not set up in this stage).
|
||||
CMD ["node_modules/.bin/next", "start"]
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
+51
-1
@@ -1,10 +1,60 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Självhostad drift i SIAX Cloud (Coolify + Traefik) — ersätter Vercel.
|
||||
//
|
||||
// "standalone" får `next build` att packa en fristående Node-server i
|
||||
// .next/standalone (server.js + endast de spårade modulerna). Runtime-imagen
|
||||
// behöver då varken pnpm, corepack eller ett fullt node_modules-träd: den kör
|
||||
// `node server.js`. Det är det som gör att Dockerfilens runtime-steg kan
|
||||
// sluta släpa med npm/npx och hela dev-beroendeträdet.
|
||||
//
|
||||
// Inställningen är ofarlig på Vercel — plattformen ignorerar den — så den kan
|
||||
// landa före DNS-cutovern utan att röra den nuvarande deployen. Samma mönster
|
||||
// som sax3l/siax-io, som redan körs självhostat på flottan.
|
||||
output: "standalone",
|
||||
|
||||
// Skicka inte `X-Powered-By: Next.js`. Ingen funktionell skillnad, tar bara
|
||||
// bort en gratis versionsupplysning.
|
||||
poweredByHeader: false,
|
||||
|
||||
// OFÖRÄNDRAD. Pre-existerande skuld i den v0-genererade app/ + components/-
|
||||
// koden (se package.json's "//scripts"-not och `typecheck:app`). Den
|
||||
// BLOCKERANDE grinden är `pnpm run typecheck` över src/ + tests/, som inte
|
||||
// rörs av den här ändringen. Att slå på flaggan är sitt eget arbete.
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
images: {
|
||||
unoptimized: true,
|
||||
// `unoptimized: true` togs bort. Det var v0-generatorns standardvärde —
|
||||
// next.config.mjs hade inte ändrats sedan "Initial commit from v0"
|
||||
// (`git log -- next.config.mjs`) — alltså varken ett medvetet val eller en
|
||||
// workaround för Vercels bildloader. Självhostat är rätt lösning att låta
|
||||
// Next optimera själv.
|
||||
//
|
||||
// FÖRUTSÄTTNING, och den är inte gratis: Nexts bildoptimerare laddar
|
||||
// `sharp` med en runtime-`require`, inte ett statiskt import. `next build`
|
||||
// spårar därför INTE sharp in i .next/standalone — VERIFIERAT: ett bygge
|
||||
// med enbart `output: "standalone"` gav ett .next/standalone/node_modules
|
||||
// som bara innehöll next/, react/ och react-dom/. En container byggd så
|
||||
// svarar fel på varje /_next/image-begäran trots att sharp finns i
|
||||
// pnpm-lock.yaml (som optionalDependency till next@16.3.3).
|
||||
//
|
||||
// `outputFileTracingIncludes` är INTE lösningen här: globben
|
||||
// "./node_modules/@img/**/*" får Turbopack att panika
|
||||
// ("reading file ... @img/sharp-libvips-linux-x64 — Is a directory") när den
|
||||
// träffar pnpms katalogsymlänkar inne i node_modules/.pnpm. Reproducerat i
|
||||
// det här arbetet; bygget avslutades med exit 1.
|
||||
//
|
||||
// Sharp levereras i stället som ett eget, symlänkfritt npm-steg i
|
||||
// Dockerfile (`FROM node:22-alpine AS sharp`), pinnat till exakt den version
|
||||
// pnpm-lock.yaml redan löst ut. Se Dockerfilens kommentar.
|
||||
//
|
||||
// Ingen komponent renderar en fjärrbild i dag (`next/image` importeras på
|
||||
// exakt ett ställe, components/fade-image.tsx; alla bilder ligger i
|
||||
// public/), så ingen `remotePatterns` behövs ännu. Lägg till en sådan —
|
||||
// aldrig den föråldrade `domains` — den dagen en extern bildvärd tillkommer.
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user