# Web build stage — compile SPA from source # node:22-alpine, 2026-03-09 FROM node@sha256:8094c002d08262dba12645a3b4a15cd6cd627d30bc782f53229a2ec13ee22a00 AS web-builder # yarn 1.22.22, 2026-10-06 # The version script/bootstrap pins, checked against the sha512 of its # npm package. corepack enable replaces the yarn the image ships with # corepack's own, which runs the version activated here. RUN corepack enable \ && corepack prepare yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e --activate WORKDIR /web COPY web/package.json web/yarn.lock ./ RUN yarn install --frozen-lockfile COPY web/src/ src/ COPY web/build.sh build.sh RUN sh build.sh # Lint phase, built alone by script/lint. The linter is invoked directly # rather than through `make lint`, which is itself a docker build and # would recurse into a daemon that does not exist in a build step. # golangci/golangci-lint:v2.14.0, 2026-10-06 FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # Placeholder files so //go:embed dist/* in web/embed.go resolves # without waiting for the web-builder stage. The test phase does the same. RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css web/dist/app.js RUN golangci-lint run --config .golangci.yml ./... # Test phase, built alone by script/test. -race needs cgo and so a C # compiler, which the Debian Go image ships and the alpine one does not. # golang:1.24.13-bookworm, 2026-10-06 FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS test WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css web/dist/app.js # -p 4 because test runs on a shared build host cap their parallelism. RUN go test -p 4 -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -p 4 -timeout 90s -race -v ./...; exit 1; } # Build stage. Nothing is wanted from either phase above; the copies # are what make BuildKit build them first, so this stage cannot run # unless lint and test passed. # golang:1.24-alpine, 2026-02-26 FROM golang@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder COPY --from=lint /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null RUN apk add --no-cache git # A tar-stream context keeps the sender's file owners, which git refuses. RUN git config --system --add safe.directory /src WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . COPY --from=web-builder /web/dist/ web/dist/ # Build static binaries (no cgo needed at runtime — modernc.org/sqlite is pure Go) # # neoircd is stamped with the VERSION build arg when one is given, otherwise # with `git describe --tags --always` on the .git in the build context. With # .git present, a version that is still empty, dev or unknown fails the build: # git is missing or could not read the checkout. ARG VERSION RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \ if [ -e .git ]; then \ case "$VERSION" in ""|dev|unknown) \ echo "version is '$VERSION' although .git is present" >&2; \ exit 1 ;; \ esac; \ fi; \ CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o /neoircd ./cmd/neoircd/ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /neoirc-cli ./cmd/neoirc-cli/ # Runtime stage, and the last one: a plain `docker build .` builds this # stage's chain and nothing else. # alpine:3.21, 2026-02-26 FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 RUN apk add --no-cache ca-certificates \ && addgroup -S neoirc && adduser -S neoirc -G neoirc \ && mkdir -p /var/lib/neoirc \ && chown neoirc:neoirc /var/lib/neoirc COPY --from=builder /neoircd /usr/local/bin/neoircd USER neoirc EXPOSE 8080 6667 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:8080/.well-known/healthcheck.json || exit 1 ENTRYPOINT ["neoircd"]