-
-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Expand file tree
/
Copy pathDockerfile.production
More file actions
137 lines (114 loc) · 6.68 KB
/
Copy pathDockerfile.production
File metadata and controls
137 lines (114 loc) · 6.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# syntax=docker/dockerfile:1-labs@sha256:7d49dad25a050e14338ba7028b0460243f9d911dedc160a8fe20c34738fef3af
# Production Dockerfile for Ghost
# Targets:
# deploy — build the prod dependency closure + a self-contained /home/ghost
# (build-only stage, never shipped)
# core — server + production deps, no admin (Ghost-Pro base)
# full — core + built admin (self-hosting)
#
# Build context: the monorepo repo root. The deploy stage runs `pnpm deploy`
# against the workspace to produce a self-contained app dir (source + resolved
# node_modules), so this image no longer depends on the `pnpm pack` output that
# Ghost-CLI uses (scripts/pack.mjs). The `full` stage's admin build is injected
# into the context by CI at core/built/admin.
#
# Ownership model: application code (node_modules, server source, admin) is owned
# by nobody:nogroup so the runtime user (ghost, uid 1000) can read but not modify
# it; the writable content and log dirs are owned by ghost. Ownership is set at
# COPY time (COPY --chown) rather than via a post-hoc `chown -R`: on overlayfs a
# recursive chown copies every touched file into a new layer, which previously
# duplicated node_modules (~600MB) and the admin build (~80MB) and inflated every
# image pull.
ARG NODE_VERSION=22.23.1
# ---- deploy: build closure + self-contained app dir (build-only, not shipped) ----
FROM node:$NODE_VERSION-bookworm-slim AS deploy
WORKDIR /build
RUN corepack enable
# Full workspace source is needed so pnpm can resolve the workspace graph for the
# `ghost...` filter. .dockerignore prunes node_modules/build/dist/.git/.nx so the
# build starts from a clean tree and rebuilds outputs in-container.
#
# --exclude core/built: for the `full` target CI injects the admin build into the
# context at ghost/core/core/built/admin. Excluding the whole built dir (not just
# built/admin — the built dir itself is absent on a fresh checkout, so excluding
# only its child would still leave a differing directory entry) keeps this COPY
# layer's hash identical between the core and full builds. That lets `full` reuse
# `core`'s cached deploy/install/build layers instead of re-running the whole
# closure build, and keeps admin out of the deploy output so `core` stays
# admin-free. The server's own core/built output is regenerated in-container by the
# build steps below, so nothing is lost by excluding the host copy.
COPY --exclude=ghost/core/core/built . .
# Install only ghost and its dependency subgraph (`ghost...`), not the whole
# monorepo (admin, apps, ...). Dev deps are included — tsc etc. are needed to build
# the closure. Scripts are NOT ignored: better-sqlite3's install script runs and
# prebuild-install fetches its prebuilt binary (permitted by allowBuilds in
# pnpm-workspace.yaml). No build-essential/python3 is installed, so a node-gyp
# source-compile fallback fails loudly instead of silently pulling a toolchain into
# a discarded layer.
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,id=pnpm-store \
pnpm install --filter "ghost..." --frozen-lockfile --prefer-offline
# Build ghost's production workspace dependency closure (kg-*/adapters; @tryghost/i18n
# has no build script and is skipped) plus ghost's own server output. Driven with
# pnpm recursive filters rather than nx to avoid the NX_NATIVE_COMMAND_RUNNER
# segfault workaround inside the container.
RUN pnpm --filter-prod "ghost^..." -r run build && \
pnpm --filter ghost run build:tsc && \
pnpm --filter ghost run build:assets
# Produce a self-contained deploy dir: ghost/core's `files` set + a fully resolved,
# production-only node_modules (--prod drops devDeps, keeps optional better-sqlite3).
# Admin is never built here, so core/built/admin is absent — the core image stays
# admin-free and the full stage injects admin from the build context.
#
# inject-workspace-packages: hard-copy the workspace deps (kg-*/adapters/i18n) into
# node_modules instead of linking them back to the workspace. The deploy dir is
# COPYed into the runtime image away from the workspace, so links would dangle.
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,id=pnpm-store \
pnpm --filter=ghost --config.inject-workspace-packages=true deploy --prod /home/ghost
# Fail the build now if the native module didn't install correctly (missing/broken
# prebuilt binary) rather than at container runtime.
RUN cd /home/ghost && node -e "require('better-sqlite3'); console.log('better-sqlite3 OK')"
# ---- Core: server + production deps ----
FROM node:$NODE_VERSION-bookworm-slim AS core
ENV NODE_ENV=production
ENV NODE_COMPILE_CACHE=/home/ghost/.compile-cache
RUN apt-get update && \
apt-get install -y --no-install-recommends libjemalloc2 fontconfig && \
rm -rf /var/lib/apt/lists/* && \
groupmod -g 1001 node && \
usermod -u 1001 node && \
adduser --disabled-password --gecos "" -u 1000 ghost
WORKDIR /home/ghost
# The deploy stage produced a self-contained app dir (server source + resolved
# node_modules, no admin). Copy it once with its final ownership — no separate
# `chown -R` layer, so node_modules isn't duplicated in the image.
COPY --chown=nobody:nogroup --from=deploy /home/ghost ./
# cp -a preserves the nobody:nogroup ownership set at COPY time, so base_content
# and default need no re-chown; only the writable content/log dirs flip to ghost.
RUN mkdir -p default log && \
cp -a content base_content && \
cp -a content/themes/casper default/casper && \
([ -d content/themes/source ] && cp -a content/themes/source default/source || true) && \
chown ghost:ghost /home/ghost && \
chown nobody:nogroup default && \
chown -R ghost:ghost content log
ARG GHOST_BUILD_VERSION=""
ENV GHOST_BUILD_VERSION=${GHOST_BUILD_VERSION}
USER ghost
ENV LD_PRELOAD=libjemalloc.so.2
# Boot once to warm Node's V8 compile cache — 15-20% off every container start.
# Must run here, as the runtime user: Node namespaces the cache directory by uid
# (and writes entries 0600), so a cache warmed by root in an earlier stage is one
# the runtime never looks at. Images built on top can re-run the script to cache
# their own code, as long as they do it as the same user.
RUN bin/warm-compile-cache.sh
EXPOSE 2368
CMD ["node", "index.js"]
# ---- Full: core + admin ----
FROM core AS full
# COPY --chown sets ownership in the copy layer; a post-hoc `chown -R` would
# duplicate the ~80MB admin build into a second layer. Admin is injected into the
# build context by CI (downloaded from the admin build job) at
# ghost/core/core/built/admin — the deploy stage excludes that path, so this is the
# only stage that carries admin. Local `full` builds must populate that path first
# (e.g. `pnpm nx run @tryghost/admin:build`).
COPY --chown=nobody:nogroup ghost/core/core/built/admin core/built/admin