# Petal — multi-stage build producing the single self-contained binary. # # Stage 1 builds the frontend; stage 2 compiles the Go server with web/dist # embedded (go:embed), so the runtime image carries one executable and no # assets. modernc's SQLite is pure Go, so CGO stays off and the binary is # static — the runtime layer exists only for ffmpeg (read-aloud transcodes # Piper's WAV to mp3) and CA certificates. # ---------- stage 1: frontend ---------- FROM node:22-alpine AS web WORKDIR /src/web # Install deps against the lockfile alone so this layer caches across source # edits. The Hunspell dictionaries come from a devDependency, so a plain # `npm ci` (not --omit=dev) is required for the spell checker to ship. COPY web/package.json web/package-lock.json ./ RUN npm ci COPY web/ ./ RUN npm run build # ---------- stage 2: server ---------- FROM golang:1.25-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # The build context's web/dist is gitignored and excluded by .dockerignore; # take the freshly built one from stage 1 so go:embed picks it up. COPY --from=web /src/web/dist ./web/dist RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/petal ./cmd/server # ---------- stage 3: runtime ---------- FROM alpine:3.21 # ffmpeg: read-aloud pipes Piper's WAV through it to mp3/opus. tzdata: the # companion's bedtime nag and night mode read the local clock, so the container # needs a real timezone rather than bare UTC. RUN apk add --no-cache ca-certificates ffmpeg tzdata \ && adduser -D -u 10001 petal WORKDIR /app COPY --from=build /out/petal /app/petal # Mount point for petal.db (+ -wal/-shm), the image store, the TTS cache and # DreamDict's read-only dict.db. dict.db is deployed alongside rather than baked # in: it is ~450 MB, changes a few times a year, and is shared with other # services on the host — putting it in the image would multiply it by every tag. RUN mkdir -p /data && chown -R petal:petal /data VOLUME ["/data"] USER petal EXPOSE 8080 ENV PORT=8080 \ DATABASE_PATH=/data/petal.db \ IMAGE_DIR=/data/images \ TTS_CACHE_DIR=/data/tts \ DICT_PATH=/data/dict.db # Same endpoint Traefik and the uptime probe use; needs no session by design. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://127.0.0.1:8080/api/health || exit 1 ENTRYPOINT ["/app/petal"]