Dockerfile
Rama main · réplica de lectura del repositorio original.
# syntax=docker/dockerfile:1.7
# Crewship -- Single Binary Production Dockerfile
# Multi-stage: Go build + Next.js static export → minimal Alpine image
# Image: ghcr.io/crewship-ai/crewship:latest
#
# BuildKit cache mounts persist the pnpm store and Go module/build cache
# across `docker build` invocations even when the build context layer is
# invalidated, dramatically speeding up incremental builds. Requires
# BuildKit (default on modern Docker; enable with DOCKER_BUILDKIT=1 on
# old daemons) and the syntax directive above.
# Node 22 to match CI (NODE_VERSION: "22" in .github/workflows) and the
# support matrix of the pinned prisma 7.8.0, whose preinstall only accepts
# Node 20.19+/22.12+/24.0+. On node:26-alpine the Prisma CLI's WASM schema
# parser corrupts every schema line to `""` under the emulated linux/arm64
# build, so `pnpm prisma generate` dies with a wall of P1012 "This line is
# invalid" errors even though the schema is valid. Bump only in lockstep
# with CI + a Prisma release that declares Node 26 support.
FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend
# Newer node-alpine images stopped bundling corepack by default, so
# `corepack enable` alone can fail with "corepack: not found". Install it
# explicitly first; `pnpm install` below then picks up the exact version
# pinned in package.json's `packageManager` field, same as CI's
# pnpm/action-setup (see .github/actions/setup-node-pnpm/action.yml).
RUN npm install -g corepack@0.34.1 && corepack enable pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# Generate types and legal inventory before app sources: a UI edit must not
# redo work determined solely by Prisma inputs or the locked dependency tree.
# .npmrc stays after install, matching the existing install configuration.
COPY .npmrc prisma.config.ts ./
COPY prisma/ ./prisma/
RUN pnpm prisma generate
COPY scripts/gen-frontend-licenses.mjs ./scripts/
# License texts of the embedded frontend's npm dependencies, from the
# lockfile-installed tree (no network). Collected in-stage so every docker
# build path — PR image build, release, nightly, local — is self-contained.
RUN node scripts/gen-frontend-licenses.mjs /licenses-frontend
# Backend-only sources leave these COPY layers cached. VERSION still forces
# a fresh static export when the build identity changes.
# Root TS configs and e2e sources remain inputs because tsconfig.json includes
# them in Next's type check. The source contract tests guard new TS inputs.
COPY *.ts *.json *.mjs ./
COPY app/ ./app/
COPY components/ ./components/
COPY hooks/ ./hooks/
COPY lib/ ./lib/
COPY stores/ ./stores/
COPY public/ ./public/
# Shared JSON contracts are imported by the UI; exclude their Go sources.
COPY config/*.json ./config/
COPY schemas/*.json ./schemas/
COPY schemas/wordcraft/ ./schemas/wordcraft/
COPY internal/pages/content_types.json ./internal/pages/
COPY internal/slides/lucide_icons.json ./internal/slides/
COPY mooring/client/ ./mooring/client/
COPY mooring/protocol/generated/ts/ ./mooring/protocol/generated/ts/
COPY e2e/ ./e2e/
# Reference editors are TypeScript inputs checked by the production build.
COPY examples/ ./examples/
COPY scripts/prepare-pdf-assets.mjs scripts/prepare-chat-workers.mjs scripts/check-control-accessibility.tsx ./scripts/
COPY tools/pages-build/sdk.ts ./tools/pages-build/
# Standalone registry tooling is also part of the root TypeScript input graph.
COPY tools/wordcraft-capabilities/ ./tools/wordcraft-capabilities/
# Explicit C-E acceptance files participate in the root TypeScript graph.
COPY e2e/wordcraft-ce/ ./e2e/wordcraft-ce/
COPY tools/wordcraft-ce/ ./tools/wordcraft-ce/
COPY playwright.wordcraft-ce.config.ts ./
# The browser instantiates this shared closed model with its own pinned Yjs.
# Copy only the closed schema modules, without the separate Docs service runtime.
COPY tools/docs-service/schema-core.mjs tools/docs-service/position-migrations.mjs tools/docs-service/surface-core.mjs ./tools/docs-service/
ARG VERSION=dev
ARG NEXT_PUBLIC_SENTRY_DSN=""
ENV NEXT_PUBLIC_CREWSHIP_VERSION=$VERSION
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.27.3-alpine AS backend
ARG TARGETOS
ARG TARGETARCH
# This tag is the compiler for the shipped binary, and it is the one Go version
# no pull-request check ever exercises: the image is built by release.yml and
# nightly.yml only (#2064). Dependabot's `docker-images` group bumps it alone.
#
# `local` makes the tag above the single authority: the go command uses the
# toolchain this image ships and never downloads another. Without it the
# default is `auto`, which honours go.mod's `toolchain` directive by FETCHING
# that compiler — so a `toolchain` bump would quietly build the release with a
# Go that no line in this repo pins and no vuln scan ever graded.
#
# Deliberately not a version literal. `GOTOOLCHAIN=go1.27.0` would be one more
# copy of the string to keep in sync, and it fails the wrong way round: bump
# the FROM tag above, forget the literal, and Go downloads the OLD toolchain
# and undoes the bump while every check stays green. `local` cannot do that.
#
# What `local` does NOT do — verified against this image, not assumed — is
# object when go.mod's `toolchain` names something else. That directive is
# ignored outright under `local`, so a `toolchain go1.27.3` against this 1.27.2
# tag builds happily and silently with 1.27.2. Only the `go` DIRECTIVE can fail
# the build ("go.mod requires go >= 1.28; running go 1.27.2"), and that line
# tracks the language floor, which moves only when a dependency forces it and
# so cannot be relied on here. So nothing at build time will ever tell us these
# two files disagree — which is exactly why the disagreement is checked
# statically instead, by scripts/go-toolchain-pin.sh, on every PR.
#
# The official golang-alpine images already default to `local`, so this line
# changes nothing today. It is written down because an inherited upstream
# default is a fact that happens to hold, not an invariant: stated here it
# survives a base-image swap, and the guard can check it alongside the FROM
# tag, go.mod's `toolchain` directive and every GO_VERSION in .github/.
ENV GOTOOLCHAIN=local
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,id=go-mod,target=/go/pkg/mod \
go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
# Public schema and generated Go wire types; no Rust compiler or vendor tree.
COPY mooring/protocol/ ./mooring/protocol/
# CLI embeds the supported Pages dependency lock and source starter.
COPY tools/ ./tools/
COPY examples/ ./examples/
# config/ is the embedded model catalog (config/embed.go + models.json),
# imported by internal/llm since #2305 — a root-level package like schemas/,
# and missing from this stage the same way (#2328). scripts/dockerfile-sources
# fails the Go suite when a root package the binaries import is not copied.
COPY config/ ./config/
# schemas/ is a root-level package (embed.go + routine.v1.json) imported by
# internal/api and cmd/crewship (#849). It must be copied like any other
# source dir or the in-image `go build ./cmd/crewship` fails to resolve
# github.com/crewship-ai/crewship/schemas (#886). Regular CI hides this
# because it builds from a full checkout; this stage copies dirs selectively.
COPY schemas/ ./schemas/
# config/ is the second such root-level package (embed.go + models.json +
# rate-limits.yml), imported by internal/llm since #2305. Same rule as
# schemas/: it is not under cmd/ or internal/, so it has to be copied by
# name or the in-image build fails to resolve
# github.com/crewship-ai/crewship/config — which is exactly how the
# 2026-09-04 nightly image build broke while every PR check stayed green.
# The PR image build (#2064) now catches this class before merge, and
# scripts/pr-image-build-paths.sh keeps its path filter in step with the
# COPY lines here.
COPY web/ ./web/
COPY --from=frontend /app/out ./web/out
# Release gate (#1567). web/out/ now always compiles — a tracked placeholder
# keeps `//go:embed all:out` resolvable in a bare checkout — so the image
# build has to prove the frontend stage actually delivered a real export
# instead of letting a UI-less crewship image ship quietly. scripts/ is not
# copied into this stage, so the check is inlined rather than calling
# scripts/embed-web-out.sh; _next/ is what a Next.js export always emits and
# a placeholder or hand-rolled stub never does.
RUN test -f ./web/out/index.html && test -d ./web/out/_next \
|| (echo "ERROR: web/out/ has no Next.js static export — refusing to build an image with no web UI" >&2; exit 1)
ARG VERSION=dev
ARG COMMIT=none
ARG DATE=unknown
# SENTRY_DSN is intentionally a build-arg with empty default. The release
# workflow passes --build-arg SENTRY_DSN=$SENTRY_DSN from the GH secret;
# local `docker build` produces a binary with telemetry hard-off (the
# crashreport package treats empty DSN as "stay disabled regardless of
# opt-in" so dev images never phone home).
ARG SENTRY_DSN=""
# The daemon validates the bundled sidecar even in --no-docker mode.
# Build it for the same target architecture as the server.
RUN --mount=type=cache,id=go-mod,target=/go/pkg/mod \
--mount=type=cache,id=go-build,target=/root/.cache/go-build \
go run ./tools/gen-licenses -out /licenses-go
RUN --mount=type=cache,id=go-mod,target=/go/pkg/mod \
--mount=type=cache,id=go-build,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath \
-ldflags="-s -w" -o /crewship-sidecar ./cmd/crewship-sidecar
# -trimpath strips workspace paths from binary debug info — same
# rationale as the Makefile / goreleaser changes: reproducible builds
# so cosign-verified hashes match across builders.
RUN --mount=type=cache,id=go-mod,target=/go/pkg/mod \
--mount=type=cache,id=go-build,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath \
-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE} -X github.com/crewship-ai/crewship/internal/crashreport.DSN=${SENTRY_DSN}" \
-o /crewship ./cmd/crewship
# The workbook test barrier exists only in binaries built with the
# crewship_testbarrier tag. No image may carry it, whatever build args or
# GOFLAGS were used; the same guard checks every release archive.
COPY scripts/check-no-test-barrier.sh /usr/local/bin/check-no-test-barrier
RUN sh /usr/local/bin/check-no-test-barrier /crewship /crewship-sidecar
# -- Runner --
# The docker CLI (page builds, devcontainer images) comes from Docker's static
# release, not Alpine's docker-cli package: Alpine rebuilds that package on its
# own schedule, and alpine:3.24 shipped it built with go1.26.8, which the image
# scan fails critical on (#3047). Bump the version and both checksums together;
# check the archive's Go with `strings docker/docker | grep -m1 '^go1\.'`.
FROM alpine:3.24 AS dockercli
ARG TARGETARCH
ARG DOCKER_CLI_VERSION=29.9.0
ARG DOCKER_CLI_SHA256_amd64=33e1ab8b63d14bca449f7a3d30d7f6aa669daa544e60a86b17fec87f61afe2b4
ARG DOCKER_CLI_SHA256_arm64=811f6eee271678fc98dbe5c4d896c00fc5fe2daa4f7a37069c17abc036eab85b
RUN set -eu; \
case "$TARGETARCH" in \
amd64) arch=x86_64; sum="$DOCKER_CLI_SHA256_amd64" ;; \
arm64) arch=aarch64; sum="$DOCKER_CLI_SHA256_arm64" ;; \
*) echo "no pinned docker CLI for $TARGETARCH" >&2; exit 1 ;; \
esac; \
wget -q -O /tmp/docker.tgz "https://download.docker.com/linux/static/stable/${arch}/docker-${DOCKER_CLI_VERSION}.tgz"; \
echo "${sum} /tmp/docker.tgz" | sha256sum -c -; \
tar -xzf /tmp/docker.tgz -C /tmp docker/docker; \
install -m 0755 /tmp/docker/docker /docker; \
/docker --version
FROM alpine:3.24 AS runtime
ARG VERSION=dev
ARG COMMIT=none
ARG DATE=unknown
LABEL org.opencontainers.image.source="https://github.com/crewship-ai/crewship" \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.created=$DATE
# Refresh inherited libraries too: apk add alone retains vulnerable base packages.
RUN apk --no-cache upgrade && \
apk --no-cache add ca-certificates git && \
addgroup -g 1001 -S crewship && adduser -u 1001 -S crewship -G crewship
RUN mkdir -p /var/lib/crewship /var/log/crewship /data && \
chown -R crewship:crewship /var/lib/crewship /var/log/crewship /data
COPY --from=dockercli /docker /usr/local/bin/docker
COPY --from=backend /crewship /usr/local/bin/crewship
COPY --from=backend /crewship-sidecar /usr/local/bin/crewship-sidecar
COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
# Distribution legal files, produced inside the frontend/backend stages
# (self-contained: PR image builds and local docker builds need no host
# pre-step). The test refuses an image without them, so no supported build
# path ships license-less.
COPY LICENSE NOTICE /usr/share/doc/crewship/
COPY --from=backend /licenses-go /usr/share/doc/crewship/licenses/go
COPY --from=frontend /licenses-frontend /usr/share/doc/crewship/licenses/frontend
RUN test -f /usr/share/doc/crewship/NOTICE \
&& test -d /usr/share/doc/crewship/licenses/go \
&& test -f /usr/share/doc/crewship/licenses/frontend/npm-licenses.json
COPY docker/server-entrypoint.sh /usr/local/bin/crewship-entrypoint
RUN chmod +x /usr/local/bin/crewship-entrypoint
ENV DATABASE_URL=file:/data/crewship.db \
CREWSHIP_STORAGE_BASE_PATH=/var/lib/crewship \
CREWSHIP_LOG_PATH=/var/log/crewship \
CREWSHIP_BOLT_PATH=/data/state.db
WORKDIR /data
USER crewship
EXPOSE 8080
# The wrapper script pre-flight-checks required env vars (NEXTAUTH_SECRET,
# ENCRYPTION_KEY) and prints an actionable error before the binary panics
# deep inside server.New(). `docker run` without these vars now exits 78
# with copy-pasteable fix instructions instead of leaving the user with a
# blank :8080 and a stack trace buried in `docker logs`.
ENTRYPOINT ["crewship-entrypoint"]