Add README-dockerhub.md - the hand-maintained Docker Hub repo description #9

Merged
lilleman merged 2 commits from readme-dockerhub into main 2026-07-04 21:50:12 +02:00
3 changed files with 200 additions and 2 deletions
+193
View File
@@ -0,0 +1,193 @@
# Plainpages
A self-hostable foundation for server-rendered web apps — public or gated pages from a
zero-JS design system, with a config-driven menu and auth/permissions (Ory) baked in.
Every domain feature is a drop-in plugin folder; the app is stateless, no build step.
**Source, docs & issues: <https://gitea.larvit.se/larvit/plainpages>**
([GitHub mirror](https://github.com/larvit/plainpages))
## Tags
`X.Y.Z` · `X.Y` · `X` · `latest` — each is a release promoted from a CI-gated build.
Pin the exact `X.Y.Z` you deploy.
## Quick start
This image is the Plainpages web app plus its one-shot bootstrap seeder. It runs
alongside its Ory sidecars (Kratos, Keto) and Postgres — and it **ships their config**,
so there is nothing to clone. In an empty directory, save this as `compose.yml`:
```yaml
services:
web:
image: larvit/plainpages:0.0.2
ports:
- "3000:3000"
environment:
APP_URL: http://localhost:3000
depends_on:
bootstrap:
condition: service_completed_successfully
kratos:
condition: service_healthy
keto:
condition: service_healthy
volumes:
- ./ory/kratos/tokenizer:/etc/config/kratos/tokenizer:ro
- ./plugins:/app/plugins
restart: unless-stopped
# One-shot, idempotent seed: signing key if absent + the admin@plainpages.local / admin user.
bootstrap:
image: larvit/plainpages:0.0.2
command: node src/auth/bootstrap.ts
depends_on:
kratos:
condition: service_healthy
keto:
condition: service_healthy
volumes:
- ./ory/kratos/tokenizer:/etc/config/kratos/tokenizer
- ./plugins:/app/plugins:ro
restart: "on-failure:5"
postgres:
image: postgres:18.4-alpine3.23
environment:
POSTGRES_DB: ory
POSTGRES_PASSWORD: ory
POSTGRES_USER: ory
volumes:
- ./ory/postgres/init:/docker-entrypoint-initdb.d:ro
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ory -d ory"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
kratos-migrate:
image: oryd/kratos:v26.2.0
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
depends_on:
postgres:
condition: service_healthy
environment:
DSN: postgres://ory:ory@postgres:5432/kratos?sslmode=disable
volumes:
- ./ory/kratos:/etc/config/kratos:ro
restart: on-failure
kratos:
image: oryd/kratos:v26.2.0
command: serve -c /etc/config/kratos/kratos.yml --watch-courier
ports:
- "4433:4433" # the login form POSTs straight to Kratos from the browser
depends_on:
kratos-migrate:
condition: service_completed_successfully
environment:
DSN: postgres://ory:ory@postgres:5432/kratos?sslmode=disable
volumes:
- ./ory/kratos:/etc/config/kratos:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4433/health/ready"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
keto-migrate:
image: oryd/keto:v26.2.0
command: -c /etc/config/keto/keto.yml migrate up -y
depends_on:
postgres:
condition: service_healthy
environment:
DSN: postgres://ory:ory@postgres:5432/keto?sslmode=disable
volumes:
- ./ory/keto:/etc/config/keto:ro
restart: on-failure
keto:
image: oryd/keto:v26.2.0
command: serve -c /etc/config/keto/keto.yml
depends_on:
keto-migrate:
condition: service_completed_successfully
environment:
DSN: postgres://ory:ory@postgres:5432/keto?sslmode=disable
volumes:
- ./ory/keto:/etc/config/keto:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4466/health/ready"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
# Catches Kratos' recovery/verification emails — UI on http://localhost:8025
mailpit:
image: axllent/mailpit:v1.30.1
ports:
- "8025:8025"
restart: unless-stopped
volumes:
pgdata:
```
Extract the Ory config the image ships, then start:
```bash
docker run --rm larvit/plainpages:0.0.2 tar -cf - ory | tar -xf -
mkdir -p plugins
docker compose up -d
```
Open <http://localhost:3000> and sign in as `admin@plainpages.local` / `admin`.
This quick start runs http-on-localhost with dev-throwaway secrets, and omits Hydra (the
OAuth2 provider — only needed when other apps log in *through* Plainpages). For
production — https, real secrets (`CSRF_SECRET`, Postgres credentials, a fresh JWT
signing key), Hydra — see the repo README → Production & deployment.
## Configuration
Every behaviour is an explicit env toggle read at boot — no `NODE_ENV`. The common ones:
| Var | Default | What |
| --- | --- | --- |
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | `admin@plainpages.local` / `admin` | the seeded first admin (bootstrap service) |
| `APP_URL` | unset | canonical public URL; off-host visitors are redirected to it |
| `CACHE_TEMPLATES` | `false` | cache compiled templates (`true` in prod) |
| `CSRF_SECRET` | dev throwaway | signs the CSRF token — set a real one in prod |
| `KRATOS_*` / `KETO_*` / `HYDRA_*` URLs | compose defaults | the Ory sidecar endpoints |
| `LOG_FORMAT` / `LOG_LEVEL` | `text` / `info` | `json` for structured prod logs |
| `OTLP_ENDPOINT` | unset | export logs + traces to an OpenTelemetry Collector |
| `REQUIRE_SECURE_SECRETS` | `false` | `true` ⇒ refuse to boot on a missing/throwaway `CSRF_SECRET` |
| `SECURE_COOKIES` | `false` | mark cookies `Secure` (`true` behind https) |
Full list (JWT/JWKS, timeouts, instant revoke): repo README → Configuration.
## Your first plugin
Everything domain-specific is a plugin folder — the compose above mounts `./plugins`
into the app. Create `plugins/hello/plugin.ts`:
```ts
import { definePlugin } from "#plugin-api";
export default definePlugin({
apiVersion: "1.0.0",
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
routes: [
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
],
});
```
Restart (`docker compose restart web`) and visit <http://localhost:3000/hello>. Views,
forms, permissions, and the runnable reference plugin: repo README → Building plugins.
+5 -1
View File
@@ -1224,7 +1224,10 @@ repository on Docker Hub, generate a read/write access token **scoped to that re
account-wide PAT can push to every repo under the account), and store the account name as account-wide PAT can push to every repo under the account), and store the account name as
the Actions **variable** `DOCKERHUB_USER` and the token as the Actions **secret** the Actions **variable** `DOCKERHUB_USER` and the token as the Actions **secret**
`DOCKERHUB_TOKEN`. Until they exist, a release run fails at the Docker Hub step — after the `DOCKERHUB_TOKEN`. Until they exist, a release run fails at the Docker Hub step — after the
Gitea re-tag has succeeded — so set them, then re-run the workflow. Gitea re-tag has succeeded — so set them, then re-run the workflow. The Docker Hub
repository **description** is maintained by hand: its source is
[`README-dockerhub.md`](README-dockerhub.md) — paste it into the repository overview on
Docker Hub when it changes.
**GitHub mirror** — [github.com/larvit/plainpages](https://github.com/larvit/plainpages) is a **GitHub mirror** — [github.com/larvit/plainpages](https://github.com/larvit/plainpages) is a
read-only mirror; after every merge, `mirror.yml` force-pushes `main` and all tags there, read-only mirror; after every merge, `mirror.yml` force-pushes `main` and all tags there,
@@ -1460,6 +1463,7 @@ e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + aut
ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`) ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`)
.gitea/workflows/ Gitea Actions: ci.yml — the full gate (ci.sh) on every branch push except main; .gitea/workflows/ Gitea Actions: ci.yml — the full gate (ci.sh) on every branch push except main;
mirror.yml — force-sync main + tags to the GitHub mirror; see CI/CD mirror.yml — force-sync main + tags to the GitHub mirror; see CI/CD
README-dockerhub.md The Docker Hub repository description (docker.io/larvit/plainpages) — pasted into the Docker Hub overview by hand when it changes; see CI/CD
``` ```
## Extending the core ## Extending the core
+2 -1
View File
@@ -9,7 +9,8 @@
- [x] CI/CD - Build docker images as part of the requirements to be able to merge to main. Push them with the git commit hash as docker tag. Push to container registry at Gitea. (`ci.yml` builds + pushes `gitea.larvit.se/larvit/plainpages:<commit hash>` after a green gate — with ff-only merges that is the main commit's image; auth via the `DOCKER_REGISTRY_USER` variable + `DOCKER_REGISTRY_TOKEN` secret, retention via an org cleanup rule; documented in README → CI/CD.) - [x] CI/CD - Build docker images as part of the requirements to be able to merge to main. Push them with the git commit hash as docker tag. Push to container registry at Gitea. (`ci.yml` builds + pushes `gitea.larvit.se/larvit/plainpages:<commit hash>` after a green gate — with ff-only merges that is the main commit's image; auth via the `DOCKER_REGISTRY_USER` variable + `DOCKER_REGISTRY_TOKEN` secret, retention via an org cleanup rule; documented in README → CI/CD.)
- [x] CI/CD - Re-tag docker images from git hash to semver when a semver git tag is pushed. (`release.yml` on a `vX.Y.Z` tag pulls the commit-hash image and re-tags it `X.Y.Z`/`X.Y`/`X`/`latest`, failing loud if the gated image is missing; tag pushes also trigger the GitHub mirror; documented in README → CI/CD.) - [x] CI/CD - Re-tag docker images from git hash to semver when a semver git tag is pushed. (`release.yml` on a `vX.Y.Z` tag pulls the commit-hash image and re-tags it `X.Y.Z`/`X.Y`/`X`/`latest`, failing loud if the gated image is missing; tag pushes also trigger the GitHub mirror; documented in README → CI/CD.)
- [x] CI/CD - Sync docker images to docker hub after each re-tag to git tags. (`release.yml` pushes the same `X.Y.Z`/`X.Y`/`X`/`latest` tags to `docker.io/larvit/plainpages` after the Gitea re-tag — releases only, no hash tags; auth via the `DOCKERHUB_USER` variable + `DOCKERHUB_TOKEN` secret; documented in README → CI/CD.) - [x] CI/CD - Sync docker images to docker hub after each re-tag to git tags. (`release.yml` pushes the same `X.Y.Z`/`X.Y`/`X`/`latest` tags to `docker.io/larvit/plainpages` after the Gitea re-tag — releases only, no hash tags; auth via the `DOCKERHUB_USER` variable + `DOCKERHUB_TOKEN` secret; documented in README → CI/CD.)
- [ ] CI/CD - Setup renovate bot. - [x] Write a short text on how to use this docker image to publish on docker hub and save it to README-dockerhub.md (tagline, tags, clone-free quick start — the image ships the Ory config, extracted via `docker run … tar` + a self-contained compose.yml — env table, first plugin; pasted into the Docker Hub overview by hand — noted in README → CI/CD.)
- [ ] CI/CD - Setup renovate bot. Check how other repos on this Gitea is setup you can get access to, there should be a number of renovate bot activated ones.
- [ ] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser. - [ ] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser.
- [ ] Build and publish docker image as CI/CD. - [ ] Build and publish docker image as CI/CD.
- [ ] Add i18n support. - [ ] Add i18n support.