-
-
Notifications
You must be signed in to change notification settings - Fork 11.8k
Expand file tree
/
Copy pathDockerfile.production
More file actions
129 lines (107 loc) · 6.23 KB
/
Copy pathDockerfile.production
File metadata and controls
129 lines (107 loc) · 6.23 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
# 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
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
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