Deploying devpage

How to run devpage for a team — on a Kubernetes cluster or any container host — so they can read the docs and comment on them: the example image, the auth proxy that names each reader, the volume that keeps comments, and trying the whole thing on your machine with Docker Compose — and how to serve a public site, which runs with comments off. It is a task article under the viewer.

flowchart LR
  reader["Reader's browser"] --> proxy["Auth proxy<br/>sets X-Forwarded-Email"]
  proxy --> devpage["devpage<br/>--host 0.0.0.0"]
  devpage --> docs["Docs<br/>baked into the image"]
  devpage <--> store["Comment store<br/>volume at .devpage/"]
  click proxy href "#the-auth-proxy" "The auth proxy"
  click devpage href "#the-image" "The image"
  click docs href "#the-image" "The image"
  click store href "../comments/the-comment-store/README.md" "The comment store"

What deployed means

devpage listens on 127.0.0.1 unless --host gives another address. On any address other than a loopback one it is deployed: it takes each comment's author from the identity header instead of git, and refuses a comment that lacks it. Everything else — live updates, comments, link problems — works as it does locally.

devpage docs --host 0.0.0.0 --port 3030 --user-header X-Forwarded-Email

--host is the address to listen on, --port an exact port (failing if it is taken) and --user-header the header that names the reader, read only when deployed; their defaults are in the configuration reference.

A public site

Comments are for a team behind its auth proxy. Docs anyone on the internet can read have no proxy to name the reader, so serve them with comments off:

devpage docs --host 0.0.0.0 --port 3030 --no-comments

The pages show no way to comment, every comment request answers 404, and no .devpage/ volume is needed (Turning comments off). The same image can serve both: add --no-comments to the command of the public deployment only.

The auth proxy

devpage has no login (devpage authenticates no one). Put it behind the proxy your cluster already uses — oauth2-proxy, or your ingress's external authentication — and have it pass the signed-in user's email in X-Forwarded-Email, or name its header with --user-header.

It believes the header, so anyone who can reach it directly can claim to be anyone: expose the proxy, and keep devpage's own port inside the cluster.

The image

deploy/Dockerfile is an example image serving devpage's own docs. It installs devpage from crates.io — the same cargo install devpage anyone runs — copies the docs in, and starts devpage deployed:

FROM rust:1.97-slim-bookworm AS build
RUN cargo install devpage --version 0.2.0 --locked

FROM debian:bookworm-slim
COPY --from=build /usr/local/cargo/bin/devpage /usr/local/bin/devpage
COPY docs /srv/docs
COPY LICENSE /srv/LICENSE
WORKDIR /srv
EXPOSE 3030
CMD ["devpage", "docs", "--host", "0.0.0.0", "--port", "3030"]

Build it from the repository root: docker build -f deploy/Dockerfile .. The builder is Debian bookworm, the same as the runtime image, so the binary finds the C library it was linked against. The docs are baked in — with the licence they link to — so a docs change is a new image, shipped like any other. devpage runs from /srv, so the served folder is /srv and the comment store is /srv/.devpage/comments:

  • /srv/ — the served folder
    • docs/ — devpage's docs, baked in
    • LICENSE
    • .devpage/ — a volume, so comments survive a new image
      • comments/

Mount a volume at /srv/.devpage so comments survive a new image, and run one replica: the store is files, written by one process.

Trying it with Docker Compose

deploy/compose.yaml runs the image behind a stand-in for the auth proxy — a Caddy server that sets X-Forwarded-Email: dev@example.com on every request — with the comment store on a named volume:

$ docker compose -f deploy/compose.yaml up --build

Open http://localhost:8090. Comments you write are stamped dev@example.com; stop and start the containers and they are still there. devpage's own port is not published, as on a cluster: only the proxy is reachable.