185 lines
7.7 KiB
Docker
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"] |