Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
43 changes: 34 additions & 9 deletions scripts/docker-ensure-db.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -33,31 +34,53 @@ 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");

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;
Expand All @@ -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
);
}
12 changes: 10 additions & 2 deletions scripts/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"