Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- feat/build
- feat/schema_upgrade
- env_var_namechange
- harden-image
tags:
- "v*"
workflow_dispatch:
Expand Down Expand Up @@ -46,12 +47,14 @@
type=ref,event=tag
type=raw,value=${{ env.SHORT_SHA }}

- name: Build and push Docker image
- name: Build and push the image, with its SBOM and provenance
uses: docker/build-push-action@v5

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action or reusable workflow Medium

Unpinned 3rd party Action 'Build and Push Docker Image' step
Uses Step
uses 'docker/build-push-action' with ref 'v5', not a pinned commit hash
with:
context: .
file: docker/server/Dockerfile
push: true
sbom: true
provenance: mode=max
build-args: |
SOURCE_COMMIT=${{ env.SHORT_SHA }}
tags: ${{ steps.meta.outputs.tags }}
Expand Down
48 changes: 30 additions & 18 deletions docker/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
# The sync server as an image: built from the locked dependencies, run as an
# unprivileged user on a slim Debian. Configured entirely by environment
# variables (docs/deploy.md lists every one); nothing is baked in but the
# listen address and the JSON log format a container platform expects.
# The sync server as an image: built from the locked dependencies, and run on
# Google's distroless runtime, which has no shell, no package manager and no
# coreutils, so a compromised server has no tools to run. The runtime is
# `cc`: glibc and libgcc, which the binary links, and nothing else it needs;
# TLS is rustls with its roots compiled in. It runs as `nonroot` (uid 65532).
# Configured entirely by environment variables (docs/deploy.md lists every
# one); nothing is baked in but the listen address and the JSON log format a
# container platform expects.
#
# There is no HEALTHCHECK, since there is no shell to run one: the platform
# probes `/healthz` and `/readyz` over HTTP (docs/deploy.md).
#
# docker build -f docker/server/Dockerfile -t streamgres .
ARG BASE_REGISTRY=docker.io/library
FROM ${BASE_REGISTRY}/rust:1.89-bookworm AS builder
#
# Both stages are on Debian 13, so the binary is linked against the glibc it
# runs on. A mirror of the same images is given whole:
#
# docker build --build-arg BUILD_IMAGE=<mirror>/rust:1.89-trixie \
# --build-arg RUNTIME_IMAGE=<mirror>/distroless/cc-debian13:nonroot ...
ARG BUILD_IMAGE=docker.io/library/rust:1.89-trixie
ARG RUNTIME_IMAGE=gcr.io/distroless/cc-debian13:nonroot

FROM ${BUILD_IMAGE} AS builder
WORKDIR /build
COPY Cargo.toml Cargo.lock clippy.toml ./
COPY src ./src
RUN cargo build --release --locked --bin server
RUN cargo build --release --locked --bin server \
&& mkdir -p /out/app /out/var/lib/streamgres \
&& cp target/release/server /out/app/server

FROM ${BASE_REGISTRY}/debian:bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --home-dir /app --shell /usr/sbin/nologin streamgres \
&& mkdir -p /app /var/lib/streamgres /var/lib/xyne-sync \
&& chown -R streamgres:streamgres /app /var/lib/streamgres /var/lib/xyne-sync
FROM ${RUNTIME_IMAGE} AS runtime
# The binary is root-owned and read-only to the server; only the plan
# file's directory is its to write.
COPY --from=builder --chown=0:0 /out/app /app
COPY --from=builder --chown=65532:65532 /out/var/lib/streamgres /var/lib/streamgres
WORKDIR /app
COPY --from=builder /build/target/release/server /app/server

ARG SOURCE_COMMIT=unknown
ENV SOURCE_COMMIT=${SOURCE_COMMIT} \
STREAMGRES_ADDR=0.0.0.0:4848 \
STREAMGRES_LOG_FORMAT=json

USER streamgres
USER 65532:65532
EXPOSE 4848
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=6 \
CMD curl -fsS http://127.0.0.1:4848/health || exit 1
CMD ["/app/server"]
14 changes: 10 additions & 4 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ For a local first run, see the README's Quick start.
docker build -f docker/server/Dockerfile -t streamgres .
```

The image is a slim Debian image of about 125 MB:
- It has one binary, `/app/server`, run as the unprivileged user `streamgres`
(uid 10001).
The image runs on Google's distroless `cc-debian13` (built on
`rust:1.89-trixie`). The runtime has no shell, no package manager and no
coreutils:
- It has one binary, `/app/server`, run as the unprivileged user `nonroot`
(uid 65532).
- It listens on port 4848 and writes JSON log lines to stderr.
- It has no Docker `HEALTHCHECK` (there is no curl to run one): probe
`/healthz` and `/readyz` over HTTP from the platform instead, which
answer 503 until the server is ready (section 6).
- It needs no file system, except an optional writable directory for
`STREAMGRES_PLAN_FILE` (section 5).
`STREAMGRES_PLAN_FILE` (section 5), and a writable `/tmp` for heap
profiles when Pyroscope is on and the root file system is read-only.

`.github/workflows/docker-publish.yml` publishes the image to the GitHub
Container Registry as `ghcr.io/<owner>/streamgres`. It runs on pushes to the
Expand Down
Loading