Repository navigation
Expand file tree
/
Copy pathDockerfile-next.template
More file actions
159 lines (147 loc) · 6.54 KB
/
Copy pathDockerfile-next.template
File metadata and controls
159 lines (147 loc) · 6.54 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
{{
def is_alpine:
env.variant | startswith("alpine")
;
def clean_apt:
# TODO once bookworm is EOL, remove this and just hard-code "apt-get dist-clean" instead
if env.variant | contains("bookworm") then
"rm -rf /var/lib/apt/lists/*"
else "apt-get dist-clean" end
-}}
FROM {{ .variants[env.variant].from }}
# grab gosu for easy step-down from root (ahead of the Ghost install so it stays cached)
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION=1.19
RUN set -eux; \
{{ if is_alpine then ( -}}
apk add --no-cache --virtual .gosu-deps ca-certificates dpkg gnupg; \
{{ ) else ( -}}
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
# unlike the Alpine image, "node:*-slim" ships neither wget nor ca-certificates
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
{{ ) end -}}
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
{{ if is_alpine then ( -}}
apk del --no-network .gosu-deps; \
{{ ) else ( -}}
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
{{ clean_apt }}; \
{{ ) end -}}
\
chmod +x /usr/local/bin/gosu; \
gosu --version; \
gosu nobody true
# the node image claims uid/gid 1000; hand it to Ghost instead, keeping the uid the CLI-based image
# ran as so existing bind mounts still work. "deluser" exists on both bases, so this needs neither
# usermod/groupmod (absent on Alpine) nor a home dir -- the install below creates it.
RUN set -eux; \
deluser node; \
rm -rf /home/node; \
{{ if is_alpine then ( -}}
addgroup -g 1000 ghost; \
adduser -u 1000 -G ghost -h /home/ghost -s /bin/sh -H -D ghost
{{ ) else ( -}}
groupadd --gid 1000 ghost; \
useradd --uid 1000 --gid 1000 --home-dir /home/ghost --no-create-home --shell /bin/bash ghost
{{ ) end -}}
ENV NODE_ENV=production
ENV GHOST_INSTALL=/home/ghost
ENV GHOST_CONTENT=/home/ghost/content
ENV GHOST_VERSION={{ .version }}
# resolved by "versions.sh" so a build cannot pick up a different tarball than the one reviewed
ENV GHOST_TARBALL={{ .tarball.url }}
ENV GHOST_SHA256={{ .tarball.sha256 }}
RUN set -eux; \
{{ if is_alpine then "" else ( -}}
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates wget; \
\
{{ ) end -}}
# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one
corepack enable; \
\
# everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive
# chown would copy the whole tree into a new layer
mkdir -p "$GHOST_INSTALL"; \
chown ghost:ghost "$GHOST_INSTALL"; \
cd "$GHOST_INSTALL"; \
\
# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them out of
# the install dir, and they are removed below
export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \
\
gosu ghost wget -O ghost.tgz "$GHOST_TARBALL"; \
echo "$GHOST_SHA256 ghost.tgz" | sha256sum -c -; \
# the release tarball has no leading "package/" component, unlike an npm pack
gosu ghost tar --extract --file ghost.tgz; \
rm ghost.tgz; \
\
# the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build
gosu ghost pnpm install --prod --frozen-lockfile; \
\
# install-time inputs only: package.json points 18 deps at "file:components/*.tgz", but pnpm
# extracts them into its virtual store and nothing resolves back here afterwards
gosu ghost rm -rf "$GHOST_INSTALL/components"; \
\
# Ghost's own pruner, as used by its production image: drops dependency TypeScript, sourcemaps,
# READMEs and vendored C/C++ (keeping licences and prebuilt .node). The tarball already has the
# "archive" profile applied, so this is mostly node_modules. Node 22 strips the .mts types itself.
gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \
\
# make a config.json symlink for NODE_ENV=development (config.production.json is copied in below)
gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
\
# need to save initial content for pre-seeding empty volumes
gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \
gosu ghost mkdir -p "$GHOST_INSTALL/content"; \
\
# "corepack enable" above repointed the yarn symlinks at corepack, orphaning the standalone copy.
# This does not shrink the image -- those bytes live in a base layer and only get whited out -- but
# it keeps a dead tree out of the runtime filesystem
rm -rf /opt/yarn-*; \
rm -rf /tmp/xdg-cache /tmp/xdg-data; \
npm cache clean --force; \
\
{{ if is_alpine then "" else ( -}}
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
# node's image ships no CA bundle of its own, and the purge below would take the one installed
# above with it; Node has a bundled store, but anything shelling out needs the system one
apt-mark manual ca-certificates > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
{{ clean_apt }}; \
\
{{ ) end -}}
# test that the optional dependencies are installed and loadable
node --version; \
gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");'
# Ghost-CLI used to generate this. Everything in it is overridable via Ghost's "__" env vars, and
# "process" is deliberately absent -- it only ever told Ghost-CLI which process manager to use
COPY --chown=ghost:ghost config.production.json $GHOST_INSTALL/
RUN set -eux; \
# a mount point, so it stays writable whatever uid ends up owning what gets mounted over it
chmod 1777 "$GHOST_CONTENT"; \
cd "$GHOST_INSTALL"; \
node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \
[ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]
WORKDIR $GHOST_INSTALL
VOLUME $GHOST_CONTENT
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 2368
CMD ["node", "index.js"]