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:
Michael Hoennig
2026-07-08 21:51:33 +02:00
co-authored by Claude Fable 5
parent fdbbd0516a
commit b104eeee05
15 changed files with 1191 additions and 9 deletions
@@ -21,11 +21,24 @@ class ConfigLoader {
fun load(workingDir: Path = Paths.get(".")): GitTallyConfig {
val raw = loadRaw(workingDir)
return if (raw.isEmpty()) {
GitTallyConfig()
} else {
yaml.convertValue(mergeBranchDefaults(raw), GitTallyConfig::class.java)
val config =
if (raw.isEmpty()) {
GitTallyConfig()
} else {
yaml.convertValue(mergeBranchDefaults(raw), GitTallyConfig::class.java)
}
return defaultPublicBaseUrl(config)
}
/** Legacy default: an empty `server.publicBaseUrl` becomes `https://<nginx.serverName>/`. */
private fun defaultPublicBaseUrl(config: GitTallyConfig): GitTallyConfig {
if (config.server.publicBaseUrl.isNotBlank() ||
config.server.nginx.serverName
.isBlank()
) {
return config
}
return config.copy(server = config.server.copy(publicBaseUrl = "https://${config.server.nginx.serverName}/"))
}
fun loadRaw(workingDir: Path = Paths.get(".")): Map<String, Any?> {
@@ -11,12 +11,44 @@ data class GitTallyConfig(
)
data class ServerConfig(
/**
* Public base URL of this installation; empty defaults to `https://<nginx.serverName>/`
* when [NginxConfig.serverName] is set (applied by [ConfigLoader]).
*/
val publicBaseUrl: String = "",
/** HTTP port of the `server` subcommand; 18080 like the legacy artifact server. */
val port: Int = 18080,
val bindAddress: String = "0.0.0.0",
/** Optional Impressum (legal disclosure) link shown in the web UI footer; empty hides the link. */
val impressumUrl: String = "",
val nginx: NginxConfig = NginxConfig(),
)
/**
* Opt-in managed nginx+certbot Docker container serving GitTally over HTTPS,
* for hosts without a usable reverse proxy (ADR 0005). Off by default; the
* reverse-proxy deployment from `docs/deployment.md` stays the recommended setup.
*/
data class NginxConfig(
/** Manage an nginx Docker container with Let's Encrypt certificates. */
val enabled: Boolean = false,
/** Public DNS name served by nginx and used for the certificate; required when [enabled]. */
val serverName: String = "",
/** Host port published as nginx port 80 (ACME challenge + HTTPS redirect). */
val httpPort: Int = 8080,
/** Host port published as nginx port 443. */
val httpsPort: Int = 8443,
/** Host nginx proxies to; empty uses [serverName] (the container cannot reach `localhost`). */
val upstreamHost: String = "",
/** Name of the managed container; empty means `gittally-nginx-<repo-name>`. */
val containerName: String = "",
/**
* Directory for nginx config, certificates, and logs; empty means the platform
* default `XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/nginx/<repo-key>`.
*/
val stateDir: String = "",
/** E-mail for the Let's Encrypt account; empty registers without one. */
val letsencryptEmail: String = "",
)
data class GitConfig(