Files
plainpages/README-dockerhub.md
lilleman 476ef6fce2
CI / full-gate (push) Successful in 2m30s
Mirror / github-mirror (push) Successful in 0s
README-dockerhub: clone-free quick start - self-contained compose, Ory config extracted from the image
2026-07-04 17:45:44 +02:00

6.2 KiB

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)

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:

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:

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:

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.