2026-06-30 16:02:55 +02:00
|
|
|
# ── Stage 1: install workspace deps (runs natively on build host) ──────────────
|
|
|
|
|
FROM oven/bun:1.3.14 AS deps
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
# nodejs/python3/make/g++ are needed to compile better-sqlite3 (camoufox-js dep)
|
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
|
nodejs npm python3 make g++ \
|
|
|
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
|
|
|
COPY package.json bun.lock* ./
|
|
|
|
|
COPY packages/types/package.json ./packages/types/
|
|
|
|
|
COPY packages/browser/package.json ./packages/browser/
|
|
|
|
|
COPY packages/tiers/package.json ./packages/tiers/
|
|
|
|
|
COPY apps/api/package.json ./apps/api/
|
|
|
|
|
COPY apps/web/package.json ./apps/web/
|
|
|
|
|
COPY apps/docs/package.json ./apps/docs/
|
|
|
|
|
|
|
|
|
|
# --production skips devDependencies (vitepress + algolia from docs, typescript, @types)
|
2026-07-01 14:33:46 +02:00
|
|
|
# --linker=hoisted avoids Bun's default isolated-linker bugs that corrupt
|
|
|
|
|
# multi-stage Docker builds: transitive deps not symlinked (oven-sh/bun#23524,
|
|
|
|
|
# e.g. @sinclair/typebox via elysia) and a store-population race producing
|
|
|
|
|
# EISDIR on freshly-linked packages (oven-sh/bun#29489, e.g. camoufox-js).
|
2026-07-06 23:41:53 +02:00
|
|
|
# The `--omit=dev` flag is the modern spelling — `--production` alone leaves workspace
|
|
|
|
|
# devDeps installed (bun 1.3.x quirk). Prune better-sqlite3 sources + unused impit
|
|
|
|
|
# platform binaries inside this stage so the subsequent COPY --from=deps in stage 3
|
|
|
|
|
# pulls a smaller layer (Docker layers are unions, not diffs — can't shrink post-COPY).
|
|
|
|
|
RUN bun install --frozen-lockfile --production --omit=dev --linker=hoisted \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*/deps' -prune -exec rm -rf {} + \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*/src' -prune -exec rm -rf {} + \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*' -name '*.md' -delete \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*' -name '*.gyp' -delete \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*' -name '*.c' -delete \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*' -name '*.h' -delete \
|
|
|
|
|
&& find node_modules -path '*/better-sqlite3*' -name '*.map' -delete \
|
|
|
|
|
&& rm -rf node_modules/typescript node_modules/bun-types node_modules/@types \
|
|
|
|
|
&& rm -rf node_modules/impit-linux-arm64-musl \
|
|
|
|
|
node_modules/impit-linux-x64-musl \
|
|
|
|
|
node_modules/impit-darwin-* \
|
|
|
|
|
node_modules/impit-win32-*
|
2026-06-30 16:02:55 +02:00
|
|
|
|
2026-07-06 23:10:16 +02:00
|
|
|
# ── Stage 2: fetch the Camoufox Firefox binary (pinned version) ─────
|
2026-06-30 16:02:55 +02:00
|
|
|
FROM oven/bun:1.3.14 AS camoufox
|
|
|
|
|
ENV CAMOUFOX_INSTALL_DIR=/opt/camoufox
|
|
|
|
|
|
2026-08-10 02:49:33 +02:00
|
|
|
ARG UBO_VERSION=1.73.0
|
|
|
|
|
ARG UBO_AMO_FILE_ID=4940584
|
|
|
|
|
ARG UBO_SHA256=bccc51a773150af4af6e1fd62c7bfdeb7238b79ff2381b998fa9f2e38f64786a
|
|
|
|
|
|
2026-06-30 16:02:55 +02:00
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
2026-07-06 23:10:16 +02:00
|
|
|
curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*
|
2026-06-30 16:02:55 +02:00
|
|
|
|
2026-08-10 19:20:57 +02:00
|
|
|
# Pin the browser release and verify the architecture-specific asset before extracting it.
|
|
|
|
|
# Direct downloads keep builds reproducible and avoid camoufox-js resolving "latest".
|
2026-07-06 23:10:16 +02:00
|
|
|
ARG TARGETARCH
|
2026-06-30 16:02:55 +02:00
|
|
|
RUN --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \
|
2026-07-06 23:10:16 +02:00
|
|
|
case "$TARGETARCH" in \
|
2026-08-10 19:20:57 +02:00
|
|
|
amd64) ZIP="camoufox-152.0.4-beta.28-lin.x86_64.zip"; SHA256="924f3109ccd6d47cd6a0384d67a345fadf975d48b6319f8dbbd5954c588982bd" ;; \
|
|
|
|
|
arm64) ZIP="camoufox-152.0.4-beta.28-lin.arm64.zip"; SHA256="3a105a2fc929e80a79b4b7fce2c93ed62c4fb2c877f3c1ed2a5d66a1c4fe968f" ;; \
|
2026-07-06 23:10:16 +02:00
|
|
|
*) echo "unsupported arch: $TARGETARCH"; exit 1 ;; \
|
|
|
|
|
esac && \
|
|
|
|
|
curl -fsSL \
|
2026-08-10 19:20:57 +02:00
|
|
|
"https://github.com/daijro/camoufox/releases/download/v152.0.4-beta.28/${ZIP}" \
|
2026-07-06 23:10:16 +02:00
|
|
|
-o /tmp/camoufox.zip && \
|
2026-08-10 19:20:57 +02:00
|
|
|
echo "${SHA256} /tmp/camoufox.zip" | sha256sum --check --strict - && \
|
2026-07-06 23:10:16 +02:00
|
|
|
unzip -q /tmp/camoufox.zip -d /opt/camoufox && \
|
|
|
|
|
rm /tmp/camoufox.zip && \
|
2026-08-10 19:20:57 +02:00
|
|
|
printf '{"version":"152.0.4","release":"beta.28"}\n' > /opt/camoufox/version.json && \
|
2026-07-06 23:10:16 +02:00
|
|
|
chmod -R 755 /opt/camoufox && \
|
|
|
|
|
rm -rf /opt/camoufox/fonts/macos /opt/camoufox/fonts/windows
|
2026-06-30 16:02:55 +02:00
|
|
|
|
2026-07-08 15:48:48 +02:00
|
|
|
# Bake the GeoIP database so camoufox-js never downloads it at runtime.
|
|
|
|
|
# `geoip: true` (packages/browser/src/pool.ts) otherwise makes camoufox-js fetch
|
|
|
|
|
# GeoLite2-City.mmdb from P3TERX/GeoLite.mmdb on first browser launch, into
|
|
|
|
|
# $CAMOUFOX_INSTALL_DIR. An interrupted/truncated download leaves a corrupt file that
|
|
|
|
|
# camoufox-js reuses forever (getGeolocation only re-downloads when the file is absent),
|
|
|
|
|
# crashing every launch with "Invalid Extended Type at offset N val 7" (issue #20).
|
|
|
|
|
# Baking a verified copy into the image makes startup deterministic and removes the
|
|
|
|
|
# runtime GitHub dependency + first-launch download latency. The size check fails the
|
|
|
|
|
# build if the download is truncated, so a corrupt file can never be baked in.
|
2026-08-10 19:20:57 +02:00
|
|
|
ARG GEOLITE_RELEASE=2026.08.07
|
|
|
|
|
ARG GEOLITE_CITY_SHA256=5c32206cd9a67d3b8995e8ca010191a802e9cb06d3c089963f08b55deb1bd921
|
2026-07-08 15:48:48 +02:00
|
|
|
RUN curl -fsSL \
|
2026-08-10 19:20:57 +02:00
|
|
|
"https://github.com/P3TERX/GeoLite.mmdb/releases/download/${GEOLITE_RELEASE}/GeoLite2-City.mmdb" \
|
2026-07-08 15:48:48 +02:00
|
|
|
-o /opt/camoufox/GeoLite2-City.mmdb && \
|
2026-08-10 19:20:57 +02:00
|
|
|
echo "${GEOLITE_CITY_SHA256} /opt/camoufox/GeoLite2-City.mmdb" | sha256sum --check --strict -
|
2026-07-08 15:48:48 +02:00
|
|
|
|
2026-08-10 02:49:33 +02:00
|
|
|
# Bake uBlock Origin into Camoufox's expected addon directory. camoufox-js still
|
|
|
|
|
# performs its normal addon registration at startup, but no runtime download or
|
|
|
|
|
# extraction is needed (including when /opt/camoufox is read-only).
|
|
|
|
|
RUN curl -fsSL \
|
|
|
|
|
"https://addons.mozilla.org/firefox/downloads/file/${UBO_AMO_FILE_ID}/ublock_origin-${UBO_VERSION}.xpi" \
|
|
|
|
|
-o /tmp/ubo.xpi && \
|
|
|
|
|
echo "${UBO_SHA256} /tmp/ubo.xpi" | sha256sum --check --strict - && \
|
|
|
|
|
mkdir -p /opt/camoufox/addons/UBO && \
|
|
|
|
|
unzip -q /tmp/ubo.xpi -d /opt/camoufox/addons/UBO && \
|
|
|
|
|
test -f /opt/camoufox/addons/UBO/manifest.json && \
|
|
|
|
|
test "$(bun -e 'console.log(JSON.parse(await Bun.file("/opt/camoufox/addons/UBO/manifest.json").text()).version)')" = "${UBO_VERSION}" && \
|
|
|
|
|
rm /tmp/ubo.xpi
|
|
|
|
|
|
2026-06-30 16:02:55 +02:00
|
|
|
# ── Stage 3: lean runtime (only API-required files) ────────────────────────────
|
2026-07-06 23:10:16 +02:00
|
|
|
# debian:bookworm-slim replaces ubuntu:22.04 — same glibc family, ~50 MB smaller base.
|
|
|
|
|
# Camoufox/Firefox require glibc; Alpine's musl is incompatible.
|
|
|
|
|
FROM debian:bookworm-slim
|
2026-06-30 16:02:55 +02:00
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
|
|
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
|
|
|
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
|
|
|
|
apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
|
libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 \
|
|
|
|
|
libcairo2 libcairo-gobject2 \
|
|
|
|
|
libdbus-1-3 libdbus-glib-1-2 \
|
|
|
|
|
libfontconfig1 \
|
|
|
|
|
libgdk-pixbuf-2.0-0 \
|
|
|
|
|
libglib2.0-0 \
|
|
|
|
|
libgtk-3-0 \
|
|
|
|
|
libnspr4 libnss3 \
|
|
|
|
|
libpango-1.0-0 libpangocairo-1.0-0 \
|
|
|
|
|
libx11-6 libx11-xcb1 libxcb1 libxcb-shm0 \
|
|
|
|
|
libxcomposite1 libxcursor1 libxdamage1 \
|
|
|
|
|
libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
|
|
|
|
|
libdrm2 libgbm1 \
|
|
|
|
|
libasound2 \
|
|
|
|
|
fonts-liberation \
|
|
|
|
|
ca-certificates unzip curl \
|
2026-07-21 18:31:26 +02:00
|
|
|
&& apt-get clean \
|
|
|
|
|
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* /usr/share/locale /usr/share/doc /usr/share/man
|
2026-06-30 16:02:55 +02:00
|
|
|
|
2026-07-21 18:31:26 +02:00
|
|
|
# Pull the Bun runtime directly from GitHub releases. The baseline-built binary
|
|
|
|
|
# supports older x86_64 CPUs without AVX2 (pre-Haswell / 2013-era hardware,
|
|
|
|
|
# common on Synology NAS, Atom tablets, and older mini-PCs).
|
|
|
|
|
# arm64 has no baseline variant — every armv8 CPU is supported by the standard
|
|
|
|
|
# `bun-linux-aarch64.zip` binary.
|
|
|
|
|
ARG TARGETARCH
|
|
|
|
|
ARG BUN_VERSION=1.3.14
|
2026-06-30 16:02:55 +02:00
|
|
|
RUN set -eux; \
|
|
|
|
|
case "$TARGETARCH" in \
|
|
|
|
|
amd64) BUN_ZIP=bun-linux-x64-baseline.zip ;; \
|
|
|
|
|
arm64) BUN_ZIP=bun-linux-aarch64.zip ;; \
|
|
|
|
|
*) echo "unsupported arch: $TARGETARCH"; exit 1 ;; \
|
|
|
|
|
esac; \
|
|
|
|
|
curl -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${BUN_ZIP}" -o /tmp/bun.zip; \
|
|
|
|
|
unzip -q /tmp/bun.zip -d /tmp/bun-extract; \
|
|
|
|
|
BUN_BIN=$(find /tmp/bun-extract -name bun -type f | head -1); \
|
|
|
|
|
install -m 0755 "$BUN_BIN" /usr/local/bin/bun; \
|
|
|
|
|
rm -rf /tmp/bun.zip /tmp/bun-extract; \
|
|
|
|
|
/usr/local/bin/bun --version
|
|
|
|
|
|
|
|
|
|
COPY --from=camoufox /opt/camoufox /opt/camoufox
|
|
|
|
|
COPY --from=deps /app/node_modules /app/node_modules
|
|
|
|
|
|
2026-07-06 23:10:16 +02:00
|
|
|
# Strip debug symbols — Bun is statically linked (safe to strip-all); Firefox's glibc/NSS
|
|
|
|
|
# .so files keep their dynamic symbols via --strip-unneeded. Saves ~75 MB uncompressed
|
|
|
|
|
# without affecting runtime behaviour. `|| true` so a strip failure on one file
|
|
|
|
|
# doesn't abort the build.
|
|
|
|
|
RUN strip --strip-all /usr/local/bin/bun 2>/dev/null || true \
|
|
|
|
|
&& find /opt/camoufox -type f \( -name 'firefox' -o -name 'firefox-bin' -o -name '*.so*' \) \
|
|
|
|
|
-exec strip --strip-unneeded {} + 2>/dev/null || true
|
|
|
|
|
|
2026-06-30 16:02:55 +02:00
|
|
|
COPY packages/types/ /app/packages/types/
|
|
|
|
|
COPY packages/browser/ /app/packages/browser/
|
|
|
|
|
COPY packages/tiers/ /app/packages/tiers/
|
|
|
|
|
COPY apps/api/ /app/apps/api/
|
|
|
|
|
COPY package.json /app/
|
|
|
|
|
|
2026-07-21 18:31:26 +02:00
|
|
|
ENV CAMOUFOX_INSTALL_DIR=/opt/camoufox \
|
2026-08-10 02:49:33 +02:00
|
|
|
HOME=/tmp \
|
2026-07-21 18:31:26 +02:00
|
|
|
# Safe Bun runtime knobs — all tested runtime-neutral (no behavior change).
|
|
|
|
|
BUN_DISABLE_CJS=1 \
|
|
|
|
|
BUN_DEBUG=0 \
|
|
|
|
|
BUN_DISABLE_SOURCEMAPS=1 \
|
|
|
|
|
BUN_HTTP_KEEPALIVE=0 \
|
|
|
|
|
BUN_AGENT_DISABLE=1 \
|
|
|
|
|
BUN_INSPECT=0 \
|
|
|
|
|
BUN_LOCKFILE_MIGRATION=false \
|
|
|
|
|
MIMALLOC_PURGE_DELAY=0 \
|
|
|
|
|
NODE_NO_WARNINGS=1
|
|
|
|
|
|
2026-06-30 16:02:55 +02:00
|
|
|
WORKDIR /app
|
|
|
|
|
EXPOSE 8191
|
|
|
|
|
CMD ["bun", "run", "apps/api/src/index.ts"]
|