ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
at master 80 lines 4.0 kB view raw
1# ─── Stage 1: Builder ──────────────────────────────────────────────────────── 2# This stage has all the dev tools needed to compile TypeScript. 3# It will NOT be included in the final image. 4FROM node:20-alpine AS builder 5 6WORKDIR /app 7 8# Enable Corepack and activate the exact pnpm version from package.json. 9# This ensures we use pnpm 10.28.0 — the same version that generated the lockfile. 10# Using a different pnpm version with --frozen-lockfile can cause build failures. 11RUN corepack enable && corepack prepare pnpm@10.28.0 --activate 12 13# ── Layer caching: copy package manifests BEFORE source code ── 14# Docker caches each layer. If these files do not change, the 15# expensive `pnpm install` step below is skipped on subsequent builds. 16# Changing a .ts source file will NOT invalidate this layer. 17# Copy ALL workspace package.json files before running pnpm install. 18# pnpm needs to see every package in the workspace to correctly resolve 19# the lockfile and install each package's dependencies into the virtual store. 20# Without all 6 package.json files, workspace package deps (kysely, hono, pg) 21# are not linked even though the install appears to succeed. 22COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ 23COPY packages/api/package.json ./packages/api/ 24COPY packages/shared/package.json ./packages/shared/ 25COPY packages/worker/package.json ./packages/worker/ 26COPY packages/web/package.json ./packages/web/ 27COPY packages/functions/package.json ./packages/functions/ 28COPY packages/extension/package.json ./packages/extension/ 29 30# Install ALL dependencies (including devDependencies needed for TypeScript compilation). 31# --frozen-lockfile: fail if pnpm-lock.yaml would be updated (ensures reproducible builds). 32RUN pnpm install --frozen-lockfile 33 34# Now copy source code. This layer changes often, but that is fine because 35# the slow `pnpm install` layer above is already cached. 36COPY packages/api ./packages/api 37COPY packages/shared ./packages/shared 38 39# Build shared package first — api imports from @atlast/shared. 40# The TypeScript compiler needs shared's output before compiling api. 41RUN pnpm --filter=@atlast/shared build 42 43# Build the API package (tsc outputs to packages/api/dist/). 44RUN pnpm --filter=@atlast/api build 45 46# Create a self-contained production deployment bundle. 47# `pnpm deploy` resolves workspace symlinks (@atlast/shared) into real files, 48# installs only production dependencies, and writes everything to /deploy. 49# Without this, the container would have broken symlinks to packages that 50# do not exist at the expected path inside the container. 51# --legacy flag required in pnpm v10 for workspaces without inject-workspace-packages=true 52RUN pnpm --filter=@atlast/api --prod deploy --legacy /deploy 53 54 55# ─── Stage 2: Production ───────────────────────────────────────────────────── 56# Start fresh from a minimal Node.js image. 57# NOTHING from the builder stage is included — no TypeScript, no pnpm, no source. 58FROM node:20-alpine 59 60WORKDIR /app 61 62# Create a non-root user. 63# By default, Node.js containers run as root. If an attacker exploits a 64# vulnerability in the app, running as non-root limits what they can do. 65# They cannot modify system files, install packages, or affect other containers. 66RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 -G nodejs 67 68# Copy only the deployment bundle from the builder stage. 69# This includes: dist/ (compiled JS) + node_modules/ (resolved deps, no symlinks). 70# --chown sets file ownership to the non-root user we created above. 71COPY --from=builder --chown=nodejs:nodejs /deploy . 72 73# Switch to non-root user before the process starts. 74USER nodejs 75 76# Document which port the app listens on (does not actually open the port). 77EXPOSE 3000 78 79# Start the compiled server. 80CMD ["node", "dist/server.js"]