ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
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"]