From 49327cf9ae5d280a67f751282acebd69af31a542 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 07:10:41 +0100 Subject: [PATCH] fix(docker): switch alpine to node:22-slim for pre-built better-sqlite3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alpine uses musl libc; better-sqlite3 has no pre-built musl binaries so it always compiles from source (installs 300 MB of gcc/g++/python3, takes 3-5 min). node:22-slim (Debian) has glibc so prebuild-install downloads a pre-built binary instead — build stays under 1 minute. Changes: - Both stages: node:22-alpine -> node:22-slim - deps stage: remove apk/build-tool installation (not needed) - runtime stage: remove apk libstdc++ install (present in debian-slim) - HEALTHCHECK: wget -> node built-in http (wget absent from debian-slim) - docker-compose.yaml: same healthcheck fix --- Dockerfile | 26 +++++++++++++------------- docker-compose.yaml | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7db0078..23f36a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,24 @@ # --------------------------------------------------------------------------- # Stage 1 — deps: install production dependencies only # --------------------------------------------------------------------------- -FROM node:22-alpine AS deps +# node:22-slim (Debian slim) is used instead of Alpine so that +# better-sqlite3 can use its pre-built glibc binaries rather than +# compiling from source (Alpine uses musl libc — no pre-builds exist). +# This keeps build times fast (~1 min vs ~5 min for source compilation). +FROM node:22-slim AS deps WORKDIR /app # Copy manifests and install production deps only (no devDependencies). -# build-base provides the C++ toolchain needed to compile better-sqlite3's -# native addon. It stays in the deps stage and is NOT copied to runtime. +# prebuild-install will download the pre-built better-sqlite3 binary for +# linux-x64-glibc, so no C++ toolchain is needed here. COPY package.json package-lock.json ./ -RUN apk add --no-cache python3 make g++ && \ - npm ci --omit=dev +RUN npm ci --omit=dev # --------------------------------------------------------------------------- # Stage 2 — runtime image (minimal attack surface) # --------------------------------------------------------------------------- -FROM node:22-alpine AS runtime +FROM node:22-slim AS runtime LABEL org.opencontainers.image.title="sofarr" LABEL org.opencontainers.image.description="Personal media download dashboard for *arr services" @@ -30,11 +33,7 @@ LABEL custom.hardware.requirement="None - runs on any Docker-supported platform # The /app directory is owned by root; data directory is owned by node WORKDIR /app -# libstdc++ is required at runtime to load the better-sqlite3 native addon. -# The build tools (g++, make, python3) remain in the deps stage only. -RUN apk add --no-cache libstdc++ - -# Copy production deps from deps stage +# Copy production deps from deps stage (includes pre-built better-sqlite3) COPY --from=deps /app/node_modules ./node_modules # Copy application source owned by root (read-only at runtime) @@ -53,8 +52,9 @@ USER node EXPOSE 3001 -# HEALTHCHECK — Docker will restart the container if this fails 3 times +# HEALTHCHECK — Docker will restart the container if this fails 3 times. +# Uses node's built-in http module (no wget/curl needed in slim image). HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget -qO- http://localhost:3001/health || exit 1 + CMD node -e "require('http').get('http://localhost:3001/health',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))" CMD ["node", "server/index.js"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 145b885..e6ed6ce 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,7 +35,7 @@ services: - ALL # drop all Linux capabilities cap_add: [] # add back none — Node.js needs no special caps healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3001/health"] + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"] interval: 30s timeout: 5s retries: 3