-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathDockerfile
More file actions
150 lines (129 loc) · 6.62 KB
/
Copy pathDockerfile
File metadata and controls
150 lines (129 loc) · 6.62 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
# Terrence container image.
#
# Builder: oven/bun:1 (byte-for-byte the bun that owns the committed bun.lock).
# Runtime: Chainguard Wolfi (glibc) — see below for why.
#
# Runtime base rationale (validated by prototype builds during the swap):
# - Near-zero CVEs out of the box: the previous Debian oven/bun:1-slim
# stage carried 145+ OS-package vulns (16 CRITICAL) with no published fix.
# Wolfi ships hardened packages (built from source, signature-verified,
# SLSA-buildable, cgr.dev SBOMs). Scanned image: 0 findings.
# - ~58% smaller: 173 MB (Debian) -> ~73 MB (Wolfi).
# - glibc runtime: matches Debian ABI, so on-demand tofu/terraform/infracost/
# opa Go binaries and the static C landlock runner behave identically
# (musl-Alpine needed extra libstdc++/libgcc handling and is not used here).
# - The pinned Bun binary comes from the builder stage; apk provides the
# remaining runtime tools without another downloader.
# - Runs as uid 65532 (nonroot) by default, matching the app's unprivileged
# Landlock sandbox model (no chroot, no capabilities).
#
# The runtime stage layout EXACTLY mirrors the pre-Wolfi layout so the workspace
# install resolves identically: root package.json + bun.lock (workspace context),
# backend only (NOT frontend — frontend dev tooling vite/rolldown/tsx would leak
# esbuild and add CVEs), then bun install --production --frozen-lockfile from
# /app/backend. This is what keeps frozen-lockfile succeeding and esbuild out.
# terraform-config-inspect is feature-complete but does not publish release
# binaries, so build the pinned upstream revision once and keep Go out of the
# runtime image.
FROM golang:1.27-bookworm@sha256:648f440f42a0958804efb24df176f806f9d353b41f1c0627f666428e40310f6b AS config-inspect-builder
RUN CGO_ENABLED=0 GOBIN=/out go install github.com/hashicorp/terraform-config-inspect@v0.0.0-20260709150029-2fb54c236733
# ---------- Build stage: Bun backend workspaces + frontend + static landlock ---
FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895 AS builder
WORKDIR /app
# Copy dependency manifests first for optimal Docker layer caching
COPY bunfig.toml ./
COPY bun.lock ./
COPY package.json ./
COPY backend/package.json ./backend/
COPY frontend/package.json ./frontend/
# Install toolchain first for optimal cache: any source change won't re-download gcc
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies (workspaces)
RUN bun install --frozen-lockfile
# Copy the rest of the monorepo
COPY . .
# Build frontend
WORKDIR /app/frontend
RUN bun run build
# Compile the static Landlock runner (needs a C toolchain; the final image
# does not ship one). Static glibc binary -> runs identically on any base.
WORKDIR /app
RUN backend/bin/build-landlock-runner.sh
# ---------- Runtime: Chainguard Wolfi (glibc, near-zero CVE) ----------
# Base pinned to an immutable digest (reviewed/immutable supply chain).
# Tag at pin time: cgr.dev/chainguard/wolfi-base:latest. Bump deliberately
# after reviewing what changed in the new tag (pin the new digest).
FROM cgr.dev/chainguard/wolfi-base@sha256:e624c5d5e42382ce7165ddafcbbf8e6769a24cbd02ea6114b880b05ae5ba2a8d
ARG BUILD_VERSION=0.0.0
ARG BUILD_SHA=unknown
LABEL org.opencontainers.image.title="Terrence" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="${BUILD_SHA}" \
org.opencontainers.image.source="https://github.com/essinghigh-org/terrence"
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000 \
STORAGE_DIR=/app/backend/storage \
INFRACOST_ENABLED=false \
INFRACOST_VERSION=0.10.45 \
BUILD_VERSION=${BUILD_VERSION} \
BUILD_SHA=${BUILD_SHA}
# wolfi-base ships busybox (tar/cp/which), glibc, apk and ca-certificates-bundle.
# Add the external tools the worker shells out to at runtime. Copy the exact Bun
# binary that built the application to a world-executable runtime path.
#
# Infracost is intentionally NOT baked into the image: it is installed on demand
# at runtime into <storage>/binaries/infracost/<version>/ (digest-verified) by
# backend/src/lib/infracost-bin.ts, selected by INFRACOST_VERSION. Baking it was
# the single remaining CVE surface in the image and forced a rebuild to bump the
# version; managing it like tofu/terraform removes both.
COPY --from=builder /usr/local/bin/bun /usr/bin/bun
RUN apk add --no-cache \
git \
unzip \
wget \
curl \
ca-certificates-bundle \
&& (git config --system init.defaultBranch main 2>/dev/null || true)
# Workspace root + backend ONLY (no frontend -> no esbuild/vite/rolldown dev
# tooling). Mirrors the previous runtime COPY set exactly.
COPY bunfig.toml ./
COPY bun.lock ./
COPY package.json ./
COPY backend/package.json ./backend/
COPY backend/drizzle.config.ts ./backend/
COPY backend/drizzle ./backend/drizzle
COPY backend/index.ts ./backend/
COPY backend/openapi.json ./backend/
COPY backend/src ./backend/src
COPY backend/docs ./backend/docs
# Operational scripts (doctor, tfectl) so containerized installs can run the
# documented diagnostics entry point without a source checkout (issue #593).
COPY backend/scripts ./backend/scripts
# landlock-runner is compiled to a static glibc binary in the builder stage.
COPY --from=builder /app/backend/bin/landlock-runner ./backend/bin/landlock-runner
COPY --from=config-inspect-builder /out/terraform-config-inspect ./backend/bin/terraform-config-inspect
# Install production dependencies for backend, inside the workspace context,
# exactly as before. Frozen-lockfile resolves because root package.json
# (workspaces) + bun.lock are present and the frontend is absent.
WORKDIR /app/backend
RUN bun install --production --frozen-lockfile && \
(rm -rf /root/.bun/install/cache 2>/dev/null || true)
# Copy built frontend static assets
COPY --from=builder /app/frontend/dist /app/frontend/dist
# Create storage directory. Wolfi images run as uid 65532 (nonroot); the Landlock
# sandbox needs no capabilities, so keep that user and grant it only the storage
# dir.
RUN mkdir -p /app/backend/storage && \
chown -R 65532:65532 /app/backend/storage
VOLUME ["/app/backend/storage"]
# Backend is the app entry; run from /app/backend so `bun run index.ts` resolves
# the backend entry.
WORKDIR /app/backend
USER 65532:65532
# Expose the API/UI
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD /usr/bin/bun -e 'fetch("http://127.0.0.1:" + (process.env.PORT || "3000") + "/readyz").then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))'
CMD ["/usr/bin/bun", "run", "index.ts"]