added opt-in managed nginx/TLS container (ADR 0005, plan step 13): server.nginx.* config serves GitTally over HTTPS on hosts without a reverse proxy — two-phase startup (ACME webroot via certbot container, then full HTTPS config), daily certificate renewal with nginx reload, labelled container removed on shutdown; all failures are non-fatal, the plain HTTP server keeps running
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
fdbbd0516a
commit
b104eeee05
@@ -32,6 +32,26 @@ server:
|
||||
bindAddress: 0.0.0.0
|
||||
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
||||
impressumUrl: ""
|
||||
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
|
||||
# a usable reverse proxy (ADR 0005; see notes below and deployment.md).
|
||||
nginx:
|
||||
# manage an nginx Docker container with Let's Encrypt certificates
|
||||
enabled: false
|
||||
# public DNS name served by nginx and used for the certificate; required when enabled
|
||||
serverName: ""
|
||||
# host port published as nginx port 80 (ACME challenge + HTTPS redirect)
|
||||
httpPort: 8080
|
||||
# host port published as nginx port 443
|
||||
httpsPort: 8443
|
||||
# host nginx proxies to; empty = serverName (the container cannot reach localhost)
|
||||
upstreamHost: ""
|
||||
# name of the managed container; empty = gittally-nginx-<repo-name>
|
||||
containerName: ""
|
||||
# directory for nginx config, certificates, and logs;
|
||||
# empty = $XDG_STATE_HOME (or ~/.local/state) plus /gittally/nginx/<repo-key>
|
||||
stateDir: ""
|
||||
# e-mail for the Let's Encrypt account; empty registers without one
|
||||
letsencryptEmail: ""
|
||||
|
||||
# Gitea integration for fetching commits and posting build statuses.
|
||||
gitea:
|
||||
@@ -127,6 +147,16 @@ branches:
|
||||
- "04:00"
|
||||
```
|
||||
|
||||
### Notes on `server.nginx`
|
||||
|
||||
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves GitTally over HTTPS (ADR 0005).
|
||||
This is meant for hosts that provide Docker but no usable reverse proxy (e.g. Hostsharing managed containers); otherwise prefer the reverse-proxy setup in [deployment.md](deployment.md).
|
||||
Certificates are obtained and renewed via Let's Encrypt (certbot Docker container, webroot mode), so `serverName` must be a public DNS name pointing at the host and `httpPort` must be reachable from the internet as port 80 (or via a port forward).
|
||||
When `server.publicBaseUrl` is empty and `serverName` is set, it defaults to `https://<serverName>/`.
|
||||
All nginx/certificate failures are non-fatal warnings; the plain HTTP server keeps running without the proxy.
|
||||
The container is labelled `org.hoennig.gittally`; stale nginx containers of the repository are removed before each start, and the container is removed on shutdown.
|
||||
`server.port` must differ from `httpPort` and `httpsPort`.
|
||||
|
||||
### Notes on `branches.<name>.requirePullRequest`
|
||||
|
||||
The gate applies to all watcher-triggered builds (push-triggered and scheduled auto builds).
|
||||
|
||||
+31
-2
@@ -2,7 +2,8 @@
|
||||
|
||||
This document describes how to run GitTally as a permanent service.
|
||||
The recommended setup is a systemd user service behind an existing reverse proxy.
|
||||
GitTally does not manage nginx or TLS certificates itself (unlike the legacy script); it relies on the host's existing web server and certbot.
|
||||
By default GitTally does not manage nginx or TLS certificates itself; it relies on the host's existing web server and certbot.
|
||||
For hosts without one, an opt-in managed nginx/TLS container is available, see [Hosts Without a Reverse Proxy](#hosts-without-a-reverse-proxy-managed-nginxtls).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -122,4 +123,32 @@ sudo certbot --nginx -d ci.example.org
|
||||
```
|
||||
|
||||
This replaces the legacy script's managed nginx/Let's Encrypt Docker container for hosts that have their own web server.
|
||||
For hosts without a usable reverse proxy (e.g. Hostsharing managed containers), an opt-in managed nginx/TLS container is planned (see `docs/plan/13-nginx-tls.md`, ADR 0005).
|
||||
|
||||
## Hosts Without a Reverse Proxy (Managed nginx/TLS)
|
||||
|
||||
Some hosts provide Docker but no root access and no host web server, e.g. Hostsharing managed container environments.
|
||||
For these, GitTally can manage its own nginx+certbot Docker container (ADR 0005).
|
||||
This is opt-in; where a host web server exists, prefer the reverse-proxy setup above.
|
||||
|
||||
Enable it in the server section of the configuration:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 18080
|
||||
nginx:
|
||||
enabled: true
|
||||
serverName: ci.example.org
|
||||
httpPort: 8080
|
||||
httpsPort: 8443
|
||||
letsencryptEmail: admin@example.org
|
||||
```
|
||||
|
||||
On server start, GitTally writes the nginx configuration, starts a labelled nginx container publishing `httpPort` and `httpsPort`, obtains a Let's Encrypt certificate via a certbot container (webroot mode), and restarts nginx with the full HTTPS configuration.
|
||||
A renewal check runs daily; certificates and nginx state persist in `server.nginx.stateDir` across restarts.
|
||||
On shutdown the container is removed.
|
||||
All nginx and certificate failures are non-fatal warnings — the plain HTTP server keeps running without the proxy.
|
||||
|
||||
`serverName` must be a public DNS name pointing at the host, reachable from the internet on port 80/443 (directly or via a port forward to `httpPort`/`httpsPort`), otherwise the ACME challenge fails.
|
||||
The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers.
|
||||
With the managed nginx, keep `server.bindAddress: 0.0.0.0` (or an address reachable from the Docker network) — binding GitTally to `127.0.0.1` would make it unreachable for the proxy.
|
||||
See [configuration.md](configuration.md) for all `server.nginx.*` keys.
|
||||
|
||||
@@ -37,13 +37,18 @@ Branch-level keys below live under `branches.<name>`; use `branches.default` for
|
||||
| `GITTALLY_GITEA_STATUS_CONTEXT` | `gitea.statusContext` |
|
||||
| `GITTALLY_GITEA_GIT_USERNAME` | `git.account` — in `.git/gittally/.gittally.yml` |
|
||||
| `GITTALLY_GITEA_TOKEN` | `git.token` — in `.git/gittally/.gittally.yml`, never committed |
|
||||
| `GITTALLY_ARTIFACT_NGINX_SERVER_NAME` | `server.nginx.serverName` — also set `server.nginx.enabled: true` (replaces the `--nginx` flag) |
|
||||
| `GITTALLY_ARTIFACT_NGINX_HTTP_PORT` | `server.nginx.httpPort` |
|
||||
| `GITTALLY_ARTIFACT_NGINX_HTTPS_PORT` | `server.nginx.httpsPort` |
|
||||
| `GITTALLY_ARTIFACT_NGINX_UPSTREAM_HOST` | `server.nginx.upstreamHost` |
|
||||
| `GITTALLY_ARTIFACT_NGINX_CONTAINER_NAME` | `server.nginx.containerName` |
|
||||
| `GITTALLY_ARTIFACT_NGINX_STATE_DIR` | `server.nginx.stateDir` |
|
||||
| `GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL` | `server.nginx.letsencryptEmail` |
|
||||
|
||||
New keys without a legacy counterpart: `builds.maxConcurrent`, `artifacts.rootDir`, and `watcher.pollInterval`.
|
||||
|
||||
## Intentionally Not Ported
|
||||
|
||||
- Managed nginx/Let's Encrypt container (`GITTALLY_ARTIFACT_NGINX_*`, `GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL`) — not ported yet, but planned as an opt-in feature for hosts without a reverse proxy (see `docs/plan/13-nginx-tls.md`).
|
||||
Until then, use the host's reverse proxy, see [deployment.md](deployment.md).
|
||||
- Self-install and self-update (`--install`, `--pull`, `GITTALLY_INSTALL_DIR`) — replaced by jar deployment plus `init --systemd`.
|
||||
- `GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND` and `GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS` — hsadmin-ng-specific; use `branches.<name>.docker.env` if needed.
|
||||
- `HSADMIN_NG_*` environment-variable fallbacks.
|
||||
|
||||
@@ -39,3 +39,25 @@ Port the legacy nginx subsystem (functions `configure_artifact_nginx_defaults` ~
|
||||
- Manual walkthrough on a Docker host: nginx container starts with the init config and proxies HTTP to GitTally.
|
||||
Full ACME issuance needs a public DNS name; if none is available, verify the certbot argv and the full-config path against the legacy script and document that in this file.
|
||||
- `docs/deployment.md` gains a section for hosts without a reverse proxy; `docs/migration-from-legacy.md` maps the `GITTALLY_ARTIFACT_NGINX_*`/`GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL` variables.
|
||||
|
||||
## Result (2026-07-08)
|
||||
|
||||
Implemented as `NginxConfigFiles` (config generation), `NginxProxyManager` (docker orchestration), and `ServerNginxLifecycle` (server-profile startup, daily renewal, shutdown cleanup).
|
||||
|
||||
Deviations from the design above and from legacy:
|
||||
|
||||
- Renewal reloads nginx via `docker exec <container> nginx -s reload` after `certbot renew` instead of restarting the container (no dropped connections).
|
||||
- Legacy auto-moved the artifact server port on a collision with the nginx ports; the rewrite refuses to start the proxy with a warning instead — the Spring port cannot move after startup.
|
||||
- Legacy derived a missing `serverName` from the public base URL host; the rewrite requires `serverName` explicitly (the config default direction is only publicBaseUrl ← serverName).
|
||||
- `serverName` and `upstreamHost` are validated against a host-name pattern instead of substituting raw values, so no nginx directives can be injected via config.
|
||||
- The container label namespace is `org.hoennig.gittally` (like the Docker build runner), not `org.hostsharing.gittally`; port cleanup still also matches legacy-named containers.
|
||||
- The legacy `--nginx` CLI flag is not ported; enablement is `server.nginx.enabled` only.
|
||||
- `ssl-dhparams.pem` is downloaded via the Java HTTP client instead of `curl` (replaceable seam for tests).
|
||||
|
||||
Manual walkthrough (dev machine, Rancher Desktop Docker, no public DNS name — so no real ACME issuance):
|
||||
|
||||
- Fresh state dir: nginx container started with the init config; port 80 served the ACME challenge location and answered `301 https://<serverName>/...`; `certonly` then failed as expected without public DNS and the HTTP server kept running (non-fatal path).
|
||||
- Pre-seeded certificate (self-signed): manager took the full-config path, `certbot renew` ran (exit 0), and HTTPS end-to-end worked — `/api/branches` JSON served through the TLS proxy with the configured no-cache headers.
|
||||
- `SIGTERM` removed the container (lifecycle `@PreDestroy`).
|
||||
- The certbot `certonly`/`renew` argv and both config modes are asserted verbatim against the legacy script in `NginxProxyManagerTest`/`NginxConfigFilesTest`.
|
||||
- Environment note: the nginx container cannot reach the host's `localhost`; on the walkthrough machine the upstream had to be `host.docker.internal` (documented in `deployment.md` as `upstreamHost`).
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ Completion:
|
||||
|
||||
Added after the initial plan (ADR 0005):
|
||||
|
||||
- [ ] `13-nginx-tls.md` — opt-in managed nginx/TLS container for hosts without a reverse proxy
|
||||
- [x] `13-nginx-tls.md` — opt-in managed nginx/TLS container for hosts without a reverse proxy
|
||||
|
||||
Steps 01–03 are independent of each other.
|
||||
Steps 04–06 depend on 01–03.
|
||||
|
||||
Reference in New Issue
Block a user