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

2.2 KB · 56 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
 4FROM oven/bun:1-alpine@sha256:07235578f79ef8c6f97d94aee7938e76f5cdba5f21ae5dbfdd3d3d38058437eb AS frontend
 5WORKDIR /src
 6COPY frontend/package.json frontend/bun.lock ./
 7RUN bun install --frozen-lockfile
 8COPY frontend/ ./
 9RUN bun run build
10
11FROM golang:1.27-alpine@sha256:4c9fe60190a2a3350ddc51de80d0224b8a6698d12bdfc999fee45ea9d6c46dbc AS build
12WORKDIR /src
13
14COPY go.mod go.su[m] ./
15COPY web/ ./web/
16COPY *.go ./
17COPY templates/ ./templates/
18COPY --from=frontend /build/dist ./build/dist
19# CGO off, since modernc.org/sqlite is SQLite transpiled to Go rather than bound
20# to it, so the binary is static and needs no libc.
21RUN CGO_ENABLED=0 go build -tags embed -trimpath -ldflags="-s -w" -o /app .
22
23# alpine, not scratch: the clone and push wire is git itself. `git http-backend`
24# is git's own CGI and has to be on disk to serve a single clone, and every page
25# read is a git plumbing subprocess.
26FROM alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40
27
28# git-daemon is the Alpine package that ships git-http-backend, and plain `git`
29# does not, so without it the site starts fine and then 404s every clone.
30# ca-certificates is for the mirror lane talking to api.github.com over TLS.
31RUN apk add --no-cache git git-daemon ca-certificates
32
33COPY --from=build /app /app
34
35ENV REPOS_ROOT=/data/repos
36ENV REPOS_DATA=/data
37# git writes a config and a temporary or two, and the app user has no home.
38ENV HOME=/tmp
39
40# chown before the VOLUME, and the order matters: Docker seeds a fresh volume
41# from the image path it mounts over and carries its ownership. Without it the
42# mountpoint is root-owned and the first push dies on a permission error.
43RUN mkdir -p /data/repos && chown -R 65532:65532 /data
44VOLUME ["/data"]
45
46# UID 65532 is what every other image here runs as, so one number owns every
47# /data volume.
48RUN addgroup -g 65532 app && adduser -u 65532 -G app -D -H app
49USER 65532:65532
50
51HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
52    CMD ["/app", "-healthcheck"]
53
54EXPOSE 8000
55ENTRYPOINT ["/app"]