What every KTHAIS project runs: its Dokploy project, its OpenBao secrets wiring, and the exact image of each environment. Merging here is deploying.
The design is in
kthaisociety/infrastructure: docs/delivery-plan.md,
the step-by-step guide in
docs/app-delivery.md.
infrastructure holds the platform underneath (OpenBao, Dokploy core, storage); this repo holds the
projects on top of it.
projects/<project>/
project.yaml # config, secret names, environments (staging, production)
release.yaml # the image each environment runs, tag@digest; written by the deploy bot
terraform/ # one OpenTofu root: OpenBao (provider tokens) and Dokploy
modules/project/ # a project's Dokploy side: project, environments, vault providers, apps, volumes, domains, databases, backups
.github/workflows/
pr.yml # PR title
tofu.yml # checks and plan on PRs; apply on main, weekly and by hand
deploy.yml # deploy requests: validate, commit the release.yaml line, apply, report (one run)
docs/ # the project.yaml guide, schema and template; decision records in adr/ and pcr/
scripts/ # check-project-yaml, the same project.yaml check `check` runs on every PR
tests/ # pytest tests for scripts/
A project's OpenBao side (its policies and empty secret paths) lives in
kthaisociety/infrastructure's terraform/openbao:
policies decide what a token can read, so this repo's CI can't write them. It mints provider tokens,
and writes the credentials of the databases it creates to secret/<project>/<env>/database/*: nothing
else in OpenBao.
For the project.yaml fields and a complete checked example, read the
project.yaml guide. Start a new project from
the template. Run scripts/check-project-yaml before pushing; it
reports the same problems the PR's check does.
infrastructure: one line interraform/openbao/projects.yaml,my-app: {}(staging and production). That creates its policies and empty secret paths. Every project can read the shared store for its environment,secret/shared/<environment>, with no extra line. This repo'scheckfails, with that line, until it's there.- Write its secrets in OpenBao (
secret/<project>/<environment>). - Here, one PR:
projects/<project>/project.yaml;projects/<project>/release.yamlwith every environmentnull; and the project added to theprojectoptions in.github/workflows/deploy.yml(the dropdown;checkfails if it's missing). Merging creates the Dokploy project, environments, vault providers, and each environment's app and volumes, not deployed. - Deploy with the deploy workflow (or the app repo's
deploy-stagingjob). From then on only the deploy workflow changesrelease.yaml.
- Add the domain under the environment in
projects/<project>/project.yaml. See the project.yaml guide for the domain fields and examples. - Left out: HTTPS on, with a Let's Encrypt certificate; everything else is Dokploy's default. A misspelt setting fails the plan instead of being ignored.
- Merging creates the domain in Dokploy. Removing its lines removes it; other domains are untouched.
- Everything changes by PR, squash-merged, with a Conventional Commit title, and a code owner's approval.
- Except
release.yaml: only the deploy workflow changes it (it commits the line tomainaskthais-deployand applies in the same run). PRs that edit an existingrelease.yamlfailcheck. - Public by design: no secret value is ever in this repo. Values live in OpenBao.
- Automatically, from an app repo that has opted in (a
request-deployjob in itsbuild.ymlfor staging, in itsrelease.ymlfor production): its CI startsdeploy.ymlhere with the project, environment and tag, and waits for the result. - By hand: Actions → deploy → Run workflow: pick the project and environment from the dropdowns,
type the tag (
sha-<7>orX.Y.Z). Rolling back is the same, with a previous tag.
One run does the whole deploy:
- Validate the request, and look up the tag's digest in GHCR. For production: the tag must be a
release (
X.Y.Zwith a GitHub Release), built from the release commit. - Commit the one
release.yamlline tomainaskthais-deploy(signed by GitHub), unless it's already there. tofu applymain, in the same run, and report the result.
Applies (deploys and tofu runs) go one at a time. If two more requests arrive while one runs, the older
waiting one is cancelled by GitHub; its caller sees "cancelled" and can re-run it.