# Edge image: builds the React SPA, then serves it from Caddy (which also reverse-proxies the API). # The SPA calls the API at the same origin (apiClient base "/rest"), so it's deployment-agnostic — # no server URL is baked into the web bundle. FROM node:24-alpine AS web WORKDIR /web # Release CI passes the agent's package + signing checksum (+ optional APK URL) so the in-product # enrollment QR matches the signed release APK. Defaults (in provisioning.ts) cover the debug build. ARG VITE_AGENT_PACKAGE ARG VITE_AGENT_CHECKSUM ARG VITE_AGENT_APK_URL # Build version, baked so the open console can detect it lags a freshly-deployed one (ReloadPrompt). ARG VITE_APP_VERSION=dev ENV VITE_AGENT_PACKAGE=$VITE_AGENT_PACKAGE \ VITE_AGENT_CHECKSUM=$VITE_AGENT_CHECKSUM \ VITE_AGENT_APK_URL=$VITE_AGENT_APK_URL \ VITE_APP_VERSION=$VITE_APP_VERSION COPY web/package*.json ./ RUN npm ci COPY web/ ./ # The What's new dialog's notes, read at build time from the repository's CHANGELOG.md (web/vite.changelog.ts); the build fails without it. COPY CHANGELOG.md /CHANGELOG.md RUN npm run build # An exact Caddy release, not caddy:2-alpine: a Caddy release can change how docker/Caddyfile parses (v0.3.1's # Cloudflare-mode outage was a Caddyfile that stopped parsing). Dependabot proposes each new release in its weekly # docker-images PR, and T0's edge check (scripts/edge-check.sh, which reads the image from this line) validates the # Caddyfile in every hosting mode against it before it can merge. The tag itself is still rebuilt upstream for # Alpine fixes. FROM caddy:2.11.9-alpine # Run Caddy unprivileged. It still needs to bind :80/:443 in own-domain mode, so grant just that capability # to the binary; /data (certs) and /config are mounted volumes that older deployments created root-owned, # so a tiny root entrypoint fixes their ownership and then su-execs to "caddy". RUN apk add --no-cache libcap su-exec \ && addgroup -S caddy && adduser -S -G caddy -h /data caddy \ && setcap cap_net_bind_service=+ep /usr/bin/caddy COPY --from=web /web/dist /srv COPY docker/Caddyfile /etc/caddy/Caddyfile COPY docker/web-entrypoint.sh /web-entrypoint.sh COPY docker/edge-peers.sh /edge-peers.sh RUN chmod +x /web-entrypoint.sh && chown -R caddy:caddy /srv /etc/caddy /data /config EXPOSE 80 443 ENTRYPOINT ["/web-entrypoint.sh"] CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]