1# The forge as one container: sshd (git over ssh), Caddy (pages, sites, https2# clones), and optionally tailcat, which publishes both ports over WireGuard3# to an edge without opening any port on the host. All state lives in4# /srv/git, which should be a volume.5FROM golang:1.27.2 AS tailcat6RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tailscale/tailcat/cmd/tailcat@v0.7.078# rclone syncs the pages mirror (forge mirror). Debian's 1.60 predates R29# support for its uploads (R2 answers 501), so build a pinned release; the Go10# checksum database verifies the source, as for tailcat.11FROM golang:1.27.2 AS rclone12RUN CGO_ENABLED=0 GOBIN=/out go install github.com/rclone/rclone@v1.75.21314# litestream copies identity.db to the backup bucket as it changes (forge15# identity replicate). Pinned and checked the same way; it builds without16# cgo.17FROM golang:1.27.2 AS litestream18RUN CGO_ENABLED=0 GOBIN=/out go install github.com/benbjohnson/litestream/cmd/litestream@v0.5.171920# Caddy serves pages, sites and HTTPS clones. Debian's is 2.6, from 2022, and21# the edge runs 2.11 (Alpine's, deploy/edge), so build a pinned release of22# the same line, checked the same way. Built as Caddy's own releases are23# (trimmed paths, no symbol table: a third smaller); a build of Caddy's own24# module can't read its version, so caddy version is told it (CustomVersion).25FROM golang:1.27.2 AS caddy26RUN v=v2.11.7 && CGO_ENABLED=0 GOBIN=/out go install -trimpath \27 -ldflags "-s -w -X github.com/caddyserver/caddy/v2.CustomVersion=$v" \28 github.com/caddyserver/caddy/v2/cmd/caddy@$v2930# Node runs the TypeScript sources directly (type stripping). 24 is the LTS31# whose WebCrypto has ML-DSA (24.7 and later), so passkeys can be32# post-quantum; the forge itself needs 22.18 or later.33FROM node:24-trixie-slim3435# Caddy gets the system user Debian's package would make, in the git group36# to read pages and repos. media-types is /etc/mime.types, the content37# types Caddy sends for files beyond the few Go knows itself.38RUN apt-get update \39 && apt-get install -y --no-install-recommends git openssh-server ca-certificates tini fcgiwrap restic media-types \40 && rm -rf /var/lib/apt/lists/* \41 && adduser --system --group --shell /bin/sh --home /srv/git git \42 && adduser --system --group --shell /usr/sbin/nologin --home /var/lib/caddy --gecos 'Caddy web server' caddy \43 && usermod -aG git caddy \44 && mkdir -p /etc/caddy \45 && rm -f /etc/ssh/ssh_host_*4647COPY --from=tailcat /out/tailcat /usr/local/bin/tailcat48COPY --from=rclone /out/rclone /usr/local/bin/rclone49COPY --from=litestream /out/litestream /usr/local/bin/litestream50COPY --from=caddy /out/caddy /usr/local/bin/caddy51COPY server/container/sshd.conf /etc/ssh/sshd_config.d/forge.conf5253WORKDIR /opt/forge54COPY package.json package-lock.json ./55# npm also installs DuckDB's musl build, which glibc doesn't need (65M).56RUN npm ci --omit=dev --no-audit --no-fund && rm -rf node_modules/@duckdb/node-bindings-linux-*-musl57COPY . .58# forge lake's DuckDB extensions (DuckLake, SQLite for its catalog, httpfs59# for data on R2), installed now so the container never downloads one; the60# install checks they are the builds lib/lake.ts was tested with.61RUN FORGE_DUCKDB_EXTENSIONS=/opt/forge/.duckdb/extensions node lib/lake.ts install62RUN chmod -R a+rX /opt/forge && ln -s /opt/forge/server/container/forge-admin /usr/local/bin/forge-admin6364EXPOSE 2222 808065# -s: reap background builds even when tini is not PID 1 (as on Fly).66ENTRYPOINT ["tini", "-s", "--"]67CMD ["/opt/forge/server/container/start.sh"]