From 5a25dafa4f62f796e2aba1562a88c612b8d83db0 Mon Sep 17 00:00:00 2001 From: Lorem Dev Date: Sun, 2 Aug 2026 22:28:19 +0200 Subject: [PATCH 1/2] feat: publish a Docker Hub overview on release The image's page on Docker Hub was empty: whatever is not pushed there stays blank, and it is the only description an image gets for someone who never opens the repository. DOCKERHUB.md rather than the README. The reader of that page has already chosen the image, so it opens with `docker run` instead of the installer, drops building from source, and leads with the two things that bite -- binding 0.0.0.0 and the templates volume. Every link is absolute; nothing there is served from a checkout. The `docker run` and compose snippets are rewritten to the tag being released before the file is pushed, so the commands name a version that can actually be pulled. The tags table stays illustrative, matching docs/usage/docker.md. Pre-releases skip the push, on the same reasoning that keeps them off the moving tags: the page would tell everyone to pull an rc. The logo is a 128px PNG, referenced by raw.githubusercontent.com URL. Docker Hub renders markdown and no real overview on the hub uses raw ``, so there is no way to size a 512px original down; and a relative path would resolve against hub.docker.com. --- .agents/skills/check-docs/SKILL.md | 7 +- .github/workflows/release.yml | 31 +++++ DOCKERHUB.md | 189 +++++++++++++++++++++++++++++ docs/assets/icon-128.png | Bin 0 -> 22960 bytes 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 DOCKERHUB.md create mode 100644 docs/assets/icon-128.png diff --git a/.agents/skills/check-docs/SKILL.md b/.agents/skills/check-docs/SKILL.md index 67da2c4..fbdc767 100644 --- a/.agents/skills/check-docs/SKILL.md +++ b/.agents/skills/check-docs/SKILL.md @@ -5,12 +5,17 @@ description: Use before a release, or after changing a command, a flag, a config # Check the documentation -Documentation in this repository has four homes, and they go stale in +Documentation in this repository has five homes, and they go stale in different ways: - `README.md` -- the entry point. What Doppel is, what it does and does not do yet, how to run it. - `docs/` -- the mkdocs site, in three sections (below). +- `DOCKERHUB.md` -- the Docker Hub overview, pushed by the release workflow. + Written for someone who has already chosen the image, so it opens with + `docker run` and never mentions building from source. Check it against + `docs/usage/docker.md`, which covers the same ground at length: a fact that + changed in one and not the other is the failure mode here. - `AGENTS.md` -- addressed to agents. Holds rules and pointers, never a second copy of anything in `docs/`. - Doc comments in the code -- where the reasoning lives. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c698135..dcfccb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -245,6 +245,37 @@ jobs: # here would be a fixed string and would give both architectures the # same binary. + # Docker Hub's page is the one place an image is described to someone who + # never opens the repository, and it stays whatever was last pushed to it + # -- including empty. DOCKERHUB.md is written for that page rather than + # being the README, which opens with an installer the image's reader has + # no use for. + # + # Skipped for a pre-release, on the same reasoning as the moving tags: the + # page would tell everyone to pull an rc. + - name: Point the overview at the version being released + if: ${{ !contains(github.event.inputs.tag || github.ref_name, '-') }} + env: + TAG: ${{ github.event.inputs.tag || github.ref_name }} + run: | + set -euo pipefail + version="${TAG#v}" + # Only the copy-paste commands, which name the image. The tags table + # uses bare `1.2.3-alpine` to show the shape and is left alone. + sed -i -E "s|loremdev/doppel:[0-9][^[:space:]]*-alpine|loremdev/doppel:${version}-alpine|g" \ + DOCKERHUB.md + grep -n 'loremdev/doppel:' DOCKERHUB.md + + - name: Push the overview to Docker Hub + if: ${{ !contains(github.event.inputs.tag || github.ref_name, '-') }} + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: loremdev/doppel + readme-filepath: ./DOCKERHUB.md + short-description: "CLI-driven HTTP reverse proxy that forwards, degrades, or mocks a backend" + publish: name: Publish the release needs: build diff --git a/DOCKERHUB.md b/DOCKERHUB.md new file mode 100644 index 0000000..45127aa --- /dev/null +++ b/DOCKERHUB.md @@ -0,0 +1,189 @@ + + +![Doppel](https://raw.githubusercontent.com/lorem-dev/doppel/main/docs/assets/icon-128.png) + +# Doppel + +A CLI-driven HTTP reverse proxy. The name is from "doppelganger": it stands in +front of, or in place of, a real backend, so clients of that backend can be +developed and tested against a realistic, deliberately degraded, or entirely +absent upstream. It forwards traffic to a configured upstream, injects +controlled latency and loss, and replaces selected endpoints with templated +mock responses. + +- **Documentation:** +- **Source:** +- **Licence:** Apache-2.0 + +## Tags + +Published for `linux/amd64` and `linux/arm64`. + +| Tag | Moves? | Points at | +|---|---|---| +| `1.2.3-alpine` | no | exactly that release | +| `1.2-alpine` | yes | the newest patch of 1.2 | +| `1-alpine` | yes | the newest minor of 1 | + +Every tag names its base, the convention the images this one sits beside +use (`postgres:17.6-alpine`, `node:22-alpine`). That leaves the unsuffixed +names free, so a second base could be published later without renaming +anything anyone has already pinned. + +There is deliberately **no `latest`**. A tag that moves is one an unpinned +deployment follows into a release nobody reviewed. Pin the full version in +anything that matters. + +A pre-release (`1.2.3-rc.1`) is published as `1.2.3-rc.1-alpine` only. It never +takes `1.2-alpine` or `1-alpine` with it. + +## Quick start + +The image ships no configuration. That is deliberate: a default that quietly +proxied somewhere unintended would be worse than a container that refuses to +start. + +```bash +docker run --rm \ + -p 8080:8080 -p 8081:8081 \ + -v "$PWD/main.yaml:/etc/doppel/main.yaml:ro" \ + -v doppel-templates:/var/lib/doppel/templates \ + loremdev/doppel:0.1.0-alpine +``` + +`8080` carries proxied traffic, `8081` the admin API. + +A minimal `main.yaml`: + +```yaml +server: + host: "0.0.0.0" + port: 8080 + +admin: + host: "0.0.0.0" + port: 8081 + tokens: [] + access: {} + upload: + limit: 1Mi + +proxies: + - name: backend + type: http + url: "https://example.com/" + resolve: + type: default +``` + +Both listeners must bind `0.0.0.0` rather than `127.0.0.1`, or nothing outside +the container can reach them. + +## The templates volume is not optional + +Templates uploaded through the admin API are written to disk at upload and read +from disk per request. Without a volume they live in the container's writable +layer and go with it -- `docker stop`, `docker run` again, and every uploaded +template is gone. Nothing reports this: the mock that named the file answers +with a render error on the next request, which reads like a broken template +rather than a missing volume. + +```yaml +templates: + dir: /var/lib/doppel/templates +``` + +A relative `./templates` also works: the image's working directory is +`/var/lib/doppel`, so it resolves to the same place. + +## Compose + +```yaml +services: + doppel: + image: loremdev/doppel:0.1.0-alpine + ports: + - "8080:8080" + - "8081:8081" + volumes: + - ./main.yaml:/etc/doppel/main.yaml:ro + - doppel-templates:/var/lib/doppel/templates + environment: + DOPPEL_ADMIN_TOKENS: '{"ci":{"token":"...","group":"admin"}}' + healthcheck: + test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8081/status"] + interval: 5s + timeout: 3s + start_period: 5s + retries: 5 + +volumes: + doppel-templates: +``` + +`/status` needs no token by default and answers only once the runtime is +compiled and both listeners are bound. `-f` matters: without it `curl` exits 0 +on a 500 and the check passes for answering at all rather than for answering +correctly. + +## Tokens + +`DOPPEL_ADMIN_TOKENS` keeps secrets out of the configuration file that gets +mounted in: + +```bash +docker run --rm \ + -e DOPPEL_ADMIN_TOKENS='{"ci":{"token":"...","group":"admin"}}' \ + ... +``` + +## Reloading + +`doppel config reload` talks to a control socket inside the container. Two ways +in: + +```bash +# Through the admin API, if it is enabled and you hold a token. +curl -X POST -H "X-Proxy-Authorization: Bearer $TOKEN" \ + http://localhost:8081/api/v1/config/reload + +# Or the same binary, inside the container. +docker exec doppel config reload --socket /tmp/doppel.sock +``` + +Editing a bind-mounted `main.yaml` on the host changes the file the container +reads, so a reload picks it up without restarting anything. + +## What the image contains + +Alpine, `ca-certificates`, `tini`, and one statically linked binary. It runs as +a non-root user, so a configuration asking for a port below 1024 fails at +bind -- the right answer in a container; publish a low port on the host +instead. + +`ca-certificates` is not optional: without a trust store a proxy cannot verify +an `https` upstream, and every request fails with `UPSTREAM_ERROR`. + +Doppel runs under `tini`, so `docker stop` delivers `SIGTERM` and the graceful +drain runs -- in-flight requests finish, then the process exits. + +## More + +Full documentation, including fault injection, mocks, the admin API, PostgreSQL +storage and observability, is at . The +Docker page there covers this ground in more detail: +. diff --git a/docs/assets/icon-128.png b/docs/assets/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..70ab06a717ce9fae94c7579a0192d8b8a3740c4c GIT binary patch literal 22960 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 literal 0 HcmV?d00001 From e952c2b4ab6080682b2718de8f8727c9bb986498 Mon Sep 17 00:00:00 2001 From: Lorem Dev Date: Sun, 2 Aug 2026 22:30:27 +0200 Subject: [PATCH 2/2] fix: strip the maintainer note before pushing the overview DOCKERHUB.md opens with an HTML comment explaining what the file is for. Docker Hub renders markdown, and no overview on the hub uses raw HTML, so whether it strips a comment or escapes it into visible text at the top of the page is not worth discovering live. The comment is deleted before the push instead. --- .github/workflows/release.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcfccb9..86b6156 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -253,7 +253,7 @@ jobs: # # Skipped for a pre-release, on the same reasoning as the moving tags: the # page would tell everyone to pull an rc. - - name: Point the overview at the version being released + - name: Prepare the overview for the version being released if: ${{ !contains(github.event.inputs.tag || github.ref_name, '-') }} env: TAG: ${{ github.event.inputs.tag || github.ref_name }} @@ -264,6 +264,15 @@ jobs: # uses bare `1.2.3-alpine` to show the shape and is left alone. sed -i -E "s|loremdev/doppel:[0-9][^[:space:]]*-alpine|loremdev/doppel:${version}-alpine|g" \ DOCKERHUB.md + # Drop the file's own note to maintainers. Docker Hub renders + # markdown, and every overview on the hub that shows an image uses + # `![]()` rather than a tag -- so whether it strips HTML or escapes + # it is not something to find out on a live page. Deleting the + # comment makes the question moot. The range form assumes the + # comment does not wrap anything meant to be published, which is + # what the one at the top of DOCKERHUB.md says it is for. + sed -i '//d' DOCKERHUB.md + head -4 DOCKERHUB.md grep -n 'loremdev/doppel:' DOCKERHUB.md - name: Push the overview to Docker Hub