mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
200 lines
10 KiB
Docker
200 lines
10 KiB
Docker
# the production all-in-one image: caddy + web (nuxt SSR) + core +
|
|
# auth-realtime, supervised by the launcher under tini. Build it with
|
|
# `make prod-build` from the repository root — the core binary comes
|
|
# from goreleaser and is staged into docker/prod/.build/ first, so a
|
|
# bare `docker build` fails on that COPY by design.
|
|
#
|
|
# every FROM is pinned by digest, and the third-party binaries (caddy,
|
|
# tini) are downloaded at pinned versions and checksum-verified.
|
|
#
|
|
# the runtime ships no node_modules at all — auth-realtime and the
|
|
# launcher are single esbuild bundles and the web server is nitro's
|
|
# self-contained output — so the runtime libc (musl) cannot clash with
|
|
# anything; the web builder still guards against a native addon ever
|
|
# sneaking into the output.
|
|
#
|
|
# the image is published for linux/amd64 and linux/arm64. Every stage but
|
|
# the last runs on the build machine's own architecture ($BUILDPLATFORM)
|
|
# whatever the target: their output is JavaScript, or binaries downloaded
|
|
# for the target, so a multi-arch build produces it once instead of again
|
|
# under emulation. That holds only while nothing a builder executes lands
|
|
# in its output.
|
|
|
|
# mirrors web/Dockerfile.dev (the dev/e2e image); change them together.
|
|
FROM --platform=$BUILDPLATFORM node:24.20.0-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS web-builder
|
|
|
|
RUN npm install -g pnpm@11.1.3
|
|
|
|
WORKDIR /app
|
|
|
|
# the vite client build exceeds node's default heap.
|
|
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
|
|
# dependency layer that survives ordinary source edits. Scripts are skipped
|
|
# because the root prepare builds the promql grammar and runs nuxt prepare,
|
|
# and neither has its sources yet.
|
|
COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml ./
|
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
|
|
COPY web/ .
|
|
|
|
# the second install only runs the previously skipped scripts. The find
|
|
# guard fails the build if a native addon ever lands in the output — the
|
|
# runtime is musl and a glibc .node file would crash at boot instead.
|
|
RUN pnpm install --frozen-lockfile \
|
|
&& NITRO_PRESET=node-server NITRO_MINIFY=true pnpm run build:web \
|
|
&& test -z "$(find .output -name '*.node' -print -quit)"
|
|
|
|
# the runtime ships the package's own single-file esbuild bundle (see
|
|
# build:bundle and src/bundle.ts — the same artifact Dockerfile.dev and
|
|
# the e2e stack run) instead of the ~83MB production node_modules tree.
|
|
FROM --platform=$BUILDPLATFORM node:24.20.0-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS auth-realtime-builder
|
|
|
|
RUN npm install -g pnpm@11.1.3
|
|
|
|
WORKDIR /server
|
|
|
|
COPY server/auth-realtime/package.json server/auth-realtime/pnpm-lock.yaml server/auth-realtime/pnpm-workspace.yaml server/auth-realtime/tsconfig.json server/auth-realtime/tsconfig.build.json ./
|
|
COPY server/auth-realtime/src ./src
|
|
|
|
# check-types gates (esbuild does not look at types); the import proves
|
|
# the bundle loads — with no environment set it must reject with its own
|
|
# validation error, so an unresolvable import fails the build here rather
|
|
# than when the container starts.
|
|
RUN pnpm install --frozen-lockfile && pnpm run check-types && pnpm run build:bundle \
|
|
&& node -e "import('/server/dist/bundle.mjs').then(() => { console.error('bundle skipped env validation'); process.exit(1) }, (err) => { if (!String(err).includes('invalid environment')) { console.error(err); process.exit(1) } })"
|
|
|
|
FROM --platform=$BUILDPLATFORM node:24.20.0-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS launcher-builder
|
|
|
|
RUN npm install -g pnpm@11.1.3
|
|
|
|
WORKDIR /launcher
|
|
|
|
COPY docker/prod/launcher/package.json docker/prod/launcher/pnpm-lock.yaml docker/prod/launcher/pnpm-workspace.yaml docker/prod/launcher/tsconfig.json ./
|
|
RUN pnpm install --frozen-lockfile
|
|
|
|
COPY docker/prod/launcher/src ./src
|
|
|
|
# the central telemetry DSNs, baked at build time only — empty for local
|
|
# builds (sentry disabled), set by the release pipeline. There is no
|
|
# runtime override; OXYNOTE_ERROR_REPORTING_DISABLED is the off switch.
|
|
# The args reach build:bundle as environment variables, where the
|
|
# package's own script turns them into esbuild defines.
|
|
ARG SENTRY_WEB_DSN=""
|
|
ARG SENTRY_CORE_DSN=""
|
|
ARG SENTRY_AUTH_REALTIME_DSN=""
|
|
ARG SENTRY_LAUNCHER_DSN=""
|
|
|
|
RUN pnpm run check-types && pnpm run build:bundle
|
|
|
|
# caddy and tini at pinned versions, checksum-verified before use. Both
|
|
# licenses ship in the final image: caddy's Apache-2.0 rides in its release
|
|
# tarball, tini's MIT is fetched from the same tag as its binary. The name
|
|
# "Caddy" is trademarked; the binary is bundled unmodified and unrebranded.
|
|
# Each architecture pins its own checksums; a version bump updates them all.
|
|
FROM --platform=$BUILDPLATFORM node:24.20.0-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS fetch
|
|
|
|
ARG CADDY_VERSION=2.11.4
|
|
ARG CADDY_SHA512_AMD64=8220d1f013b6f27510247b2360c9e0ca9f018feebd82515f07635318b34ff9777ccc8fd0b6e6f2486ce3a33fe389fbb7db12d05baa474f4587509fb4f5ebf1c9
|
|
ARG CADDY_SHA512_ARM64=d5a7c423853c24a799765e0e8210d5c7c22a8f56ed37a3cae2fb9f58be138853c02b4efd6b59d576e6d8c7c0d30b9c1592deeaa6a536ff69bcca23b8c1ea709c
|
|
ARG TINI_VERSION=0.19.0
|
|
ARG TINI_SHA256_AMD64=c5b0666b4cb676901f90dfcb37106783c5fe2077b04590973b885950611b30ee
|
|
ARG TINI_SHA256_ARM64=eae1d3aa50c48fb23b8cbdf4e369d0910dfc538566bfd09df89a774aa84a48b9
|
|
ARG TARGETARCH
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /fetch
|
|
|
|
RUN case "${TARGETARCH}" in \
|
|
amd64) caddy_sha512="${CADDY_SHA512_AMD64}" tini_sha256="${TINI_SHA256_AMD64}" ;; \
|
|
arm64) caddy_sha512="${CADDY_SHA512_ARM64}" tini_sha256="${TINI_SHA256_ARM64}" ;; \
|
|
*) echo "no pinned checksums for ${TARGETARCH}" >&2; exit 1 ;; \
|
|
esac \
|
|
&& curl -fsSLo caddy.tar.gz "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_${TARGETARCH}.tar.gz" \
|
|
&& echo "${caddy_sha512} caddy.tar.gz" | sha512sum -c - \
|
|
&& tar -xzf caddy.tar.gz caddy LICENSE \
|
|
&& mv LICENSE caddy-LICENSE \
|
|
&& curl -fsSLo tini "https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-${TARGETARCH}" \
|
|
&& echo "${tini_sha256} tini" | sha256sum -c - \
|
|
&& curl -fsSLo tini-LICENSE "https://raw.githubusercontent.com/krallin/tini/v${TINI_VERSION}/LICENSE" \
|
|
&& chmod 0755 caddy tini
|
|
|
|
# the runtime node is alpine's own build — v24 linked against shared
|
|
# system libraries, roughly half the size of the official node binary.
|
|
# The base is digest-pinned; nodejs then tracks alpine 3.24's package
|
|
# repo, which only ships security bumps within the branch.
|
|
FROM alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
|
|
|
|
# the exact pins make the build fail — instead of silently shifting —
|
|
# when alpine's branch rotates a package (they keep only the newest build
|
|
# per branch); bump the ARGs together with the base digest, which a
|
|
# dependabot bump of the digest alone cannot do. icu-data-full replaces the
|
|
# English-only default so Intl formats every locale during SSR, and
|
|
# postgresql18 is the embedded database, with pg_dump among its tools. Its
|
|
# major is part of the package name and of the launcher's
|
|
# /usr/libexec/postgresql18 paths: a data directory only opens under the
|
|
# major that wrote it, so a major bump needs an upgrade path for existing
|
|
# volumes, not only a new pin.
|
|
ARG NODE_APK_VERSION=24.18.1-r0
|
|
ARG ICU_DATA_APK_VERSION=78.1-r0
|
|
ARG POSTGRESQL_APK_VERSION=18.6-r0
|
|
|
|
# the uid is explicit and never changes: the data volume's files are owned
|
|
# by it, so a shift between releases would lock an existing deployment out
|
|
# of its own secrets. The three directories are the volume mount point
|
|
# (which the launcher writes generated secrets into), the mount point for
|
|
# a GitHub App private key, and caddy's scratch dir.
|
|
RUN apk add --no-cache "nodejs=${NODE_APK_VERSION}" "icu-data-full=${ICU_DATA_APK_VERSION}" "postgresql18=${POSTGRESQL_APK_VERSION}" \
|
|
&& adduser -D -H -u 65532 oxynote \
|
|
&& mkdir -p /oxynote/data /oxynote/github /tmp/caddy \
|
|
&& chown -R oxynote /oxynote /tmp/caddy
|
|
|
|
COPY --from=fetch /fetch/tini /tini
|
|
COPY --from=fetch /fetch/caddy /usr/local/bin/caddy
|
|
COPY --from=fetch /fetch/caddy-LICENSE /oxynote/licenses/caddy/LICENSE
|
|
COPY --from=fetch /fetch/tini-LICENSE /oxynote/licenses/tini/LICENSE
|
|
|
|
# staged per architecture by `make prod-build` and `make prod-publish` from
|
|
# goreleaser's output.
|
|
ARG TARGETARCH
|
|
COPY docker/prod/.build/${TARGETARCH}/oxynote-core /oxynote/core/server
|
|
|
|
COPY --from=web-builder /app/.output /oxynote/web
|
|
COPY --from=auth-realtime-builder /server/dist/bundle.mjs /oxynote/auth-realtime/index.mjs
|
|
COPY --from=launcher-builder /launcher/dist/launcher.mjs /oxynote/prod/launcher.mjs
|
|
COPY docker/prod/healthcheck.mjs /oxynote/prod/healthcheck.mjs
|
|
COPY docker/prod/Caddyfile /oxynote/prod/Caddyfile
|
|
COPY docker/prod/pg_hba.conf /oxynote/prod/pg_hba.conf
|
|
|
|
# what ghcr reads to link the package to this repository, and what a
|
|
# scanner reads to tell one build from another. IMAGE_VERSION and
|
|
# IMAGE_REVISION come from `make prod-publish`; a local build leaves them
|
|
# at dev/unknown.
|
|
ARG IMAGE_VERSION=dev
|
|
ARG IMAGE_REVISION=unknown
|
|
LABEL org.opencontainers.image.title="Oxynote" \
|
|
org.opencontainers.image.description="Collaborative documentation, in one container" \
|
|
org.opencontainers.image.source="https://github.com/oxynote/oxynote" \
|
|
org.opencontainers.image.url="https://github.com/oxynote/oxynote" \
|
|
org.opencontainers.image.documentation="https://github.com/oxynote/oxynote/blob/main/docker/prod/README.md" \
|
|
org.opencontainers.image.licenses="Apache-2.0" \
|
|
org.opencontainers.image.vendor="Oxynote" \
|
|
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
|
org.opencontainers.image.revision="${IMAGE_REVISION}"
|
|
|
|
ENV NODE_ENV=production
|
|
ENV HOME=/oxynote
|
|
|
|
USER oxynote
|
|
|
|
# generated secrets persist here even when no volume is mounted explicitly.
|
|
VOLUME /oxynote/data
|
|
|
|
EXPOSE 8080
|
|
|
|
HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start-period=180s \
|
|
CMD ["/usr/bin/node", "/oxynote/prod/healthcheck.mjs"]
|
|
|
|
ENTRYPOINT ["/tini", "--", "/usr/bin/node", "/oxynote/prod/launcher.mjs"]
|