forked from torrust/torrust-index
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathContainerfile
More file actions
358 lines (327 loc) · 17.3 KB
/
Containerfile
File metadata and controls
358 lines (327 loc) · 17.3 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# syntax=docker/dockerfile:latest
# Torrust Index
## Builder Image
FROM rust:trixie AS chef
WORKDIR /tmp
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/v1.18.1/install-from-binstall-release.sh | bash
RUN cargo binstall --no-confirm --locked cargo-chef cargo-nextest
## Tester Image
FROM rust:slim-trixie AS tester
WORKDIR /tmp
RUN apt-get update; apt-get install -y curl sqlite3; apt-get autoclean
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/v1.18.1/install-from-binstall-release.sh | bash
RUN cargo binstall --no-confirm --locked cargo-nextest imdl
COPY ./share/ /app/share/torrust
RUN mkdir -p /app/share/torrust/default/database/; \
sqlite3 /app/share/torrust/default/database/index.sqlite3.db "VACUUM;"
## jq donor (pristine base, no user code)
#
# Debian trixie's `jq` package is dynamically linked against
# libjq and libonig. The distroless runtime does not ship
# either, so we stage them here at deterministic paths
# alongside the binary, and the runtime stages copy all
# three artefacts (binary + two shared libs).
#
# The `*-linux-gnu` glob resolves to whichever multi-arch
# tuple the donor was built for; we re-stage under stable
# names (`/jq/...`) so the runtime COPY directives don't
# need to know the tuple.
FROM rust:slim-trixie AS jq_donor
RUN apt-get update && \
apt-get install -y --no-install-recommends jq && \
rm -rf /var/lib/apt/lists/*
# Pin jq's runtime shared-library set so a future donor-base
# upgrade that drags in a new transitive dep fails the build
# instead of producing a silently-broken runtime image. The
# allow-list mirrors what the runtime stages COPY across
# (libjq, libonig) plus libraries already present in the
# `cc-debian13` runtime base (libc, libm, ld-linux, vdso).
RUN set -eu; \
expected='libc.so.6 libjq.so.1 libm.so.6 libonig.so.5 ld-linux-x86-64.so.2 linux-vdso.so.1'; \
# ldd column 1 is sometimes a bare soname ("libc.so.6")
# and sometimes an absolute path ("/lib64/ld-linux-…").
# Reduce to basenames so the allow-list check is uniform.
actual="$(ldd /usr/bin/jq | awk '{print $1}' | sed 's|.*/||' | sort -u | tr '\n' ' ')"; \
for lib in $expected; do \
case " $actual " in *" $lib "*) ;; *) echo "ERROR: jq lost expected library: $lib" >&2; exit 1 ;; esac; \
done; \
for lib in $actual; do \
case " $expected " in *" $lib "*) ;; *) echo "ERROR: jq pulls unexpected library: $lib (allow-list: $expected)" >&2; exit 1 ;; esac; \
done
# Stage jq + its two non-glibc shared libraries at
# deterministic paths under `/jq/`. The `*-linux-gnu` glob
# resolves to whichever multi-arch tuple the donor was
# built for; re-staging under stable names lets the runtime
# COPY directives stay tuple-agnostic.
RUN mkdir -p /jq && \
cp /usr/bin/jq /jq/jq && \
cp -L /usr/lib/*-linux-gnu/libjq.so.1 /jq/libjq.so.1 && \
cp -L /usr/lib/*-linux-gnu/libonig.so.5 /jq/libonig.so.5
## Su Exe Compile
FROM docker.io/library/gcc:trixie AS gcc
COPY ./contrib/dev-tools/su-exec/ /usr/local/src/su-exec/
RUN cc -Wall -Werror -g /usr/local/src/su-exec/su-exec.c -o /usr/local/bin/su-exec; chmod +x /usr/local/bin/su-exec
## Chef Prepare (look at project and see what we need)
FROM chef AS recipe
WORKDIR /build/src
COPY . /build/src
RUN cargo chef prepare --recipe-path /build/recipe.json
## Cook (debug)
FROM chef AS dependencies_debug
WORKDIR /build/src
COPY --from=recipe /build/recipe.json /build/recipe.json
RUN cargo chef cook --workspace --all-targets --all-features --recipe-path /build/recipe.json
RUN cargo nextest archive --workspace --all-targets --all-features --archive-file /build/temp.tar.zst ; rm -f /build/temp.tar.zst
## Cook (release)
FROM chef AS dependencies
WORKDIR /build/src
COPY --from=recipe /build/recipe.json /build/recipe.json
RUN cargo chef cook --workspace --all-targets --all-features --recipe-path /build/recipe.json --release
RUN cargo nextest archive --workspace --all-targets --all-features --archive-file /build/temp.tar.zst --release ; rm -f /build/temp.tar.zst
## Build Archive (debug)
FROM dependencies_debug AS build_debug
WORKDIR /build/src
COPY . /build/src
RUN cargo nextest archive --workspace --all-targets --all-features --archive-file /build/torrust-index-debug.tar.zst
## Build Archive (release)
FROM dependencies AS build
WORKDIR /build/src
COPY . /build/src
RUN cargo nextest archive --workspace --all-targets --all-features --archive-file /build/torrust-index.tar.zst --release
# Extract and Test (debug)
FROM tester AS test_debug
WORKDIR /test
COPY . /test/src/
COPY --from=build_debug \
/build/torrust-index-debug.tar.zst \
/test/torrust-index-debug.tar.zst
RUN cargo nextest run --workspace-remap /test/src/ --extract-to /test/src/ --no-run --archive-file /test/torrust-index-debug.tar.zst
RUN cargo nextest run --workspace-remap /test/src/ --target-dir-remap /test/src/target/ --cargo-metadata /test/src/target/nextest/cargo-metadata.json --binaries-metadata /test/src/target/nextest/binaries-metadata.json
RUN mkdir -p /app/bin/; \
cp -l /test/src/target/debug/torrust-index /app/bin/torrust-index; \
cp -l /test/src/target/debug/torrust-index-health-check /app/bin/torrust-index-health-check; \
cp -l /test/src/target/debug/torrust-index-auth-keypair /app/bin/torrust-index-auth-keypair; \
cp -l /test/src/target/debug/torrust-index-config-probe /app/bin/torrust-index-config-probe
# Phase 4: per-binary modes. Application binary stays
# world-executable; root-phase-only helpers (health-check,
# auth-keypair, config-probe) tighten to root-only
# (0500 root:root) per ADR-T-009 §D4 / §D5 — same posture as
# busybox, su-exec, jq. The healthcheck binary is invoked
# from HEALTHCHECK, which runs as root (no --user in the
# directive), so 0500 is sufficient. The config-probe is
# invoked only from the entry script's pre-su-exec phase,
# so it is also root-only.
RUN chown -R root:root /app; chmod -R u=rw,go=r,a+X /app; \
chmod 0755 /app/bin/torrust-index; \
chown 0:0 /app/bin/torrust-index-health-check \
/app/bin/torrust-index-auth-keypair \
/app/bin/torrust-index-config-probe; \
chmod 0500 /app/bin/torrust-index-health-check \
/app/bin/torrust-index-auth-keypair \
/app/bin/torrust-index-config-probe
# Extract and Test (release)
FROM tester AS test
WORKDIR /test
COPY . /test/src
COPY --from=build \
/build/torrust-index.tar.zst \
/test/torrust-index.tar.zst
RUN cargo nextest run --workspace-remap /test/src/ --extract-to /test/src/ --no-run --archive-file /test/torrust-index.tar.zst
RUN cargo nextest run --workspace-remap /test/src/ --target-dir-remap /test/src/target/ --cargo-metadata /test/src/target/nextest/cargo-metadata.json --binaries-metadata /test/src/target/nextest/binaries-metadata.json
RUN mkdir -p /app/bin/; \
cp -l /test/src/target/release/torrust-index /app/bin/torrust-index; \
cp -l /test/src/target/release/torrust-index-health-check /app/bin/torrust-index-health-check; \
cp -l /test/src/target/release/torrust-index-auth-keypair /app/bin/torrust-index-auth-keypair; \
cp -l /test/src/target/release/torrust-index-config-probe /app/bin/torrust-index-config-probe
# Phase 4: per-binary modes (see test_debug above for rationale).
# The healthcheck binary is invoked from HEALTHCHECK, which
# runs as root (no --user in the directive), so 0500 is
# sufficient. The config-probe (ADR-T-009 §D3) is invoked
# only from the entry script's pre-su-exec phase, so it is
# also root-only.
RUN chown -R root:root /app; chmod -R u=rw,go=r,a+X /app; \
chmod 0755 /app/bin/torrust-index; \
chown 0:0 /app/bin/torrust-index-health-check \
/app/bin/torrust-index-auth-keypair \
/app/bin/torrust-index-config-probe; \
chmod 0500 /app/bin/torrust-index-health-check \
/app/bin/torrust-index-auth-keypair \
/app/bin/torrust-index-config-probe
## ── Runtime asset bundle (base-agnostic) ─────────────────────
# The lean release base does not ship busybox at all; the
# :debug variant does. Use the :debug image as a "donor" we
# extract a single root-only busybox binary from for the
# release base.
FROM gcr.io/distroless/cc-debian13:debug AS busybox_donor
# Preflight: assert the donor still ships a real busybox ELF
# at the path we copy from, and that its `install` applet
# supports `-D` (the entry script's `inst()` helper depends on
# it). Cheap insurance against a future base reshuffle.
FROM busybox_donor AS busybox_preflight
RUN ["/busybox/sh", "-c", \
"test -f /busybox/busybox \
&& /busybox/busybox --help >/dev/null \
&& /busybox/busybox install --help 2>&1 | grep -q -- '-D'"]
# Minimal /etc account files generated locally so adduser
# works regardless of base. The base image's own
# /etc/nsswitch.conf (which includes `hosts: files dns`) is
# deliberately left alone — the application makes outbound
# connections where host-name resolution must work.
FROM busybox_preflight AS etc_seed
# /etc/profile is seeded as an empty file so the debug image's
# `ENV ENV=/etc/profile` points at a real path. Operators who
# bind-mount a richer profile over it get the expected
# behaviour; absent that, busybox `sh` sources an empty file
# and continues silently rather than warning on every
# invocation.
RUN ["/busybox/sh", "-c", \
"mkdir -p /seed/etc && \
printf 'root:x:0:0:root:/:/bin/sh\\n' > /seed/etc/passwd && \
printf 'root:x:0:\\n' > /seed/etc/group && \
: > /seed/etc/profile"]
# Preflight: assert `adduser -D` works without /etc/shadow.
# The entry script runs `adduser -D -s /bin/sh -u $USER_ID
# torrust` at first boot against the etc_seed layout (passwd
# + group, no shadow). Busybox adduser behaviour when shadow
# is absent varies by version; this stage catches regressions
# at build time rather than first boot.
FROM busybox_donor AS adduser_preflight
COPY --from=etc_seed /seed/etc/passwd /etc/passwd
COPY --from=etc_seed /seed/etc/group /etc/group
# Busybox `adduser` rejects UIDs outside 0..60000 (well below
# the conventional `nobody` value of 65534). Use a value in
# range that is still well above `USER_ID=1000` so the
# preflight cannot collide with a realistic runtime user.
RUN ["/busybox/sh", "-c", \
"/busybox/adduser -D -s /bin/sh -u 59999 testuser \
&& /busybox/grep -q '^testuser:' /etc/passwd \
&& /busybox/test -d /home/testuser"]
FROM scratch AS runtime_assets
COPY --from=etc_seed --chmod=0644 --chown=0:0 /seed/etc/ /etc/
# Single busybox binary, root-only. Copy the file directly
# (not via the /busybox/sh symlink) to avoid depending on the
# donor's symlink layout.
COPY --from=busybox_preflight --chmod=0700 --chown=0:0 \
/busybox/busybox /bin/busybox
COPY --from=gcc --chmod=0700 --chown=0:0 \
/usr/local/bin/su-exec /bin/su-exec
COPY --chmod=0555 --chown=0:0 \
./share/container/entry_script_sh /usr/local/bin/entry.sh
COPY --chmod=0444 --chown=0:0 \
./share/container/entry_script_lib_sh /usr/local/lib/torrust/entry_script_lib_sh
## ── Preflight gate (aggregates all donor-validation stages) ──
# Both runtime bases COPY from this stage, creating an
# explicit BuildKit dependency edge that prevents any
# preflight from being pruned regardless of which image
# variant is built.
FROM scratch AS preflight_gate
COPY --from=busybox_preflight /etc/passwd /tmp/.busybox-ok
COPY --from=adduser_preflight /etc/passwd /tmp/.adduser-ok
## ── Runtime base: release (root-only curated subset) ─────────
FROM gcr.io/distroless/cc-debian13 AS runtime_release
# Note: distroless cc-debian13 is usrmerged — `/bin` is a
# symlink to `/usr/bin`, so a recursive `COPY / /` from a
# stage that ships `/bin/` as a real directory fails with
# "cannot copy to non-directory". Copy each curated path
# individually instead so BuildKit resolves the symlink.
COPY --from=runtime_assets /etc/passwd /etc/passwd
COPY --from=runtime_assets /etc/group /etc/group
COPY --from=runtime_assets /etc/profile /etc/profile
COPY --from=runtime_assets /bin/busybox /usr/bin/busybox
COPY --from=runtime_assets /bin/su-exec /usr/bin/su-exec
COPY --from=runtime_assets /usr/local/bin/entry.sh /usr/local/bin/entry.sh
# `entry.sh` sources its pure-function library from this path
# at start-up; without it the container exits immediately with
# "can't open /usr/local/lib/torrust/entry_script_lib_sh".
# The debug runtime base copies the file directly from the
# build context (see further down); the release base goes via
# `runtime_assets` to keep the per-path copy list explicit.
COPY --from=runtime_assets /usr/local/lib/torrust/entry_script_lib_sh /usr/local/lib/torrust/entry_script_lib_sh
COPY --from=preflight_gate /tmp/.adduser-ok /tmp/.preflight-sentinel
# Pin PATH so a future base-image change cannot silently break
# the entry script's bare-name lookups.
ENV PATH=/usr/local/bin:/bin:/usr/bin:/sbin
# Materialise the curated applet set as symlinks to the
# single root-only busybox binary. Symlinks are created in
# `/usr/bin/` (the canonical usrmerged location); `/bin/<a>`
# resolves to the same path via the base's `/bin → /usr/bin`
# symlink. The applet list must match the curated applet
# reference in ADR-T-009 §D4 — update both in the same change.
RUN ["/usr/bin/busybox", "sh", "-c", \
"for a in sh adduser addgroup install mkdir dirname chown chmod tr mktemp cat printf rm echo grep; do \
/usr/bin/busybox ln -s busybox /usr/bin/$a; \
done && rm -f /tmp/.preflight-sentinel"]
## ── Runtime base: debug (full busybox on PATH) ───────────────
FROM gcr.io/distroless/cc-debian13:debug AS runtime_debug
COPY --from=etc_seed --chmod=0644 --chown=0:0 /seed/etc/ /etc/
COPY --from=preflight_gate /tmp/.adduser-ok /tmp/.preflight-sentinel
# Pull su-exec from runtime_assets (which already copies it
# from gcc with the correct mode/ownership) so there is a
# single source for the compiled binary regardless of base.
COPY --from=runtime_assets /bin/su-exec /bin/su-exec
COPY --chmod=0555 --chown=0:0 \
./share/container/entry_script_sh /usr/local/bin/entry.sh
COPY --chmod=0444 --chown=0:0 \
./share/container/entry_script_lib_sh /usr/local/lib/torrust/entry_script_lib_sh
# Materialise /bin/sh → /busybox/sh so root's recorded login
# shell in /etc/passwd resolves correctly. The release base
# creates the same symlink as part of its curated-applet
# loop; the debug base needs an explicit one because
# /bin/busybox doesn't exist here.
RUN ["/busybox/sh", "-c", \
"/busybox/ln -s /busybox/sh /bin/sh && rm -f /tmp/.preflight-sentinel"]
ENV PATH=/usr/local/bin:/busybox:/bin:/usr/bin:/sbin
## Torrust-Index (debug)
FROM runtime_debug AS debug
ENV TORRUST_INDEX_CONFIG_TOML_PATH=/etc/torrust/index/index.toml \
TORRUST_INDEX_DATABASE_DRIVER=sqlite3 \
USER_ID=1000 \
API_PORT=3001 \
IMPORTER_API_PORT=3002 \
TZ=Etc/UTC \
ENV=/etc/profile \
RUNTIME=debug
EXPOSE 3001/tcp 3002/tcp
VOLUME ["/var/lib/torrust/index","/var/log/torrust/index","/etc/torrust/index"]
COPY --from=test_debug /app/ /usr/
# jq binary for entry-script JSON consumption (§2.2 step 4).
# Root-only (0500) — same posture as busybox and su-exec.
# The two shared libraries (libjq, libonig) are required at
# runtime; distroless cc-debian13's ld.so resolves them via
# the multiarch dir registered in /etc/ld.so.conf.d/.
COPY --from=jq_donor --chmod=0500 --chown=0:0 /jq/jq /usr/bin/jq
COPY --from=jq_donor --chmod=0444 --chown=0:0 /jq/libjq.so.1 /usr/lib/x86_64-linux-gnu/libjq.so.1
COPY --from=jq_donor --chmod=0444 --chown=0:0 /jq/libonig.so.5 /usr/lib/x86_64-linux-gnu/libonig.so.5
ENTRYPOINT ["/usr/local/bin/entry.sh"]
HEALTHCHECK --interval=5s --timeout=5s --start-period=3s --retries=3 \
CMD /usr/bin/torrust-index-health-check "http://localhost:${API_PORT}/health_check" \
&& /usr/bin/torrust-index-health-check "http://localhost:${IMPORTER_API_PORT}/health_check"
# Default CMD matches release so the debug image is a drop-in
# replacement; operators can override with `sh` (or any other
# applet on PATH) at `docker run` / compose time.
CMD ["/usr/bin/torrust-index"]
## Torrust-Index (release) (default)
FROM runtime_release AS release
ENV TORRUST_INDEX_CONFIG_TOML_PATH=/etc/torrust/index/index.toml \
TORRUST_INDEX_DATABASE_DRIVER=sqlite3 \
USER_ID=1000 \
API_PORT=3001 \
IMPORTER_API_PORT=3002 \
TZ=Etc/UTC \
RUNTIME=release
EXPOSE 3001/tcp 3002/tcp
VOLUME ["/var/lib/torrust/index","/var/log/torrust/index","/etc/torrust/index"]
COPY --from=test /app/ /usr/
# jq binary for entry-script JSON consumption (§2.2 step 4).
# Root-only (0500) — same posture as busybox and su-exec.
# The two shared libraries (libjq, libonig) are required at
# runtime; distroless cc-debian13's ld.so resolves them via
# the multiarch dir registered in /etc/ld.so.conf.d/.
COPY --from=jq_donor --chmod=0500 --chown=0:0 /jq/jq /usr/bin/jq
COPY --from=jq_donor --chmod=0444 --chown=0:0 /jq/libjq.so.1 /usr/lib/x86_64-linux-gnu/libjq.so.1
COPY --from=jq_donor --chmod=0444 --chown=0:0 /jq/libonig.so.5 /usr/lib/x86_64-linux-gnu/libonig.so.5
ENTRYPOINT ["/usr/local/bin/entry.sh"]
HEALTHCHECK --interval=5s --timeout=5s --start-period=3s --retries=3 \
CMD /usr/bin/torrust-index-health-check "http://localhost:${API_PORT}/health_check" \
&& /usr/bin/torrust-index-health-check "http://localhost:${IMPORTER_API_PORT}/health_check"
CMD ["/usr/bin/torrust-index"]