vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Document deploying to a server, one guide per path
b8e7a5679847cf4cdda0754f6154805585e1be96 · Keenan Nemetz · 3d ago · parent b86148d1
Document deploying to a server, one guide per path The guides are for someone who has a machine and a domain and has not deployed a forge before, so they start from what to have ready and end at a working https address with one instance admin. Each ends with the same three things: where first-boot output is, what a certificate warning means, and that pushing needs port 2222 reachable and a key that belongs to an account. The index page carries what the per-cloud pages should not repeat: backups are separate from a cloud snapshot because secrets.key must not travel with the data, and upgrades are a package install rather than an apply.
docs/deploy/docker.md +144
| @@ -0,0 +1,144 @@ | |||
| 1 | # Vole in a container | ||
| 2 | |||
| 3 | Vole is one process with its own SSH listener, so it fits in one container: no | ||
| 4 | `sshd`, no sidecar, and one volume holding the whole instance. | ||
| 5 | |||
| 6 | ## What you need | ||
| 7 | |||
| 8 | - Docker with the Compose plugin. | ||
| 9 | - A clone of the Vole repository, because the image compiles from source. | ||
| 10 | - A DNS name, and something to terminate TLS. Vole can do TLS itself — see | ||
| 11 | [Let Vole terminate TLS](#let-vole-terminate-tls) — but the default here is a | ||
| 12 | proxy on the same host, which is how most container hosts are already set up. | ||
| 13 | - Your SSH public key. | ||
| 14 | |||
| 15 | ## Build the image | ||
| 16 | |||
| 17 | From the repository root: | ||
| 18 | |||
| 19 | ```sh | ||
| 20 | docker build -f deploy/docker/Dockerfile -t vole:local . | ||
| 21 | ``` | ||
| 22 | |||
| 23 | The first build compiles Vole and takes a few minutes; later ones reuse the | ||
| 24 | layers. Nothing about the running instance is in the image: state lives in a | ||
| 25 | volume, and the configuration is a file you mount. | ||
| 26 | |||
| 27 | ## Configure and start | ||
| 28 | |||
| 29 | ```sh | ||
| 30 | cd deploy/docker | ||
| 31 | cp vole.toml ./vole.toml | ||
| 32 | $EDITOR ./vole.toml # set public_url to https://forge.example.com | ||
| 33 | docker compose up -d | ||
| 34 | ``` | ||
| 35 | |||
| 36 | `public_url` is the one setting that must be right before anyone signs in: | ||
| 37 | browsers bind a passkey to that address, and changing it later means re-enrolling | ||
| 38 | everyone. | ||
| 39 | |||
| 40 | The default ports are 2222 for Git over SSH and 8080 published on loopback, for a | ||
| 41 | proxy. Port 2222 is a plain TCP port, not HTTP: a proxy cannot route it, so | ||
| 42 | publish it directly. | ||
| 43 | |||
| 44 | Create the first account and make it an instance admin: | ||
| 45 | |||
| 46 | ```sh | ||
| 47 | docker compose exec -T vole admin account create admin --key - < ~/.ssh/id_ed25519.pub | ||
| 48 | docker compose exec vole admin account role admin admin | ||
| 49 | ``` | ||
| 50 | |||
| 51 | Pushing works immediately, because the account and the key are written together. | ||
| 52 | The browser needs a passkey, which that command prints when the account has none: | ||
| 53 | |||
| 54 | ```sh | ||
| 55 | docker compose exec vole admin account invite admin | ||
| 56 | ``` | ||
| 57 | |||
| 58 | Keep the link private: it is the one thing that grants browser access. | ||
| 59 | |||
| 60 | ## Put TLS in front | ||
| 61 | |||
| 62 | The smallest thing that works is Caddy on the same host: | ||
| 63 | |||
| 64 | ```caddyfile | ||
| 65 | forge.example.com { | ||
| 66 | reverse_proxy 127.0.0.1:8080 { | ||
| 67 | header_up Host {host} | ||
| 68 | } | ||
| 69 | } | ||
| 70 | ``` | ||
| 71 | |||
| 72 | Caddy sets `X-Forwarded-For` itself, and `vole.toml` names that header, so the | ||
| 73 | per-client clone limit and the sign-in rate limits see the real address. Whatever | ||
| 74 | proxy you use has to keep the `Host` header unchanged: hosted sites are routed by | ||
| 75 | host name. | ||
| 76 | |||
| 77 | ## Let Vole terminate TLS | ||
| 78 | |||
| 79 | If nothing else on the host serves 80 and 443, Vole can hold the certificate: | ||
| 80 | |||
| 81 | ```toml | ||
| 82 | http_addr = ":80" | ||
| 83 | https_addr = ":443" | ||
| 84 | public_url = "https://forge.example.com" | ||
| 85 | acme_email = "you@example.com" | ||
| 86 | ``` | ||
| 87 | |||
| 88 | and in `compose.yaml`, publish `80:80` and `443:443` instead of the loopback 8080. | ||
| 89 | The container runs as an unprivileged user, so add the capability for the two low | ||
| 90 | ports: | ||
| 91 | |||
| 92 | ```yaml | ||
| 93 | cap_add: | ||
| 94 | - NET_BIND_SERVICE | ||
| 95 | ``` | ||
| 96 | |||
| 97 | The health check in the packaged `compose.yaml` asks `/healthz` on port 8080, | ||
| 98 | which is the proxy arrangement above. With `https_addr` set, port 80 answers the | ||
| 99 | certificate challenge and redirects everything else, so either point the check at | ||
| 100 | `https://127.0.0.1/healthz` or remove it. | ||
| 101 | |||
| 102 | ## CI runners stay outside | ||
| 103 | |||
| 104 | A runner executes repository code in containers and needs a real OCI runtime, so | ||
| 105 | it does not belong in this container. Run it on the host or another machine and | ||
| 106 | point it at the instance: | ||
| 107 | |||
| 108 | ```sh | ||
| 109 | vole runner --remote ssh://git@forge.example.com:2222 --container-runtime podman | ||
| 110 | ``` | ||
| 111 | |||
| 112 | See [Runners](../runner.md). | ||
| 113 | |||
| 114 | ## Where the data is, and how to get it out | ||
| 115 | |||
| 116 | The volume holds everything: repositories, the account database, CI history, | ||
| 117 | logs, LFS objects, the SSH host key, and the key that seals repository secrets. | ||
| 118 | |||
| 119 | ```sh | ||
| 120 | docker compose exec vole admin account backup /var/lib/vole/users.db.bak | ||
| 121 | docker compose cp vole:/var/lib/vole/users.db.bak ./users.db.bak | ||
| 122 | ``` | ||
| 123 | |||
| 124 | Two files must be copied out separately and kept apart from that backup: the SSH | ||
| 125 | host key, `data/ssh_host_ed25519_key`, and the secrets key, `data/secrets.key`. A | ||
| 126 | backup holding the data directory and that key is a backup of plaintext. The full | ||
| 127 | procedure is in [Operations § Back up an instance](../operations.md#back-up-an-instance). | ||
| 128 | |||
| 129 | A bind mount instead of a named volume works, but the directory has to be writable | ||
| 130 | by the image's user: `sudo chown -R 999:999 /srv/vole`. | ||
| 131 | |||
| 132 | ## Upgrading | ||
| 133 | |||
| 134 | ```sh | ||
| 135 | docker compose exec vole admin account backup /var/lib/vole/users.db.bak | ||
| 136 | docker compose cp vole:/var/lib/vole/users.db.bak ./users.db.bak | ||
| 137 | git pull | ||
| 138 | docker build -f deploy/docker/Dockerfile -t vole:local . | ||
| 139 | docker compose up -d | ||
| 140 | ``` | ||
| 141 | |||
| 142 | Back up first: the account database is migrated in place the first time a newer | ||
| 143 | binary reads it, and that migration cannot be undone. A new image also reads the | ||
| 144 | configuration file you mounted, so settings survive. | ||