Dockerfile
Rama main · réplica de lectura del repositorio original.
# syntax=docker/dockerfile:1.28
#
# Inkweld Backend Dockerfile
# Uses Bun's --compile flag to create a single-file executable with embedded native modules.
# This produces a ~200MB image instead of ~1.5GB.
#
# The key challenge is native module embedding: node-gyp-build dynamically resolves .node
# files at runtime, but Bun's compiler needs static requires to embed them. We patch
# classic-level/binding.js to directly require the N-API prebuild before compilation.
#
# Multi-arch build strategy:
# - When FRONTEND_PREBUILT build arg is set, skip frontend build and use pre-built dist
# - This allows CI to build frontend once on AMD64 and share with ARM64 builds
# Frontend builder stage (Angular) - only used when not using pre-built frontend
FROM oven/bun:1.4.4 AS frontend-builder
WORKDIR /app
# Angular CLI 22 requires Node ^22.22.3 || ^24.15.0 || >=26; the bun image's
# `node` shim reports itself as Node 24.3.0 and fails the CLI's version check,
# so overwrite it with a real Node binary for `ng build`.
COPY --from=node:24-bookworm-slim /usr/local/bin/node /usr/local/bin/node
# Check if we should skip building (pre-built frontend provided)
ARG FRONTEND_PREBUILT=false
# Install git for dependencies that need to be cloned
RUN if [ "$FRONTEND_PREBUILT" = "false" ]; then \
apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*; \
fi
# Workspace-aware install: the frontend resolves `@inkweld/prosemirror/*`
# via tsconfig path aliases that point at `../packages/inkweld-prosemirror/src/*`.
# That package in turn imports `prosemirror-model` and `yjs`, so the workspace
# package needs its own `node_modules`. We install at the workspace root first
# (which links the `packages/*` workspace deps), then per-app inside `frontend/`.
COPY package.json bun.lock* ./
COPY packages ./packages
COPY frontend/bun.lock frontend/package.json ./frontend/
# copy-changelog.mjs ships the repo-root changelog as an in-app asset
COPY CHANGELOG.md ./
# Install workspace deps so `packages/inkweld-prosemirror/node_modules/{prosemirror-model,yjs}`
# get linked. The root `postinstall` script chains into `frontend && backend && docs/site`
# installs that we don't want here, so override it.
RUN if [ "$FRONTEND_PREBUILT" = "false" ]; then \
bun install --frozen-lockfile --ignore-scripts; \
fi
WORKDIR /app/frontend
# Install with --ignore-scripts to disable arbitrary postinstall execution,
# then explicitly run esbuild's binary installer (the only postinstall the
# Angular build genuinely needs).
RUN if [ "$FRONTEND_PREBUILT" = "false" ]; then \
bun install --frozen-lockfile --ignore-scripts \
&& node node_modules/esbuild/install.js; \
fi
COPY frontend ./
# `--ignore-scripts` above skipped the frontend postinstall. Run its steps
# explicitly (the same ones a developer's `bun install` runs), because the
# build relies on their output:
# fetch-publish-fonts -> public/assets/fonts/*.ttf, embedded into PDFs by
# Typst; verified against scripts/font-digests.json
# generate-font-licenses-> public/assets/fonts/LICENSE.txt, the OFL notice
# the redistributed woff2/ttf faces require
# copy-changelog -> public/assets/CHANGELOG.md for the in-app changelog
# Then build, verify output exists, and compress WASM files with Brotli
# (they're served with Content-Encoding: br).
RUN if [ "$FRONTEND_PREBUILT" = "false" ]; then \
node scripts/fetch-publish-fonts.mjs \
&& node scripts/generate-font-licenses.mjs \
&& node scripts/copy-changelog.mjs \
&& bun run build \
&& if [ ! -d /app/frontend/dist ]; then \
echo "ERROR: frontend build did not produce /app/frontend/dist"; \
ls -la /app/frontend || true; \
exit 1; \
fi \
&& node scripts/compress-wasm.js; \
fi
# Backend builder stage - produces a single compiled binary
FROM oven/bun:1.4.4 AS backend-builder
WORKDIR /app/backend
# Install git for dependencies that need to be cloned
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
# No build tools needed - we use bun:sqlite (native to Bun), not better-sqlite3
# better-sqlite3 is only needed for the Node.js runner (node-runner.ts)
# Native modules (classic-level, bcrypt) ship with prebuilds and are patched later
# Install dependencies with --ignore-scripts to skip better-sqlite3's node-gyp build
# (Bun doesn't support prebuild-install, causing it to fall back to node-gyp which fails)
# Then run postinstall for packages that need platform-specific binaries:
# - esbuild: downloads platform-specific binary
# - sharp: downloads prebuilt libvips binaries
COPY backend/bun.lock backend/package.json ./
# bun.lock references patchedDependencies; the patch files must be present
# before `bun install --frozen-lockfile` or the install fails.
COPY backend/patches ./patches
RUN bun install --frozen-lockfile --ignore-scripts && \
node node_modules/esbuild/install.js && \
{ cd node_modules/sharp && node install/check.js || true; }
# Copy source and build scripts
COPY backend .
# Copy the workspace `@inkweld/prosemirror` package source plus root manifests:
# backend imports it via tsconfig path aliases (`../packages/inkweld-prosemirror/src/*`)
# which Bun resolves at build/compile time. The package itself depends on
# `prosemirror-model` and `yjs`, which Bun resolves through the workspace's
# `packages/inkweld-prosemirror/node_modules` (linked by a workspace-root install).
COPY package.json bun.lock* /app/
COPY packages /app/packages
WORKDIR /app
RUN bun install --frozen-lockfile --ignore-scripts
WORKDIR /app/backend
# Determine target architecture for native module patching and Bun compilation
ARG TARGETARCH
# Patch native modules for Bun binary compilation:
# This modifies classic-level/binding.js to directly require the Linux glibc prebuild
# instead of using node-gyp-build's runtime resolution. This allows Bun to statically
# analyze and embed the .node file in the compiled binary.
RUN PATCH_TARGET=$([ "${TARGETARCH}" = "arm64" ] && echo "linux-arm64-glibc" || echo "linux-x64-glibc") && \
echo "Patching native modules for: $PATCH_TARGET" && \
bun scripts/patch-native-modules.ts patch "$PATCH_TARGET"
# Copy frontend dist for binary embedding
# When FRONTEND_PREBUILT=true, the dist was already in the build context and copied into frontend-builder
# When FRONTEND_PREBUILT=false, frontend-builder built it fresh
# Either way, it's in /app/frontend/dist in the frontend-builder stage
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
# Generate frontend imports file for binary embedding
# This scans the frontend dist and creates TypeScript imports with { type: 'file' }
# so Bun can embed all frontend assets in the compiled binary
RUN bun scripts/generate-frontend-imports.ts
# Build TypeScript and compile to single binary
# The --compile flag creates a standalone executable with embedded Bun runtime
# The patched native module is embedded because it's now a static require
# Bun's bundler replaces `process.env.NODE_ENV` with its value at build time,
# so the compile runs with the NODE_ENV the image runs with. It is set on that
# command only, not as a stage ENV, so the `bun install` steps above keep
# devDependencies. Inkweld's own code reads NODE_ENV at run time
# (src/config/runtime-env.ts); this keeps bundled dependencies in step.
RUN BUN_TARGET=$([ "${TARGETARCH}" = "arm64" ] && echo "bun-linux-arm64" || echo "bun-linux-x64") && \
echo "Compiling with target: $BUN_TARGET" && \
bun run build && \
NODE_ENV=production bun build --compile --minify --sourcemap \
--target="$BUN_TARGET" \
./src/bun-runner.ts \
--outfile ./inkweld-server
# Runtime image - minimal with just the binary
FROM debian:bookworm-slim AS backend-runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=8333 \
HOST=0.0.0.0 \
DB_TYPE=sqlite \
DB_PATH=/data/sqlite.db \
DATA_PATH=/data/yjs \
DB_LOGGING=false \
FRONTEND_DIST=/app/frontend/browser
# Install only runtime dependencies (curl for healthcheck, ca-certs for HTTPS)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
adduser --disabled-password --gecos "" inkweld && \
mkdir -p /data && chown -R inkweld:inkweld /data /app
# Copy only the compiled binary and drizzle migrations (required for DB setup)
COPY --from=backend-builder --chown=inkweld:inkweld --chmod=555 /app/backend/inkweld-server ./
COPY --from=backend-builder --chown=inkweld:inkweld --chmod=555 /app/backend/drizzle ./drizzle
# Generates and keeps a key in /data when no DATABASE_KEY or SESSION_SECRET
# is set (see the script), so an install that never set one still starts.
# Owned by root, so the server's own user cannot change it.
COPY --from=backend-builder --chmod=555 /app/backend/scripts/docker-entrypoint.sh ./docker-entrypoint.sh
# Copy frontend assets from dist directory
# Angular outputs to dist/browser/ in production mode. We copy the entire dist/ directory
# to /app/frontend/, which creates /app/frontend/browser/ containing index.html and assets.
# FRONTEND_DIST env var points to /app/frontend/browser for serving.
COPY --from=frontend-builder --chown=inkweld:inkweld --chmod=555 /app/frontend/dist /app/frontend
USER inkweld
VOLUME ["/data"]
EXPOSE 8333
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8333/api/v1/health || exit 1
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["./inkweld-server"]