2026-08-08 23:02:42 +02:00
|
|
|
# phpPgAdmin on Kubernetes
|
2025-10-09 21:31:30 +02:00
|
|
|
|
2026-08-08 23:02:42 +02:00
|
|
|
Production deployment for the brunner.ninja phpPgAdmin service. The repository
|
|
|
|
|
uses a maintained public container image, a single plain Kubernetes manifest,
|
|
|
|
|
Traefik ingress, health probes, zero-unavailable rolling updates, and restricted
|
|
|
|
|
Gitea Actions deployment credentials.
|
|
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
- `tozd/phppgadmin` supplies phpPgAdmin, Nginx, PHP, and PostgreSQL client tools.
|
|
|
|
|
- The image is pinned to an immutable multi-platform manifest digest rather
|
|
|
|
|
than a mutable tag.
|
|
|
|
|
- `default/phppgadmin-config` mounts the complete multi-server configuration at
|
|
|
|
|
`/etc/phppgadmin/config.inc.php`.
|
|
|
|
|
- A ClusterIP Service exposes TCP port 80 to Traefik.
|
|
|
|
|
- `https://phppgadmin.brunner.ninja` redirects to the image's native
|
|
|
|
|
`/phppgadmin/` path.
|
|
|
|
|
- One replica uses `maxUnavailable: 0`, `maxSurge: 1`, and five seconds of
|
|
|
|
|
minimum readiness. The old pod remains available until the replacement has
|
|
|
|
|
passed its HTTP probe continuously for five seconds.
|
|
|
|
|
- CI places the Git commit in a pod-template annotation. This forces a safe
|
|
|
|
|
rollout when a ConfigMap-only change would otherwise leave a `subPath` mount
|
|
|
|
|
unchanged in the running container.
|
|
|
|
|
- Startup, readiness, and liveness probes request `/phppgadmin/` over HTTP.
|
|
|
|
|
|
|
|
|
|
The deployment is stateless. phpPgAdmin does not store database passwords or
|
|
|
|
|
sessions on a PVC; users authenticate directly to the selected PostgreSQL
|
|
|
|
|
server. The ConfigMap lists server addresses but contains no credentials.
|
|
|
|
|
|
|
|
|
|
## Why there is no Dockerfile
|
|
|
|
|
|
|
|
|
|
This repository previously built phpPgAdmin from the Arch User Repository on
|
|
|
|
|
top of a private base image and pushed the result to the private Quay registry.
|
|
|
|
|
That duplicated public image storage and made every repository commit rebuild
|
|
|
|
|
an otherwise unchanged application.
|
|
|
|
|
|
|
|
|
|
The current deployment instead uses the actively maintained public
|
|
|
|
|
[`tozd/phppgadmin`](https://gitlab.com/tozd/docker/phppgadmin) image. It supports
|
|
|
|
|
the same configuration-file mount used here. The pinned image contains
|
|
|
|
|
phpPgAdmin 7.14.7, which is also the current AUR package version at the time of
|
|
|
|
|
this repository conversion.
|
|
|
|
|
|
|
|
|
|
The committed image is:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
tozd/phppgadmin@sha256:4ee690768d54b0c057dbc4ed5c1baa152c1a5ef0ebd6eb12f1e310e545eb4489
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It was inspected and smoke-tested before deployment. Pinning prevents an
|
|
|
|
|
upstream rebuild from silently changing the running software. To upgrade,
|
|
|
|
|
inspect and test the new upstream digest, replace the image line in
|
|
|
|
|
`phppgadmin.yaml`, and merge the change through the normal deployment workflow.
|
|
|
|
|
|
|
|
|
|
## Repository contents
|
|
|
|
|
|
|
|
|
|
- `phppgadmin.yaml`: ConfigMap, Deployment, Service, Ingress, and Traefik root
|
|
|
|
|
redirect Middleware.
|
|
|
|
|
- `install.sh`: server-side dry-run, apply, and rollout verification.
|
|
|
|
|
- `test.sh`: local script and Kubernetes schema validation.
|
|
|
|
|
- `ci-deployer.yaml`: restricted Gitea Actions ServiceAccount, Role, binding,
|
|
|
|
|
and revocable token.
|
|
|
|
|
- `create-ci-kubeconfig.sh`: one-time CI identity bootstrap.
|
|
|
|
|
- `.gitea/workflows/deploy.yml`: validation on every push and pull request;
|
|
|
|
|
deployment after successful validation of `main`.
|
|
|
|
|
|
|
|
|
|
There is no Helm chart, Kustomize overlay, Argo CD application, private image,
|
|
|
|
|
or container build pipeline.
|
|
|
|
|
|
|
|
|
|
## Prerequisites
|
|
|
|
|
|
|
|
|
|
- Kubernetes and kubectl.
|
|
|
|
|
- An amd64 worker node. The pinned upstream image currently publishes amd64.
|
|
|
|
|
- Traefik with the `traefik.io/v1alpha1` Middleware CRD and ingress class
|
|
|
|
|
`traefik`.
|
|
|
|
|
- The existing Traefik `websecure` entrypoint and default
|
|
|
|
|
`*.brunner.ninja` TLS certificate.
|
|
|
|
|
- Network connectivity from the pod to every PostgreSQL hostname listed in the
|
|
|
|
|
ConfigMap.
|
|
|
|
|
|
|
|
|
|
Another cluster operator can reuse the manifest by changing the namespace,
|
|
|
|
|
database server list, ingress hostname, ingress class, and redirect Middleware.
|
|
|
|
|
|
|
|
|
|
## Install manually
|
|
|
|
|
|
|
|
|
|
With an administrator kubeconfig active, run:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
./install.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The script performs a server-side dry-run, applies all resources, and waits up
|
|
|
|
|
to ten minutes for the Deployment. Existing resource names are preserved, so
|
|
|
|
|
the Service keeps its ClusterIP and the change rolls out in place.
|
|
|
|
|
|
|
|
|
|
Verify both application paths:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -I https://phppgadmin.brunner.ninja/
|
|
|
|
|
curl -I https://phppgadmin.brunner.ninja/phppgadmin/
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The first should redirect permanently and the second should return the
|
|
|
|
|
phpPgAdmin application.
|
|
|
|
|
|
|
|
|
|
## Configure PostgreSQL servers
|
|
|
|
|
|
|
|
|
|
Edit `config.inc.php` inside the ConfigMap in `phppgadmin.yaml`. Each server
|
|
|
|
|
entry contains a display name, service DNS name, port, SSL mode, and default
|
|
|
|
|
database. Do not put PostgreSQL usernames or passwords into the ConfigMap or
|
|
|
|
|
Git repository.
|
|
|
|
|
|
|
|
|
|
The file is mounted using `subPath`, so Kubernetes does not update it inside an
|
|
|
|
|
already-running pod. The CI workflow replaces the manifest's `manual` revision
|
|
|
|
|
annotation with the Git commit SHA before applying it, which safely rolls the
|
|
|
|
|
pod for every merged configuration change. A manual `./install.sh` changes the
|
|
|
|
|
annotation back to `manual` and likewise triggers a rollout after a CI deploy.
|
|
|
|
|
|
|
|
|
|
## Validate locally
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
./test.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The test checks both shell scripts and validates the Kubernetes resources with
|
|
|
|
|
kubeconform. The Traefik Middleware is reported as skipped because kubeconform's
|
|
|
|
|
standard schema catalog does not contain cluster-specific CRDs; the Kubernetes
|
|
|
|
|
API validates it during the server-side dry-run.
|
|
|
|
|
|
|
|
|
|
## Automatic deployment with Gitea Actions
|
|
|
|
|
|
|
|
|
|
The workflow validates every push and pull request. A successful push to
|
|
|
|
|
`main` applies the complete `phppgadmin.yaml`, including ConfigMap, Deployment,
|
|
|
|
|
Service, Ingress, and Middleware, then polls the Deployment for up to ten
|
|
|
|
|
minutes.
|
|
|
|
|
|
|
|
|
|
No image is built or pushed. The workflow requires only one Gitea Actions
|
|
|
|
|
secret:
|
|
|
|
|
|
|
|
|
|
- `KUBE_CONFIG_BASE64`: the restricted phpPgAdmin deployment kubeconfig,
|
|
|
|
|
base64-encoded on one line.
|
|
|
|
|
|
|
|
|
|
### One-time CI setup
|
|
|
|
|
|
|
|
|
|
1. Run `./install.sh` with an administrator identity so every application
|
|
|
|
|
resource already exists.
|
|
|
|
|
2. Run:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
./create-ci-kubeconfig.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3. Copy only the final base64 line into the Gitea repository Actions secret
|
|
|
|
|
named `KUBE_CONFIG_BASE64`.
|
|
|
|
|
4. Protect `main`, require successful validation before merging, and review
|
|
|
|
|
manifest and workflow changes.
|
|
|
|
|
5. Push a harmless README change and confirm the deployment job succeeds.
|
|
|
|
|
|
|
|
|
|
The workflow matches the existing brunner.ninja runner: Kubernetes API URL
|
|
|
|
|
`https://host.containers.internal:6443`, with TLS server name `192.168.0.2`.
|
|
|
|
|
Other clusters must change those values.
|
|
|
|
|
|
|
|
|
|
CI can get, patch, and update only these existing named objects:
|
|
|
|
|
|
|
|
|
|
- ConfigMap `phppgadmin-config`;
|
|
|
|
|
- Deployment and Service `phppgadmin`;
|
|
|
|
|
- Ingress `phppgadmin`;
|
|
|
|
|
- Middleware `phppgadmin-root-redirect`.
|
|
|
|
|
|
|
|
|
|
It cannot create or delete resources and cannot call the Kubernetes Secrets
|
|
|
|
|
API. Revoke it by deleting `default/phppgadmin-deployer-token` and the Gitea
|
|
|
|
|
secret. Rerunning the wrapper creates a replacement token.
|
|
|
|
|
|
|
|
|
|
Kubernetes RBAC cannot constrain individual fields inside an allowed
|
|
|
|
|
Deployment patch. A malicious change merged into `main` could alter the pod
|
|
|
|
|
specification. Branch protection and review therefore remain part of the
|
|
|
|
|
security boundary.
|
|
|
|
|
|
|
|
|
|
## Internet exposure
|
|
|
|
|
|
|
|
|
|
This instance is reachable from the public internet and presents direct
|
|
|
|
|
PostgreSQL username/password authentication. Use long, unique database
|
|
|
|
|
passwords, restrictive PostgreSQL roles and `pg_hba.conf` rules, TLS for remote
|
|
|
|
|
database connections, and external rate limiting or access control where
|
|
|
|
|
appropriate. phpPgAdmin's `extra_login_security` remains enabled in the
|
|
|
|
|
committed configuration.
|