# ───────────────────────────────────────────────────────────────────────────── # Stage 1 — Build client # ───────────────────────────────────────────────────────────────────────────── FROM node:22-alpine AS client-builder WORKDIR /build # Install pnpm RUN corepack enable && corepack prepare pnpm@latest --activate # Copy workspace manifests first for better layer caching COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* ./ COPY apps/client/package.json ./apps/client/ COPY apps/server/package.json ./apps/server/ COPY tsconfig.base.json ./ # Install all workspace deps RUN pnpm install --frozen-lockfile # Copy client source and build COPY apps/client ./apps/client/ RUN pnpm --filter client build # ───────────────────────────────────────────────────────────────────────────── # Stage 2 — Build server # ───────────────────────────────────────────────────────────────────────────── FROM node:22-alpine AS server-builder WORKDIR /build RUN corepack enable && corepack prepare pnpm@latest --activate COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* ./ COPY apps/server/package.json ./apps/server/ COPY apps/client/package.json ./apps/client/ COPY tsconfig.base.json ./ # Install only server production deps + rebuild native modules for target arch RUN pnpm install --frozen-lockfile COPY apps/server ./apps/server/ RUN pnpm --filter server build # ───────────────────────────────────────────────────────────────────────────── # Stage 3 — Production runtime # ───────────────────────────────────────────────────────────────────────────── FROM node:22-alpine AS runtime # Install tini for proper PID 1 signal handling RUN apk add --no-cache tini su-exec WORKDIR /app # No native deps — runtime only needs node_modules for pure-JS packages (express, cors, etc.) # pnpm hoists everything to the root node_modules (shamefully-hoist=true in .npmrc) COPY --from=server-builder /build/node_modules ./node_modules # Copy compiled server output (includes dist/db/migrations/*.js compiled by tsc) COPY --from=server-builder /build/apps/server/dist ./apps/server/dist # Copy built client into the path the server expects COPY --from=client-builder /build/apps/client/dist ./apps/client/dist # ── Runtime configuration ───────────────────────────────────────────────── ENV NODE_ENV=production \ PORT=3001 \ DATA_DIR=/data \ PHOTOS_DIR=/photos \ PUID=99 \ PGID=100 \ TZ=UTC \ NODE_NO_WARNINGS=1 # /data — persistent: SQLite database # /photos — bind-mount: read-only user photo library VOLUME ["/data"] EXPOSE 3001 # Entrypoint: fix ownership then drop to PUID/PGID COPY docker-entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"] CMD ["node", "apps/server/dist/index.js"]