diff --git a/Dockerfile b/Dockerfile index 06030f0..f18f45f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,15 @@ RUN --mount=type=cache,target=/var/cache/apk \ apk add --update-cache --cache-dir /var/cache/apk python3 make g++ COPY package.json package-lock.json ./ +# sharp prebuilds need x86-64-v2 (SSE4.2+). Hosts like Intel Atom N2800 SIGILL +# in libvips. Install WASM addon explicitly (do NOT use `npm install --cpu=wasm32`, +# which prunes other musl natives e.g. lightningcss) then strip native @img binaries. RUN --mount=type=cache,target=/root/.npm \ - HUSKY=0 npm ci --prefer-offline --no-audit + HUSKY=0 npm ci --prefer-offline --no-audit \ + && npm install --no-save --no-audit --no-fund @img/sharp-wasm32@0.35.4 \ + && find node_modules/@img -mindepth 1 -maxdepth 1 -type d \( \ + -name 'sharp-linux*' -o -name 'sharp-libvips-*' \ + \) -exec rm -rf {} + # ── Étape 2 : build ─────────────────────────────────────────────────────────── FROM deps AS builder diff --git a/README.md b/README.md index dfe5144..a7a049e 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ This complements the SQLite backup (`hobbyhoops.db`): CSV is ideal for spreadshe The image starts with an **empty collection**: only a writable `data/` directory is required (the SQLite database is created there automatically). The container runs as the **`hobbyhoops`** system user (UID/GID **1111**). Mount `data/` at `/app/data` and make it writable by that user. +Card-photo processing uses **sharp via WebAssembly** in the Docker image (not the native libvips prebuild). Native sharp/libvips binaries require **x86-64-v2** (SSE4.2+) and crash with `SIGILL` / exit `132` on older CPUs such as Intel Atom N2800. + The application listens on **`127.0.0.1:3000`** (not exposed on all interfaces). In production, place a reverse proxy (e.g. Apache) in front of the host and proxy to that address. Two Compose files: diff --git a/docker-compose.yml b/docker-compose.yml index 5e600d3..72023b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,9 @@ # export HOBBYHOOPS_VERSION=2.0.0 # tag publié sur GHCR # docker compose pull && docker compose up -d # -# Au démarrage, l'entrypoint exécute la migration SQLite avant le serveur. -# Si la migration échoue, le conteneur quitte avec le code 1 (Compose échoue). +# Au démarrage, l'entrypoint vérifie AUTH_SECRET et que data/ est writable (UID 1111). +# Échec = exit 1 → boucle de restart (unless-stopped). Les migrations SQLite +# s'exécutent ensuite au premier accès DB (healthcheck / requête). # # Dev local (build) : docker compose -f docker-compose.dev.yml up --build diff --git a/next.config.ts b/next.config.ts index 386401f..0a05bdd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,7 +7,7 @@ const projectRoot = path.dirname(fileURLToPath(import.meta.url)); const nextConfig: NextConfig = { output: "standalone", poweredByHeader: false, - serverExternalPackages: ["better-sqlite3"], + serverExternalPackages: ["better-sqlite3", "sharp"], turbopack: { root: projectRoot, }, diff --git a/scripts/docker-ensure-db.mjs b/scripts/docker-ensure-db.mjs index 6578292..b1b807f 100644 --- a/scripts/docker-ensure-db.mjs +++ b/scripts/docker-ensure-db.mjs @@ -6,6 +6,7 @@ * Self-contained: only this file is copied into the production image (see Dockerfile). * Pino is resolved from /app/node_modules via the Next.js standalone output. */ +import fs from "node:fs"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path from "node:path"; @@ -33,6 +34,19 @@ const log = pino({ }, }).child({ scope: "docker-ensure-db" }); +/** Always print a plain line on stderr so Compose restart loops are diagnosable. */ +function abort(detail, error) { + const errMsg = error instanceof Error ? error.message : error ? String(error) : undefined; + const hint = errMsg ? `${detail} (${errMsg})` : detail; + console.error(`hobbyhoops: startup aborted — ${hint}`); + log.error({ + msg: "Startup aborted", + detail, + ...(error ? { err: error } : {}), + }); + process.exit(1); +} + const require = createRequire(import.meta.url); const instrumentationPath = path.join(appRoot, ".next/server/instrumentation.js"); @@ -40,24 +54,33 @@ let register; try { ({ register } = require(instrumentationPath)); } catch { - log.error({ - msg: "Startup aborted", - detail: "instrumentation module not found", - }); - process.exit(1); + abort("instrumentation module not found"); } try { await register(); } catch (error) { - log.error({ msg: "Startup aborted", err: error }); - process.exit(1); + abort( + "AUTH_SECRET missing or too short (min 32 chars) — set it in .env", + error + ); } const dbPath = path.resolve( appRoot, process.env.HOBBYHOOPS_DB_PATH?.trim() || "data/hobbyhoops.db" ); +const dataDir = path.dirname(dbPath); + +try { + fs.mkdirSync(dataDir, { recursive: true }); + fs.accessSync(dataDir, fs.constants.W_OK); +} catch (error) { + abort( + `${dataDir} is not writable by UID 1111 — on the host run: sudo chown -R 1111:1111 data`, + error + ); +} try { const Database = (await import("better-sqlite3")).default; @@ -66,6 +89,8 @@ try { db.prepare("SELECT 1").get(); db.close(); } catch (error) { - log.error({ msg: "Startup aborted", detail: "database unavailable", err: error }); - process.exit(1); + abort( + `cannot open SQLite at ${dbPath} — check ownership/permissions of data/ (UID 1111)`, + error + ); } diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index f39afe7..22ca40c 100644 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -1,6 +1,14 @@ #!/bin/sh set -eu -mkdir -p /app/data -node /app/scripts/docker-ensure-db.mjs +if ! mkdir -p /app/data; then + echo "hobbyhoops: cannot create /app/data — mount ./data and chown 1111:1111" >&2 + exit 1 +fi + +if ! node /app/scripts/docker-ensure-db.mjs; then + echo "hobbyhoops: preflight failed (see message above). Container will exit." >&2 + exit 1 +fi + exec "$@"