repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

5.1 KB · 114 lines · Docker Raw History
  1# Build context is this directory alone: the site is its own Go module with its
  2# own copy of web/. Base images are pinned by digest because :1-alpine floats.
  3#
  4# This is the heaviest image in the repo and Lighthouse is all of it: the audit
  5# is Google's own JavaScript driving a real Chromium over DevTools, so the
  6# runtime carries a browser, a JavaScript runtime and the CLI.
  7
  8# Typst is copied into the runtime image rather than discarded after the build,
  9# because a report describes a live monitoring state that changes every three
 10# minutes, so there is nothing finite to precompile. See typst.go.
 11FROM alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40 AS typst
 12ARG TYPST_VERSION=0.14.0
 13ARG TYPST_SHA256=99816d2982de08d2b091bac56b59b2faa523a10e1378ad3cdd68e35b8eb74b3d
 14RUN apk add --no-cache curl tar xz && \
 15    curl -fsSL -o /tmp/typst.tar.xz \
 16      "https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-x86_64-unknown-linux-musl.tar.xz" && \
 17    echo "${TYPST_SHA256}  /tmp/typst.tar.xz" | sha256sum -c - && \
 18    tar -xJf /tmp/typst.tar.xz -C /tmp && \
 19    mv /tmp/typst-x86_64-unknown-linux-musl/typst /usr/local/bin/typst
 20
 21FROM oven/bun:1-alpine@sha256:07235578f79ef8c6f97d94aee7938e76f5cdba5f21ae5dbfdd3d3d38058437eb AS frontend
 22WORKDIR /src
 23# bunfig.toml has to arrive before install runs, or install.peer = false
 24# silently does nothing.
 25COPY frontend/package.json frontend/bun.lock frontend/bunfig.toml ./
 26RUN bun install --frozen-lockfile
 27COPY frontend/ ./
 28RUN bun run build
 29
 30# Its own stage so a CSS change does not reinstall the Lighthouse dependency
 31# tree, which changes only when the Lighthouse version does.
 32FROM oven/bun:1-alpine@sha256:07235578f79ef8c6f97d94aee7938e76f5cdba5f21ae5dbfdd3d3d38058437eb AS lighthouse
 33WORKDIR /src
 34COPY package.json bun.lock ./
 35RUN bun install --frozen-lockfile
 36
 37# PNG, not SVG, since no social platform accepts image/svg+xml for og:image.
 38# Geist comes out of the frontend stage's node_modules because @fontsource ships
 39# woff2 only and Typst reads TrueType.
 40FROM typst AS card
 41COPY --from=frontend /src/node_modules/geist/dist/fonts/geist-sans /fonts/geist-sans
 42COPY typst/card.typ /card.typ
 43RUN typst compile --format png --ppi 72 --font-path /fonts/geist-sans /card.typ /card.png
 44
 45FROM golang:1.27-alpine@sha256:4c9fe60190a2a3350ddc51de80d0224b8a6698d12bdfc999fee45ea9d6c46dbc AS build
 46WORKDIR /src
 47
 48# The bracket glob makes go.sum optional, since a plain COPY go.sum fails the
 49# build outright for a module with no third party dependency.
 50COPY go.mod go.su[m] ./
 51COPY web/ ./web/
 52COPY *.go ./
 53COPY templates/ ./templates/
 54COPY reports/ ./reports/
 55# -tags embed reads build/dist at compile time, so the bundle has to land here
 56# before the compile below, the card included.
 57COPY --from=frontend /build/dist ./build/dist
 58COPY --from=card /card.png ./build/dist/og/card.png
 59# CGO off, since modernc.org/sqlite is SQLite transpiled to Go rather than bound
 60# to it, so the binary is static and needs no libc.
 61RUN CGO_ENABLED=0 go build -tags embed -trimpath -ldflags="-s -w" \
 62    -o /app .
 63
 64FROM alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40
 65
 66# chromium is for Lighthouse and nothing else, and typst wants the font packages
 67# instead. No nodejs and no npm anywhere, because `bun run --bun` symlinks node
 68# to bun so the lighthouse shim's `#!/usr/bin/env node` shebang resolves.
 69RUN apk add --no-cache \
 70      ca-certificates \
 71      chromium \
 72      font-jetbrains-mono \
 73      ttf-dejavu \
 74      ttf-liberation \
 75      fontconfig && \
 76    fc-cache -f
 77
 78COPY --from=oven/bun:1-alpine@sha256:07235578f79ef8c6f97d94aee7938e76f5cdba5f21ae5dbfdd3d3d38058437eb /usr/local/bin/bun /usr/local/bin/bun
 79COPY --from=typst /usr/local/bin/typst /usr/local/bin/typst
 80COPY --from=build /app /app
 81
 82# SITE_ROOT is where lighthouse.go looks for node_modules/.bin/lighthouse and
 83# what typst resolves absolute paths against, so it doubles as the fence on what
 84# a compile is allowed to read.
 85WORKDIR /srv
 86COPY --from=lighthouse /src/node_modules /srv/node_modules
 87COPY reports/ /srv/reports/
 88
 89ENV SITE_DATA=/data
 90ENV SITE_ROOT=/srv
 91# Chromium ships in the image, so findChromium has nothing to walk.
 92ENV CHROMIUM_BIN=/usr/bin/chromium-browser
 93# fontconfig wants somewhere writable, or typst warns on every compile.
 94ENV XDG_CACHE_HOME=/tmp
 95
 96# chown before the VOLUME, and the order matters: Docker seeds a fresh volume
 97# from the image path it mounts over and carries its ownership. Without it the
 98# mountpoint is root-owned and the first boot dies on a database open error.
 99RUN mkdir -p /data && chown 65532:65532 /data
100VOLUME ["/data"]
101
102# UID 65532 like the scratch images here, so one number owns every /data volume.
103# -H because this user has no home and writes only to /data and /tmp.
104RUN addgroup -g 65532 app && adduser -u 65532 -G app -D -H app
105USER 65532:65532
106
107# The binary probes itself rather than shelling out to wget, which this image
108# does have, because the scratch images cannot. Compose sets the same check.
109HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
110    CMD ["/app", "-healthcheck"]
111
112EXPOSE 8000
113ENTRYPOINT ["/app"]