# Lint stage # Same image as Dockerfile.lint: change both pins together. # golangci/golangci-lint:v2.12.2-alpine, 2026-08-07 FROM golangci/golangci-lint:v2.12.2-alpine@sha256:91b27804074a0bacea298707f016911e60cf0cdbc6c7bf5ccacb5f0606d18d60 AS lint WORKDIR /src # script/bootstrap installs the build dependencies and downloads the Go # modules. Only script/, go.mod and go.sum are copied first, so this # layer is reused until one of them changes. COPY script/ ./script/ COPY go.mod go.sum ./ RUN script/bootstrap # Copy source code COPY . . # Tells script/lint it is inside a container, so it runs the linter. ENV container=docker # Run formatting check and linter RUN make fmt-check RUN make lint # Build stage # golang:1.25.4-alpine, 2026-02-25 FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder # Depend on lint stage passing COPY --from=lint /src/go.sum /dev/null WORKDIR /src # Build dependencies and Go modules, as in the lint stage COPY script/ ./script/ COPY go.mod go.sum ./ RUN script/bootstrap # Copy source code COPY . . # Run tests RUN make test # VERSION is declared here, not earlier: a new value reruns only the # build, not script/bootstrap or the tests. CGO stays enabled for # govips; -trimpath keeps build paths out of the binary, and -s -w # leave out the symbol table and debug information. ARG VERSION=dev RUN CGO_ENABLED=1 GOTOOLCHAIN=auto go build -trimpath \ -ldflags "-s -w -X main.Version=${VERSION}" \ -o /pixad ./cmd/pixad # Runtime stage # alpine:3.21, 2026-02-25 FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 # Install runtime dependencies only RUN apk add --no-cache \ vips \ libheif \ ca-certificates \ tzdata \ su-exec # Copy binary from builder COPY --from=builder /pixad /usr/local/bin/pixad COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh # Create non-root user, config directory, and data directory. pixad # gets uid and gid 65532, which host login and system accounts do not # use: a bind-mounted /var/lib/pixa is given to pixad, and on the host # it must not belong to a person's account. RUN addgroup -g 65532 pixad && \ adduser -D -H -s /sbin/nologin -u 65532 -G pixad pixad && \ mkdir -p /var/lib/pixa /etc/pixa && \ chown pixad:pixad /var/lib/pixa # No USER: the entrypoint must start as root to give a bind-mounted # /var/lib/pixa to pixad; it then runs the server as pixad. WORKDIR /var/lib/pixa EXPOSE 8080 # Shell form so the probe follows PORT; a port set only in a mounted # config file is not seen here. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget --spider -q "http://localhost:${PORT:-8080}/.well-known/healthcheck.json" || exit 1 # Settings come from PORT and the PIXA_ environment variables; only # PIXA_SIGNING_KEY is required. A config file mounted at # /etc/pixa/config.yml is optional and is read when present. ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]