# ============================================================================= # 0pt web console — Dockerfile för Coolify (SIAX Cloud) # ============================================================================= # 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. # # 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. # # 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). # ============================================================================= # ---- beroenden --------------------------------------------------------------- FROM node:22-alpine AS deps WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 # sharps förbyggda binär och libvips kräver libc-kompatibilitet på Alpine. RUN apk add --no-cache libc6-compat # corepack-fetch-flaken (server6-byggen, ECONNRESET mot registry.npmjs.org): # retry-loop 8 försök à 15 s — samma mönster som c0nf0rm #56 / k0b. RUN corepack enable && for i in 1 2 3 4 5 6 7 8; do corepack prepare pnpm@9 --activate && break || { echo "corepack retry $i/8"; sleep 15; }; done # 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 # --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 # corepack-fetch-flaken (server6-byggen, ECONNRESET mot registry.npmjs.org): # retry-loop 8 försök à 15 s — samma mönster som c0nf0rm #56 / k0b. RUN corepack enable && for i in 1 2 3 4 5 6 7 8; do corepack prepare pnpm@9 --activate && break || { echo "corepack retry $i/8"; sleep 15; }; done # 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 PORT=3000 ENV HOSTNAME=0.0.0.0 # 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 # 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 # 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 CMD ["node", "server.js"]