# The single image: Peekaping in single mode (ADR 0002). One process, peekaping, serves the # web app, the API and the websocket on port 8383. Its database is SQLite in /app/data, or a # PostgreSQL server in a container of its own (DB_TYPE=postgres). # Stage 1: Build peekaping and the migration tool FROM golang:1.27.3-alpine AS go-builder RUN apk add --no-cache git WORKDIR /app/server COPY apps/server/go.mod apps/server/go.sum ./ RUN go mod download COPY apps/server/ . RUN CGO_ENABLED=0 GOFLAGS="-trimpath" go build -o peekaping -ldflags="-s -w" ./cmd/peekaping RUN CGO_ENABLED=0 GOFLAGS="-trimpath" go build -o bun -ldflags="-s -w" ./cmd/bun # Stage 2: Build the web app FROM node:22-alpine AS web-builder WORKDIR /app # Bun, at the version pinned in package.json COPY --from=oven/bun:1.4.4-alpine /usr/local/bin/bun /usr/local/bin/bun COPY package.json bun.lock ./ COPY apps/web/package.json ./apps/web/package.json RUN bun install --frozen-lockfile --filter web COPY apps/web/ ./apps/web/ WORKDIR /app/apps/web RUN bun run build # Stage 3: Runtime FROM alpine:3.24 # tini runs as PID 1 and passes docker stop's SIGTERM to peekaping. su-exec drops root for # peekaping, and shadow's groupmod and usermod apply PUID and PGID (startup.user.sh). Ping # monitors fall back to busybox's ping where they can't open an ICMP socket themselves. RUN apk add --no-cache ca-certificates tini su-exec shadow # PUID and PGID can't take an ID a user or group in the image has, and Alpine gives out IDs # that Debian, which earlier bundles ran on, leaves free: groups 82 (www-data) and 999 (ping) # and user 405 (guest), among others. So the users and groups no file in the image belongs to # are dropped. # root, nobody and nogroup stay, and so do tty, disk and kmem, the groups Linux gives the # device files Docker can bring into the container. RUN for user in $(cut -d: -f1 /etc/passwd); do \ case "$user" in root | nobody) continue ;; esac; \ if [ -z "$(find / -xdev -user "$user" | head -n 1)" ]; then userdel "$user" || exit 1; fi; \ done \ && for group in $(cut -d: -f1 /etc/group); do \ case "$group" in root | tty | disk | kmem | nogroup | nobody) continue ;; esac; \ if [ -z "$(find / -xdev -group "$group" | head -n 1)" ]; then groupdel "$group" || exit 1; fi; \ done # The user peekaping runs as; startup.user.sh applies PUID/PGID RUN addgroup -g 1000 peekaping \ && adduser -u 1000 -G peekaping -H -D -h /nonexistent -s /sbin/nologin peekaping \ && mkdir -p /app/data \ && chown peekaping:peekaping /app/data COPY --from=go-builder --chmod=755 /app/server/peekaping /app/server/bun /app/server/ COPY --from=go-builder --chmod=755 /app/server/scripts/run-migrations.sh /app/server/run-migrations.sh COPY --from=web-builder /app/apps/web/dist /app/web COPY --chmod=755 startup.single.sh /app/startup.sh COPY startup.single.user.sh /app/startup.user.sh # peekaping serves the web build itself and generates /env.js. The startup script checks DB_TYPE # and forces SERVER_PORT to 8383, whatever the .env says. ENV WEB_DIR=/app/web \ TZ=UTC WORKDIR /app/server EXPOSE 8383 # peekaping takes up to 30 seconds to shut down; give docker stop -t 35 or # stop_grace_period: 35s so it isn't killed halfway STOPSIGNAL SIGTERM HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --start-interval=2s --retries=3 \ CMD wget -q -O /dev/null http://127.0.0.1:8383/api/v1/health || exit 1 ENTRYPOINT ["/sbin/tini", "--"] CMD ["/app/startup.sh"]