Finishes the small items of the security audit in docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7. The three mutating endpoints of BuildsApiController no longer accept the control token as a `token` query parameter — only the X-GitTally-Token header, which the bundled UI has always used. URLs end up in access logs, proxy logs, browser history and Referer headers, and the token never expires, so a historical log capture would yield a valid credential. `config:print` masks git.token as `***` on both the raw and the --full path and names the new --show-secrets flag in a leading YAML comment, so the output stays parseable when piped. The setup script points at --show-secrets where it used to steer the operator to the plain token. `server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the API authenticates read access, so reaching GitTally should require the host's reverse proxy. Existing .gittally.yml files keep their explicit value; the managed nginx container needs `0.0.0.0` set deliberately, which is noted in the release notes, docs/configuration.md and docs/deployment.md. Released as v0.9.9, which also carries the previous two commits. Co-Authored-By: Claude <noreply@anthropic.com>
202 lines
8.7 KiB
Markdown
202 lines
8.7 KiB
Markdown
# GitTally Deployment
|
|
|
|
This document describes how to run GitTally as a permanent service.
|
|
The recommended setup is a systemd user service behind an existing reverse proxy.
|
|
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
|
|
|
|
- Linux with systemd.
|
|
- Java runtime (JRE 21) — or none, when using the self-contained runtime bundle, see [Hosts Without a Java Runtime](#hosts-without-a-java-runtime-runtime-bundle).
|
|
- `git` CLI on the `PATH`.
|
|
- `docker` CLI on the `PATH`, only if any branch uses `docker.enabled` (see [configuration.md](configuration.md)).
|
|
- A checked-out working tree of the repository to watch, with a remote named `origin`.
|
|
|
|
## Jar Location Convention
|
|
|
|
Build the executable jar once:
|
|
|
|
```bash
|
|
./gradlew build
|
|
ls build/libs/gittally.jar
|
|
```
|
|
|
|
Copy the jar to a stable path outside any watched repository, by convention `~/bin/gittally.jar`:
|
|
|
|
```bash
|
|
mkdir -p ~/bin
|
|
cp build/libs/gittally.jar ~/bin/gittally.jar
|
|
```
|
|
|
|
The systemd unit generated below points at the jar that was used to run `init --systemd`.
|
|
So always run it via the stable path, not via `build/libs/`.
|
|
|
|
## Install the Service
|
|
|
|
Initialize GitTally in the repository to watch (see [bootstrapping.md](bootstrapping.md) for details):
|
|
|
|
```bash
|
|
cd /path/to/repo
|
|
java -jar ~/bin/gittally.jar init
|
|
# fill in git.account and git.token in .git/gittally/.gittally.yml
|
|
# review .gittally.yml
|
|
```
|
|
|
|
Generate the systemd user unit:
|
|
|
|
```bash
|
|
java -jar ~/bin/gittally.jar init --systemd
|
|
```
|
|
|
|
This writes `.git/gittally/gittally-<repo-name>.service`, `.git/gittally/gittally.env`, and the nightly Docker cleanup units (`gittally-docker-prune.service`/`.timer`), and prints the install commands:
|
|
|
|
```bash
|
|
ln -sf /path/to/repo/.git/gittally/gittally-<repo-name>.service ~/.config/systemd/user/gittally-<repo-name>.service
|
|
ln -sf /path/to/repo/.git/gittally/gittally-docker-prune.service ~/.config/systemd/user/gittally-docker-prune.service
|
|
ln -sf /path/to/repo/.git/gittally/gittally-docker-prune.timer ~/.config/systemd/user/gittally-docker-prune.timer
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now gittally-<repo-name>.service
|
|
systemctl --user enable --now gittally-docker-prune.timer
|
|
```
|
|
|
|
The unit runs `java -jar ~/bin/gittally.jar server` with the repository as working directory and `Restart=always`.
|
|
The unit name contains the repository name, so several repositories can be served by one host, each with its own service and port.
|
|
|
|
### Nightly Docker Cleanup
|
|
|
|
The `gittally-docker-prune.timer` runs `docker system prune -af` every night at 02:00 (host time), before the usual auto-build slots.
|
|
It removes stopped containers, unused images, unused networks, and dangling build cache, so nightly builds start from freshly built images.
|
|
Unlike the legacy cleanup it does **not** prune volumes — the per-repository Gradle cache volumes survive.
|
|
The units are host-global (no repository name): with several GitTally instances on one host, every `init --systemd` generates the same files and the symlinks coincide.
|
|
On hosts without a `docker` CLI the service is skipped, not failed (`ExecCondition`).
|
|
`Persistent=true` catches up a missed run after downtime.
|
|
|
|
Expect a burst of builds right after the very first start: in a fresh clone every origin branch counts as new, so each branch with commits younger than `watcher.newBranchMaxAge` (default 5d) is built once — one build per branch, executed serially up to `builds.maxConcurrent`.
|
|
Lower `watcher.newBranchMaxAge` before the first start, or enable `requirePullRequest`, to limit the initial backlog.
|
|
|
|
User services stop at logout unless lingering is enabled once per user:
|
|
|
|
```bash
|
|
loginctl enable-linger "$USER"
|
|
```
|
|
|
|
## Operating the Service
|
|
|
|
```bash
|
|
systemctl --user status gittally-<repo-name>.service # state and last log lines
|
|
journalctl --user -u gittally-<repo-name>.service -f # follow the log
|
|
systemctl --user restart gittally-<repo-name>.service # restart (e.g. after config changes)
|
|
systemctl --user stop gittally-<repo-name>.service # stop
|
|
```
|
|
|
|
To update GitTally, replace `~/bin/gittally.jar` and restart the service.
|
|
|
|
## Environment File
|
|
|
|
`.git/gittally/gittally.env` is loaded by the unit as `EnvironmentFile`.
|
|
It only tunes the JVM process, e.g. `JAVA_OPTS=-Xmx256m`.
|
|
All GitTally configuration lives in the YAML files described in [configuration.md](configuration.md), not in environment variables.
|
|
`init --systemd` never overwrites an existing environment file.
|
|
|
|
## Reverse Proxy (nginx)
|
|
|
|
Bind GitTally to localhost — the default since v0.9.9 — and set the public URL in `.gittally.yml`:
|
|
|
|
```yaml
|
|
server:
|
|
bindAddress: 127.0.0.1
|
|
port: 18080
|
|
publicBaseUrl: "https://ci.example.org/"
|
|
```
|
|
|
|
`publicBaseUrl` is used for all links posted to Gitea, so it must be the externally reachable URL.
|
|
|
|
Add a `server` block to the host's nginx:
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl;
|
|
server_name ci.example.org;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/ci.example.org/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/ci.example.org/privkey.pem;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:18080;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
}
|
|
```
|
|
|
|
Obtain and renew the certificate with the host's existing certbot, e.g.:
|
|
|
|
```bash
|
|
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.
|
|
|
|
## Hosts Without a Java Runtime (Runtime Bundle)
|
|
|
|
Some hosts provide git and Docker but no Java runtime and no way to install one, e.g. Hostsharing container servers.
|
|
For these, GitTally ships as a self-contained runtime bundle: a jlink-trimmed JRE, `gittally.jar`, and a launcher script in one tarball (ADR 0006).
|
|
|
|
Build the bundle on a Linux x86_64 machine whose glibc is not newer than the target's:
|
|
|
|
```bash
|
|
./gradlew runtimeBundle
|
|
ls build/distributions/gittally-runtime-linux-x64.tar.gz
|
|
```
|
|
|
|
Copy and unpack it on the target host, by convention to `~/opt/gittally`:
|
|
|
|
```bash
|
|
scp build/distributions/gittally-runtime-linux-x64.tar.gz user@host:/tmp/
|
|
ssh user@host 'mkdir -p ~/opt && tar -xzf /tmp/gittally-runtime-linux-x64.tar.gz -C ~/opt'
|
|
```
|
|
|
|
Then use `~/opt/gittally/bin/gittally` wherever this document says `java -jar ~/bin/gittally.jar`:
|
|
|
|
```bash
|
|
cd /path/to/repo
|
|
~/opt/gittally/bin/gittally init
|
|
~/opt/gittally/bin/gittally init --systemd
|
|
```
|
|
|
|
`init --systemd` detects the bundle automatically: the generated unit's `ExecStart` points at the bundle's `jre/bin/java` and `lib/gittally.jar`, so the install commands printed by `init --systemd` work unchanged.
|
|
`JAVA_OPTS` from the environment file applies as usual.
|
|
|
|
To update GitTally, stop the service, unpack the new bundle over `~/opt/gittally`, and restart the service.
|
|
|
|
## 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, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes GitTally unreachable for the proxy container.
|
|
See [configuration.md](configuration.md) for all `server.nginx.*` keys.
|