From 5da99d95315b59d2654979427183d831d7164b69 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Tue, 24 Feb 2026 16:21:41 -0600 Subject: [PATCH 1/4] fix: Docker security hardening and CI version alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove .env COPY from realtime, migrate, and seed Dockerfiles — env vars are provided at runtime via docker-compose env_file. Add .env to .dockerignore. Convert realtime Dockerfile to multi-stage build with USER node. Enable --frozen-lockfile in processor build. Replace no-op worker healthcheck with pgrep process check. Enable NEXT_TELEMETRY_DISABLED in web Dockerfile. Align CI to Node 22 and Postgres 17 matching production. Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 1 + .github/workflows/test.yml | 4 ++-- apps/processor/Dockerfile | 5 ++--- apps/realtime/Dockerfile | 35 +++++++++++++++++++++++------------ apps/web/Dockerfile | 6 ++---- apps/web/Dockerfile.migrate | 3 --- apps/web/Dockerfile.seed | 3 --- apps/web/Dockerfile.worker | 4 ++-- 8 files changed, 32 insertions(+), 29 deletions(-) diff --git a/.dockerignore b/.dockerignore index c287cb1a1..c2c262b67 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,6 +28,7 @@ dist/ !packages/**/dist/** # Environment files +.env .env.local .env.development.local .env.test.local diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f0bacb56..c8526e403 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: - NODE_VERSION: '20' + NODE_VERSION: '22' jobs: unit-tests: @@ -17,7 +17,7 @@ jobs: services: postgres: - image: postgres:16 + image: postgres:17-alpine env: POSTGRES_PASSWORD: postgres POSTGRES_DB: pagespace_test diff --git a/apps/processor/Dockerfile b/apps/processor/Dockerfile index 9bd2939fe..5d80f257f 100644 --- a/apps/processor/Dockerfile +++ b/apps/processor/Dockerfile @@ -27,9 +27,8 @@ COPY packages/db/package.json ./packages/db/ COPY packages/lib/package.json ./packages/lib/ COPY apps/processor/package.json ./apps/processor/ -# Install all dependencies -# Install dependencies (allow lockfile update since processor deps changed) -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --no-frozen-lockfile --prod=false +# Install dependencies +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod=false # Now copy source code AFTER dependencies are installed COPY tsconfig.json ./ diff --git a/apps/realtime/Dockerfile b/apps/realtime/Dockerfile index 5508699b1..8835b6e3f 100644 --- a/apps/realtime/Dockerfile +++ b/apps/realtime/Dockerfile @@ -1,40 +1,51 @@ # syntax=docker/dockerfile:1.6 -# This Dockerfile runs the realtime service. -# It uses the same pattern as the working migrate container. -FROM node:22.17.0-alpine +# Multi-stage build for the realtime Socket.IO service -# Set working directory +# Stage 1: Install dependencies +FROM node:22.17.0-alpine AS deps WORKDIR /app -# Install basic tools and configure npm RUN apk add --no-cache git && \ npm config set fetch-timeout 300000 && \ npm config set fetch-retry-maxtimeout 120000 && \ npm config set fetch-retry-mintimeout 10000 -# Enable corepack and prepare specific pnpm version RUN corepack enable && \ (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) -# Copy package files first for better Docker layer caching COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY packages/db/package.json ./packages/db/ COPY packages/lib/package.json ./packages/lib/ COPY apps/web/package.json ./apps/web/ COPY apps/realtime/package.json ./apps/realtime/ -# Install ALL dependencies for the entire monorepo RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod=false -# Copy only the source code needed (avoid copying node_modules) +# Stage 2: Build shared packages +FROM node:22.17.0-alpine AS builder +WORKDIR /app +RUN corepack enable && \ + (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) + +COPY --from=deps /app . + COPY packages ./packages COPY apps ./apps COPY tsconfig.json ./ -# Copy .env file for environment variables -COPY .env ./ +# Stage 3: Production runner +FROM node:22.17.0-alpine AS runner +WORKDIR /app + +RUN corepack enable && \ + (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) + +COPY --from=builder /app . + +ENV NODE_ENV=production + +USER node EXPOSE 3001 -# Run the realtime service with tsx CMD ["pnpm", "--filter", "realtime", "start"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 82bfe6791..7b57d4199 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -47,8 +47,7 @@ ENV NEXT_PUBLIC_GOOGLE_OAUTH_IOS_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_OAUTH_IOS_CLIENT_ # Next.js collects telemetry data by default. # Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry. -# ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 # We are installing devDependencies via the --prod=false flag, so this is not needed # ENV NODE_ENV=development @@ -65,8 +64,7 @@ FROM node:22.17.0-alpine AS runner WORKDIR /app ENV NODE_ENV=production -# Uncomment the following line in case you want to disable telemetry. -# ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 # Use the existing node user (UID 1000) from the base image # This matches the processor container's UID for shared volume access diff --git a/apps/web/Dockerfile.migrate b/apps/web/Dockerfile.migrate index d9ed623f2..5327b11ea 100644 --- a/apps/web/Dockerfile.migrate +++ b/apps/web/Dockerfile.migrate @@ -34,6 +34,3 @@ COPY tsconfig.json ./ # Build the database package to ensure latest schema changes are compiled RUN pnpm --filter @pagespace/db build - -# Copy .env file for tsx --env-file -COPY .env ./ diff --git a/apps/web/Dockerfile.seed b/apps/web/Dockerfile.seed index 3326025cf..fd5fd693a 100644 --- a/apps/web/Dockerfile.seed +++ b/apps/web/Dockerfile.seed @@ -26,8 +26,5 @@ COPY packages ./packages COPY apps ./apps COPY tsconfig.json ./ -# Copy .env file for tsx --env-file -COPY .env ./ - # Run seed CMD ["pnpm", "--filter", "@pagespace/db", "db:seed"] \ No newline at end of file diff --git a/apps/web/Dockerfile.worker b/apps/web/Dockerfile.worker index 8e80da642..6888a754a 100644 --- a/apps/web/Dockerfile.worker +++ b/apps/web/Dockerfile.worker @@ -39,9 +39,9 @@ COPY --from=deps --chown=worker:nodejs /app . USER worker -# Health check +# Health check - verify the worker process is running HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD node -e "console.log('Worker health check')" || exit 1 + CMD pgrep -f "file-processor" > /dev/null || exit 1 # Run the worker using tsx CMD ["pnpm", "tsx", "apps/web/src/workers/file-processor.ts"] \ No newline at end of file From 3ae8e3eae135f1228e2d1863703c3670f1916f88 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Tue, 24 Feb 2026 16:43:59 -0600 Subject: [PATCH 2/4] fix: address review feedback on realtime Dockerfile and worker healthcheck Realtime Dockerfile: - Add build steps for shared packages and realtime service (tsc) - Production-only install in runner stage (--prod) excludes devDependencies - Remove git/npm config (only pnpm is used, no git-based deps) - Runner now executes compiled JS via node instead of tsx Worker healthcheck: - Replace pgrep with node-based PID 1 liveness check - Avoids procps-ng dependency and ancestor shell match false-positives .dockerignore: - Use .env* glob with !.env.example override for broader coverage Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 7 ++----- apps/realtime/Dockerfile | 42 +++++++++++++++++++++----------------- apps/web/Dockerfile.worker | 4 ++-- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.dockerignore b/.dockerignore index c2c262b67..c42297230 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,11 +28,8 @@ dist/ !packages/**/dist/** # Environment files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local +.env* +!.env.example # IDE and editor files .vscode/ diff --git a/apps/realtime/Dockerfile b/apps/realtime/Dockerfile index 8835b6e3f..2484a9b6f 100644 --- a/apps/realtime/Dockerfile +++ b/apps/realtime/Dockerfile @@ -1,15 +1,10 @@ # syntax=docker/dockerfile:1.6 # Multi-stage build for the realtime Socket.IO service -# Stage 1: Install dependencies -FROM node:22.17.0-alpine AS deps +# Stage 1: Install dependencies and build +FROM node:22.17.0-alpine AS builder WORKDIR /app -RUN apk add --no-cache git && \ - npm config set fetch-timeout 300000 && \ - npm config set fetch-retry-maxtimeout 120000 && \ - npm config set fetch-retry-mintimeout 10000 - RUN corepack enable && \ (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) @@ -21,26 +16,35 @@ COPY apps/realtime/package.json ./apps/realtime/ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod=false -# Stage 2: Build shared packages -FROM node:22.17.0-alpine AS builder -WORKDIR /app -RUN corepack enable && \ - (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) - -COPY --from=deps /app . - COPY packages ./packages -COPY apps ./apps +COPY apps/realtime ./apps/realtime COPY tsconfig.json ./ -# Stage 3: Production runner +# Build shared packages and the realtime service +RUN pnpm --filter @pagespace/db build && \ + pnpm --filter @pagespace/lib build && \ + pnpm --filter realtime build + +# Stage 2: Production runner FROM node:22.17.0-alpine AS runner WORKDIR /app RUN corepack enable && \ (corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate || corepack prepare pnpm@10.13.1 --activate) -COPY --from=builder /app . +# Copy manifests and lockfile for production install +COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./ +COPY --from=builder /app/packages/db/package.json ./packages/db/ +COPY --from=builder /app/packages/lib/package.json ./packages/lib/ +COPY --from=builder /app/apps/web/package.json ./apps/web/ +COPY --from=builder /app/apps/realtime/package.json ./apps/realtime/ + +# Install production-only dependencies +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod + +# Copy built artifacts from builder +COPY --from=builder /app/packages ./packages +COPY --from=builder /app/apps/realtime ./apps/realtime ENV NODE_ENV=production @@ -48,4 +52,4 @@ USER node EXPOSE 3001 -CMD ["pnpm", "--filter", "realtime", "start"] +CMD ["node", "apps/realtime/dist/index.js"] diff --git a/apps/web/Dockerfile.worker b/apps/web/Dockerfile.worker index 6888a754a..bc78c7ce6 100644 --- a/apps/web/Dockerfile.worker +++ b/apps/web/Dockerfile.worker @@ -39,9 +39,9 @@ COPY --from=deps --chown=worker:nodejs /app . USER worker -# Health check - verify the worker process is running +# Health check - verify the main process (PID 1) is alive HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD pgrep -f "file-processor" > /dev/null || exit 1 + CMD node -e "try { process.kill(1, 0); } catch(e) { process.exit(1); }" # Run the worker using tsx CMD ["pnpm", "tsx", "apps/web/src/workers/file-processor.ts"] \ No newline at end of file From eaf56623523df5d9b6d56353d2dcd59c25defdcd Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Fri, 27 Feb 2026 08:38:10 -0600 Subject: [PATCH 3/4] fix: add missing trailing newlines to worker and seed Dockerfiles Co-Authored-By: Claude Opus 4.6 --- apps/web/Dockerfile.seed | 2 +- apps/web/Dockerfile.worker | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/Dockerfile.seed b/apps/web/Dockerfile.seed index fd5fd693a..e9a3faf50 100644 --- a/apps/web/Dockerfile.seed +++ b/apps/web/Dockerfile.seed @@ -27,4 +27,4 @@ COPY apps ./apps COPY tsconfig.json ./ # Run seed -CMD ["pnpm", "--filter", "@pagespace/db", "db:seed"] \ No newline at end of file +CMD ["pnpm", "--filter", "@pagespace/db", "db:seed"] diff --git a/apps/web/Dockerfile.worker b/apps/web/Dockerfile.worker index bc78c7ce6..35695ce66 100644 --- a/apps/web/Dockerfile.worker +++ b/apps/web/Dockerfile.worker @@ -44,4 +44,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD node -e "try { process.kill(1, 0); } catch(e) { process.exit(1); }" # Run the worker using tsx -CMD ["pnpm", "tsx", "apps/web/src/workers/file-processor.ts"] \ No newline at end of file +CMD ["pnpm", "tsx", "apps/web/src/workers/file-processor.ts"] From cdd3cdb0c562837e7a704953a9f9167142680028 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Fri, 27 Feb 2026 09:10:59 -0600 Subject: [PATCH 4/4] fix: narrow realtime Dockerfile COPY to only required packages and dist artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builder stage: replace broad `COPY packages` with targeted copies of packages/db, packages/lib, and types (needed for build-time type roots). Runner stage: copy only compiled dist/ directories instead of full package trees — package.json files are already present from the prod install step, so Node module resolution via pnpm workspace links works correctly. Co-Authored-By: Claude Opus 4.6 --- apps/realtime/Dockerfile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/realtime/Dockerfile b/apps/realtime/Dockerfile index 2484a9b6f..b5177dac7 100644 --- a/apps/realtime/Dockerfile +++ b/apps/realtime/Dockerfile @@ -16,7 +16,9 @@ COPY apps/realtime/package.json ./apps/realtime/ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod=false -COPY packages ./packages +COPY types ./types +COPY packages/db ./packages/db +COPY packages/lib ./packages/lib COPY apps/realtime ./apps/realtime COPY tsconfig.json ./ @@ -42,9 +44,10 @@ COPY --from=builder /app/apps/realtime/package.json ./apps/realtime/ # Install production-only dependencies RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile --prod -# Copy built artifacts from builder -COPY --from=builder /app/packages ./packages -COPY --from=builder /app/apps/realtime ./apps/realtime +# Copy only compiled runtime artifacts from builder +COPY --from=builder /app/packages/db/dist ./packages/db/dist +COPY --from=builder /app/packages/lib/dist ./packages/lib/dist +COPY --from=builder /app/apps/realtime/dist ./apps/realtime/dist ENV NODE_ENV=production