Ctrl K
ComunidadGuías…

Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# syntax=docker/dockerfile:1

# Slim multi-stage build. The runtime image ships a PROD-ONLY node_modules (no devDeps, no tsx,
# no test tooling), the Next build output, the three esbuild-transpiled server entrypoints, and
# the migration SQL. The workers are transpiled (not inlined): deps like jsdom read their own data
# files via __dirname-relative reads, so they must stay in a real node_modules, not a bundle.
# See docs/superpowers/specs/2026-07-07-docker-single-box-deploy-design.md.

FROM node:22-alpine AS base
RUN corepack enable
WORKDIR /app

# --- deps: full install (build needs devDeps: next, esbuild, drizzle types) ---
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# --- prod-deps: production-only tree for the runtime image ---
FROM base AS prod-deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod

# --- build: Next build + transpiled ws/worker/migrate entrypoints ---
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_* vars are inlined into the client bundle at build time, so they must be present
# during `next build`, not only at runtime (F29). Compose passes this as a build arg.
ARG NEXT_PUBLIC_WS_URL
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
# Git SHA of this build. Needed here as well as in the runtime stage: a prerendered page reads
# env.APP_COMMIT once, at build time, so a runtime-only value would leave those pages unstamped.
# Free cache-wise, since the `COPY . .` above already changes on every commit.
ARG APP_COMMIT=
# Fail loudly if it is empty: the value is compile-time-inlined, so an empty build bakes a broken
# WS URL into the client bundle (board/inbox/import realtime then throw `new WebSocket("")`), and
# fixing it needs a full rebuild. Better to stop the build than ship a silently-broken bundle.
RUN test -n "$NEXT_PUBLIC_WS_URL" || (echo "ERROR: NEXT_PUBLIC_WS_URL build arg is required (compile-time inlined)" >&2; exit 1)
# `next build` imports server modules that load src/config/env.ts, which validates and throws at
# import. There is no .env in the image, so supply schema-satisfying PLACEHOLDERS just for this
# command (inline on RUN, so they never persist in a layer). Only NEXT_PUBLIC_* is inlined into
# output; these server values are never baked in and are fully overridden by the runtime env_file.
# (TOKEN_ENCRYPTION_KEY here is 32 zero bytes, base64.)
RUN NODE_ENV=production \
    APP_COMMIT=${APP_COMMIT} \
    DATABASE_URL=postgres://build:build@localhost:5432/build \
    GOOGLE_OAUTH_CLIENT_ID=build \
    GOOGLE_OAUTH_CLIENT_SECRET=build \
    GOOGLE_WORKSPACE_DOMAIN=build.example.com \
    BASE_URL=http://localhost:3000 \
    WS_TICKET_SECRET=build_placeholder_ws_ticket_secret_0123456789 \
    WS_PUBLIC_URL=ws://localhost:8080 \
    S3_ENDPOINT=http://localhost:3900 \
    S3_ACCESS_KEY=build \
    S3_SECRET_KEY=build \
    TOKEN_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \
    OAUTH_SIGNING_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \
    SEED_ADMIN_EMAIL=build@example.com \
    ALLOW_FIRST_LOGIN_ADMIN=false \
    pnpm build:docker

# --- runtime: minimal, non-root ---
FROM base AS runtime
ENV NODE_ENV=production
# Optional version stamp for the update-check banner (e.g. a release tag). When unset, the app
# falls back to the bundled package.json version, then "dev" (banner disabled). Not compile-time
# inlined: it is a server-only runtime env, read via src/config/env.ts.
ARG APP_VERSION=
ENV APP_VERSION=${APP_VERSION}
# `next start` must bind all interfaces (not localhost) to be reachable across the compose
# network / from the Caddy proxy.
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
COPY --from=prod-deps /app/node_modules ./node_modules
# `next start` writes the fetch/route cache under .next/cache at request time, so this tree must be
# owned by the non-root runtime user or those writes hit EACCES (noisy logs + disabled caching).
COPY --chown=node:node --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/dist ./dist
COPY --from=build /app/drizzle ./drizzle
COPY package.json next.config.ts ./
# Declared after the COPYs on purpose: this value changes every commit, and an earlier layer would
# invalidate the node_modules/.next copies on every build. Serves the dynamically rendered pages,
# which read it at request time.
ARG APP_COMMIT=
ENV APP_COMMIT=${APP_COMMIT}
# node:alpine ships an unprivileged `node` user; run as it, not root.
USER node
EXPOSE 3000 8080
# App is the default; compose overrides command per service (ws/worker/migrate).
CMD ["node_modules/.bin/next", "start"]