-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.prod
More file actions
147 lines (127 loc) · 7.37 KB
/
Copy pathDockerfile.prod
File metadata and controls
147 lines (127 loc) · 7.37 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
# reqmesh production Docker image
# Multi-stage: builds the React frontend, then serves everything from the
# FastAPI backend on a single origin — no Vite dev server, no CORS issues.
# ═══ Stage 1 — build frontend ═══════════════════════════════════════════════════
FROM node:20-slim AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# ═══ Stage 2 — backend + built frontend ════════════════════════════════════════
FROM python:3.12-slim
# OCI provenance labels. VERSION and REVISION are injected at build time by
# release.yml from the git tag and commit — never hardcoded here, so a release
# cannot ship a stale version label (a literal string would be a fourth file
# set_version.py cannot see). The `dev`/`unknown` defaults only ever appear on
# local or CI-only builds that are never published.
ARG VERSION=dev
ARG REVISION=unknown
LABEL org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.source="https://github.com/CallumNunesVaz/reqmesh" \
org.opencontainers.image.licenses="GPL-3.0-or-later"
# apt-get upgrade: the base tag is only rebuilt periodically, so between
# rebuilds it carries known-vulnerable system packages that Debian has already
# patched. Installing on top of it without upgrading bakes those in for the
# life of the image. This is what the trivy job flags.
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
git \
`# openssh-client: git only *Recommends* it, and --no-install-recommends` \
`# drops it — so ssh was absent and every git@/ssh:// remote failed with` \
`# "ssh: not found", despite both being in ALLOWED_REMOTE_SCHEMES and` \
`# DEPLOYMENT.md telling operators to mount a key. Also provides` \
`# ssh-keygen for managed deploy keys.` \
openssh-client \
libglib2.0-0 \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libharfbuzz0b \
libharfbuzz-subset0 \
fonts-dejavu-core \
curl `# healthcheck + tectonic download` \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# tectonic: self-contained LaTeX engine for the primary (LaTeX) PDF report path.
# Pinned to a release tarball and verified against a recorded SHA-256; the old
# installer piped an unpinned remote script straight into a shell. Architecture
# is x86_64 only — there is no arm64 build today.
ARG TECTONIC_VERSION=0.17.0
ARG TECTONIC_SHA256=1a715688baf591e650c8aeb160ae934e181685eecbb38b317de30b269ac5d606
RUN curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/tectonic.tar.gz \
"https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40${TECTONIC_VERSION}/tectonic-${TECTONIC_VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
&& echo "${TECTONIC_SHA256} /tmp/tectonic.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/tectonic.tar.gz -C /usr/local/bin tectonic \
&& rm /tmp/tectonic.tar.gz
# The cache lives in the *image*, not on the /data volume.
#
# It used to point at /data/.tectonic-cache, which looks sensible — download
# once, survive restarts — but /data is the bind-mounted volume, so:
#
# * a fresh install starts cold and has to fetch TeX packages during the
# user's first export, inside the compile timeout, or silently fall back;
# * an air-gapped install can never fetch them at all, so LaTeX reports are
# permanently unavailable and nothing says so;
# * the image is not self-contained, despite shipping the engine.
#
# Baking the cache in makes rendering a property of the image rather than of
# the network at first use. Cost is ~52MB of cache (it was ~20MB when the
# packages were fetched lazily; pre-fetching the recorded working set trades
# that extra ~30MB for a build that does not depend on a remote host).
ENV TECTONIC_CACHE_DIR=/opt/tectonic-cache
WORKDIR /app
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ ./
COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
# The TeX bundle tectonic compiles against, fetched once and treated the same
# way as the binary above: a pinned URL plus a recorded SHA-256. tectonic
# normally downloads these files on demand from a bundle server *during* the
# compile below, which made the image build depend on a third-party host and
# fail intermittently (the warmer's retry loop could not help — every attempt
# hit the same host). Fetching it here, in its own layer, lets the warming
# compile run with the network switched off and fail only on real content
# problems.
#
# The full bundle is a ~2.9 GB indexed tarball, so fetch_tectonic_bundle.py
# pulls just the files the two reports need (the same set a network warm would
# download) and lays them out as the tectonic file cache, keyed to the same
# default bundle URL tectonic uses at runtime. The digest is the bundle's own
# SHA256SUM — the value tectonic itself uses to identify a bundle — recorded
# from the tlextras-2022.0r0.tar bundle that default_bundle_v33 (format version
# 33) resolves to for tectonic 0.17.0.
ARG TECTONIC_BUNDLE_URL=https://relay.fullyjustified.net/default_bundle_v33.tar
ARG TECTONIC_BUNDLE_SHA256=6ffe055852f8faf66c0acbe1a7fb27f87b869a90bad1204f3bf4d9683f597c7c
RUN TECTONIC_BUNDLE_URL="${TECTONIC_BUNDLE_URL}" \
TECTONIC_BUNDLE_SHA256="${TECTONIC_BUNDLE_SHA256}" \
python scripts/fetch_tectonic_bundle.py
# Fetch every TeX package a real report needs, now, and fail the build if a
# report cannot be produced. The script compiles an actual report from the
# seeded demo project rather than a stand-in document, so the warm cache cannot
# drift out of step with the preamble the publisher emits.
#
# `--network=none` is the proof this whole change is for: the compile must
# succeed with networking switched off, because every TeX file it needs is now
# local (fetched in the layer above) and the cache is keyed to the default
# bundle URL tectonic would otherwise try to reach.
#
# The `rm` is not tidiness. Seeding the demo project takes file locks, which
# creates /tmp/reqmesh-locks owned by root (this stage runs as root). Baked into
# the image, that directory is unwritable by the `reqmesh` runtime user, and
# every write fails with EACCES the moment /tmp is not a fresh tmpfs.
RUN --network=none python scripts/warm_tectonic.py && rm -rf /tmp/reqmesh-locks
# GPLv3 §4: a copy of the Licence must travel with every conveyed copy of the
# Program, and an image pushed to a registry is a conveyed copy. The
# third-party notices ship alongside for the same reason.
COPY LICENSE THIRD_PARTY_NOTICES.md /app/
RUN mkdir -p /data/projects && \
groupadd -r reqmesh && useradd -r -g reqmesh -d /app reqmesh && \
chown -R reqmesh:reqmesh /app /data /opt/tectonic-cache && \
git config --system user.email "reqmesh@localhost" && \
git config --system user.name "reqmesh" && \
git config --system init.defaultBranch main
EXPOSE 8000
USER reqmesh
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"]