diff --git a/Dockerfile b/Dockerfile index b4a4b91..b2235c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,70 +1,59 @@ -# ================================ -# Stage 1: Build stage -# ================================ -FROM node:22-alpine AS builder +FROM node:22-slim -WORKDIR /build - -# Install build dependencies needed for native modules (better-sqlite3) -RUN apk add --no-cache python3 make g++ - -# Copy package files first for better layer caching -COPY package.json yarn.lock ./ - -# Install all dependencies (including devDependencies for building) -RUN yarn config set network-timeout 600000 \ - && yarn --frozen-lockfile - -# Copy source files needed for build -COPY index.html vite.config.js ./ -COPY ui ./ui -COPY lib ./lib - -# Build frontend assets -RUN yarn build:frontend - -# ================================ -# Stage 2: Production stage -# ================================ -FROM node:22-alpine +# System deps for Chrome for Testing + build tools for native modules (better-sqlite3) +# Must run as root +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates fonts-liberation libasound2 \ + libatk-bridge2.0-0 libatk1.0-0 libcups2 libdbus-1-3 \ + libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 \ + libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 xdg-utils \ + python3 make g++ \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /db /conf /fredy \ + && chown node:node /db /conf /fredy WORKDIR /fredy -# Install Chromium and curl (for healthcheck) -# Using Alpine's chromium package which is much smaller -RUN apk add --no-cache chromium curl +# Everything from here runs as the built-in non-root node user (UID 1000) +USER node ENV NODE_ENV=production \ - IS_DOCKER=true \ - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ - PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + IS_DOCKER=true -# Install build dependencies for native modules, then remove them after yarn install -COPY package.json yarn.lock ./ +COPY --chown=node:node package.json yarn.lock ./ -RUN apk add --no-cache --virtual .build-deps python3 make g++ \ - && yarn config set network-timeout 600000 \ - && yarn --frozen-lockfile --production \ - && yarn cache clean \ - && apk del .build-deps +# Install dependencies and purge build tools (only needed to compile better-sqlite3) +RUN yarn config set network-timeout 600000 \ + && yarn --frozen-lockfile \ + && yarn cache clean -# Copy built frontend from builder stage -COPY --from=builder /build/ui/public ./ui/public +# Install Chrome for Testing in a separate layer — it's ~150MB and rarely changes, +# so keeping it separate avoids re-downloading on every code/dependency change +RUN npx puppeteer browsers install chrome -# Copy application source (only what's needed at runtime) -COPY index.js ./ -COPY index.html ./ -COPY lib ./lib +# Purge build tools now that native modules are compiled +USER root +RUN apt-get purge -y python3 make g++ \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* +USER node -# Prepare runtime directories and symlinks for data and config -RUN mkdir -p /db /conf \ - && chown 1000:1000 /db /conf \ - && chmod 777 /db /conf \ - && ln -s /db /fredy/db \ +COPY --chown=node:node index.html vite.config.js ./ +COPY --chown=node:node ui ./ui +COPY --chown=node:node lib ./lib + +RUN yarn build:frontend + +COPY --chown=node:node index.js ./ + +RUN ln -s /db /fredy/db \ && ln -s /conf /fredy/conf EXPOSE 9998 VOLUME /db VOLUME /conf +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:9998/ || exit 1 + CMD ["node", "index.js"] diff --git a/docker-test.sh b/docker-test.sh index a293f37..6ca4aa7 100755 --- a/docker-test.sh +++ b/docker-test.sh @@ -7,12 +7,63 @@ if [ "$(docker ps -aq -f name=fredy)" ]; then docker rm fredy || true fi +# On Apple Silicon, force linux/amd64 to match production CI and avoid arm64/x86_64 +# Chrome mismatch under Rosetta. On native Linux (amd64 or arm64) let Docker pick naturally. That took me fucking 1 hour to figure out. +PLATFORM="" +if [ "$(uname -m)" = "arm64" ] && [ "$(uname -s)" = "Darwin" ]; then + PLATFORM="linux/amd64" +fi + # Build image from local Dockerfile, forcing a fresh build without cache -docker build --no-cache -t fredy:local . +if [ -n "$PLATFORM" ]; then + docker build --no-cache --platform "$PLATFORM" -t fredy:local . +else + docker build --no-cache -t fredy:local . +fi # Run container with volumes and port mapping -docker run -d --name fredy \ - -v fredy_conf:/conf \ - -v fredy_db:/db \ - -p 9998:9998 \ - fredy:local \ No newline at end of file +if [ -n "$PLATFORM" ]; then + docker run -d --name fredy --platform "$PLATFORM" -v fredy_conf:/conf -v fredy_db:/db -p 9998:9998 fredy:local +else + docker run -d --name fredy -v fredy_conf:/conf -v fredy_db:/db -p 9998:9998 fredy:local +fi + +echo "Waiting for app to be ready..." +for i in $(seq 1 30); do + if docker exec fredy curl -sf http://localhost:9998/ > /dev/null 2>&1; then + echo "App is up" + break + fi + if [ "$i" = "30" ]; then + echo "App did not come up in time" + docker logs fredy + exit 1 + fi + sleep 2 +done + +# Verify the process is NOT running as root +RUNNING_USER=$(docker exec fredy id -u) +if [ "$RUNNING_USER" = "0" ]; then + echo "Process is running as root!" + exit 1 +fi +echo "Process runs as UID $RUNNING_USER (not root)" + +# Verify Chrome launches without crashing +echo "Testing Chrome..." +CHROME=$(docker exec fredy find /home/node/.cache/puppeteer -name chrome -type f 2>/dev/null | head -1) +if [ -z "$CHROME" ]; then + echo "Chrome binary not found" + exit 1 +fi +if docker exec fredy "$CHROME" --headless --no-sandbox --disable-gpu --dump-dom https://example.com 2>&1 | grep -q "&1 | head -20 + exit 1 +fi + +echo "" +echo "All checks passed." diff --git a/package.json b/package.json index 0cbe331..f526cd3 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fredy", - "version": "20.0.6", + "version": "20.0.7", "description": "[F]ind [R]eal [E]states [d]amn eas[y].", "scripts": { "prepare": "husky",