Files
wehub-resource-sync 6db8fca185
docmd CI verification / verify (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:55 +08:00

185 lines
7.7 KiB
Docker

# --------------------------------------------------------------------
# docmd : the minimalist, zero-config documentation generator.
#
# @package @docmd/core (and ecosystem)
# @website https://docmd.io
# @repository https://github.com/docmd-io/docmd
# @license MIT
# @copyright Copyright (c) 2025-present docmd.io
#
# [docmd-source] - Please do not remove this header.
# --------------------------------------------------------------------
#
# Multi-stage Dockerfile for docmd
# Creates a minimal production image with docmd CLI pre-installed
#
# Usage:
# Quick Start (with demo site):
# docker run -p 3000:3000 docmd:latest
#
# Initialize new project in current directory:
# docker run -v $(pwd):/workspace docmd:latest init
#
# Run dev server with your docs:
# docker run -v $(pwd)/my-docs:/docs -p 3000:3000 docmd:latest dev
#
# Build static site:
# docker run -v $(pwd)/docs:/docs -v $(pwd)/site:/site docmd:latest build
#
# --------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Stage 1: Build stage - compiles TypeScript and prepares the application
# -----------------------------------------------------------------------------
FROM node:20-alpine AS builder
# Install build dependencies for native modules
RUN apk add --no-cache \
python3 \
make \
g++ \
git \
curl
# Install pnpm directly — avoids corepack version resolution issues in CI.
# Bump the pinned version here whenever the monorepo's packageManager field changes.
RUN npm install -g pnpm@10.33.2
WORKDIR /app
# Copy everything (excluding files via .dockerignore)
COPY . .
# Install dependencies
RUN pnpm install --frozen-lockfile
# Stub out packages that can't build in Docker before running the full build:
# - engine-rust-binaries: requires cargo; pre-built .node files are in the repo
# - _playground: dev sandbox, not part of the production image
RUN sh tools/docker-prebuild.sh
# Build all packages
RUN pnpm -r run build
# Resolve workspace:* deps to real version numbers for standalone use
RUN node tools/resolve-ws-deps.js
# Pack all packages into tarballs (mimics npm publish)
RUN pnpm -r pack --pack-destination /tarballs
# Generate a package.json listing every external (non-@docmd) runtime dep of
# @docmd/core's transitive tree. The production stage installs from this file
# instead of a hand-rolled list in the Dockerfile, so adding a dep to any
# @docmd/* package automatically propagates to the Docker image.
RUN node tools/print-okf-runtime-deps.js --emit-pkg=/tarballs/runtime-deps.json
# -----------------------------------------------------------------------------
# Stage 2: Production stage - minimal image with only runtime dependencies
# -----------------------------------------------------------------------------
FROM node:20-alpine AS production
# Install runtime dependencies
RUN apk add --no-cache \
git \
curl \
bash \
su-exec
# Create non-root user for security
RUN addgroup -g 1001 -S docmd && \
adduser -S -D -H -u 1001 -h /app -s /bin/bash -G docmd -g docmd docmd
WORKDIR /app
# Copy packed tarballs from builder
COPY --from=builder /tarballs /tarballs
# Install only @docmd/* packages (the transitive deps of @docmd/core).
# We extract each docmd-*.tgz tarball into /usr/local/lib/node_modules/@docmd/<name>.
# This bypasses npm/pnpm registry lookups (packages aren't published yet at
# build time). Legacy wrappers (doc.md, @mgks/docmd) and the monorepo root
# tarball are excluded by only matching the docmd-* prefix.
RUN mkdir -p /usr/local/lib/node_modules && \
for tarball in /tarballs/docmd-*.tgz; do \
fname=$(basename "$tarball" .tgz) && \
pkg="${fname%-*}" && \
scoped="@docmd/${pkg#docmd-}" && \
mkdir -p "/usr/local/lib/node_modules/$scoped" && \
tar -xzf "$tarball" -C "/usr/local/lib/node_modules/$scoped" --strip-components=1; \
done
# Install external deps from the auto-generated runtime-deps.json produced by
# the builder stage. We extract the dep list with node and pass it straight to
# `npm install -g`, which puts packages into /usr/local/lib/node_modules —
# exactly where Node's global resolution path looks. No hand-rolled list in the
# Dockerfile: adding a dep to any @docmd/* package propagates automatically on
# the next build because the builder stage regenerates runtime-deps.json fresh.
RUN node -e "\
const p=JSON.parse(require('fs').readFileSync('/tarballs/runtime-deps.json','utf8')); \
const args=Object.entries(p.dependencies).map(([k,v])=>k+'@'+v); \
const {execFileSync}=require('child_process'); \
execFileSync('npm',['install','-g','--omit=dev','--no-audit','--no-fund',...args],{stdio:'inherit'}); \
"
# Symlink the docmd CLI onto PATH (chmod +x on the script too — some
# base images strip the executable bit during COPY).
RUN ln -sf /usr/local/lib/node_modules/@docmd/core/dist/bin/docmd.js /usr/local/bin/docmd \
&& chmod +x /usr/local/bin/docmd /usr/local/lib/node_modules/@docmd/core/dist/bin/docmd.js
# Clean up tarballs to reduce image size
RUN rm -rf /tarballs
# Create directories for user content
RUN mkdir -p /docs /site && chown -R docmd:docmd /docs /site
# Seed the default template by running docmd init non-interactively.
# This ensures the image ships exactly what a real `docmd init` produces.
#
# Build does NOT fail on init failure: the image still publishes (so users
# who mount their own /docs are unaffected), but the demo experience is
# degraded and a clear warning is printed. Full stderr from `docmd init`
# is in the build log above this block for diagnosis.
RUN mkdir -p /template && \
cd /template && \
if docmd init --yes; then \
if [ ! -f /template/docmd.config.json ]; then \
printf '\n[docmd-docker] WARNING: docmd init exited 0 but /template/docmd.config.json is missing.\n' >&2; \
printf '[docmd-docker] Demo seeding will not work. Mount your own /docs.\n' >&2; \
fi; \
else \
printf '\n[docmd-docker] WARNING: docmd init failed during image build.\n' >&2; \
printf '[docmd-docker] The `docker run` demo experience will not work. Mount your own /docs.\n' >&2; \
printf '[docmd-docker] Full error from docmd init is in the build log above this block.\n' >&2; \
fi
# Install entrypoint + healthcheck scripts (see docker/docker-entrypoint.sh
# and docker/docker-healthcheck.sh). The entrypoint seeds /docs from
# /template when /docs is empty, so `docker run` with no volume mount
# still works out of the box. The healthcheck probes a port range because
# `docmd dev` may auto-increment past 3000 if the port is already taken.
COPY docker/docker-entrypoint.sh /usr/local/bin/docmd-entrypoint.sh
COPY docker/docker-healthcheck.sh /usr/local/bin/docmd-healthcheck.sh
RUN chmod +x /usr/local/bin/docmd-entrypoint.sh /usr/local/bin/docmd-healthcheck.sh
# Set environment variables
ENV NODE_ENV=production
ENV DOCMD_CONTAINER=true
# Run as root so the entrypoint can inspect /docs ownership and use
# su-exec to re-exec as the correct uid:gid. The entrypoint never stays
# as root — it always drops to the detected uid before exec-ing docmd.
# This is the same pattern used by postgres, redis, and other official images.
# Set working directory to docs (where user content will be mounted)
WORKDIR /docs
# Expose default port (dev server may pick a higher one if 3000 is in use)
EXPOSE 3000
# Health check — probes 3000-3005 to follow the dev server's actual port
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD ["/usr/local/bin/docmd-healthcheck.sh"]
# Default command — entrypoint detects /docs uid, drops privileges, then
# seeds /docs if empty and execs the supplied command.
ENTRYPOINT ["/usr/local/bin/docmd-entrypoint.sh"]
CMD ["dev", "--host", "0.0.0.0"]