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.
Rewrite the deployment guides as console walkthroughs
942c93cd06345416c580176c1a149f98e7e96cc0 · Keenan Nemetz · 3d ago · parent 51624dc6
Rewrite the deployment guides as console walkthroughs With the provider configurations gone, the guides are what is left, and their shape changed: render a file, paste it into a console field, point a name at the machine. The index gains the section the Terraform version did not need — first boot happens once. Editing the pasted file afterwards changes nothing, which is how cloud-init works rather than a limitation of these pages, so a later settings change is editing /etc/vole/vole.toml and restarting, and an upgrade is a package install. Each cloud page is a table of console fields: image, size, where the user data goes, and a firewall. The firewall table is load-bearing rather than advice, because Hetzner and DigitalOcean leave a new machine open and AWS hands it a security group that allows nothing inbound. The runner page leads with render.sh runner and keeps the by-hand path under it; the registration commands are written out now that no output prints them.
docs/deploy/digitalocean.md +96−70
| @@ -1,103 +1,129 @@ | |||
| 1 | 1 | # Vole on DigitalOcean | |
| 2 | 2 | ||
| 3 | This creates one droplet, a firewall, and an SSH key, then turns the machine into | ||
| 4 | a Vole instance on first boot. It takes about three minutes. | ||
| 3 | One droplet, configured on its first boot by a file you paste into the console. No | ||
| 4 | API token, nothing to install locally: a rendered file, a droplet, a firewall, and | ||
| 5 | one DNS record. | ||
| 5 | 6 | ||
| 6 | 7 | ## What you need | |
| 7 | 8 | ||
| 8 | - A DigitalOcean API token with read and write access. Put it in | ||
| 9 | `DIGITALOCEAN_TOKEN`, or in `DIGITALOCEAN_TOKEN` in your Terraform | ||
| 10 | environment, which is the same thing. | ||
| 11 | - A DNS name, such as `forge.example.com`. Either point it at the droplet's | ||
| 12 | address, or let Terraform add the name to DigitalOcean's DNS and create the | ||
| 13 | record. | ||
| 14 | - Your SSH public key, and the URL of a Vole `.deb` for `amd64`. | ||
| 9 | - A DigitalOcean account. | ||
| 10 | - A DNS name, such as `forge.example.com`. | ||
| 11 | - Your SSH public key. | ||
| 12 | - The URL of a Vole `.deb` for `amd64`. | ||
| 15 | 13 | ||
| 16 | ## Deploy | ||
| 14 | ## Render the first-boot file | ||
| 17 | 15 | ||
| 18 | From a clone of the Vole repository: | ||
| 16 | From a clone of the Vole repository, on your own computer: | ||
| 19 | 17 | ||
| 20 | 18 | ```sh | |
| 21 | export DIGITALOCEAN_TOKEN=... # your API token | ||
| 22 | cd deploy/terraform/digitalocean | ||
| 23 | terraform init | ||
| 24 | |||
| 25 | terraform apply \ | ||
| 26 | -var domain=forge.example.com \ | ||
| 27 | -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 28 | -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb | ||
| 19 | cd deploy/cloud-init | ||
| 20 | |||
| 21 | HOSTNAME=forge.example.com \ | ||
| 22 | ADMIN_ACCOUNT=admin \ | ||
| 23 | ADMIN_EMAIL=you@example.com \ | ||
| 24 | ADMIN_SSH_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 25 | VOLE_PACKAGE_URL=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb \ | ||
| 26 | ./render.sh > user-data.yaml | ||
| 29 | 27 | ``` | |
| 30 | 28 | ||
| 31 | Type `yes` when it asks. Then point `forge.example.com` at the `public_ipv4` | ||
| 32 | output, or pass `-var create_dns=true -var dns_zone=example.com` to have Terraform | ||
| 33 | create the record in DigitalOcean's DNS. | ||
| 29 | Open `user-data.yaml` before you paste it. It is the whole setup, and it is the | ||
| 30 | last chance to notice a wrong account name: names are permanent. | ||
| 34 | 31 | ||
| 35 | ## What you get | ||
| 32 | ## Create the droplet | ||
| 36 | 33 | ||
| 37 | ```text | ||
| 38 | web_url = "https://forge.example.com" | ||
| 39 | git_remote = "ssh://git@forge.example.com:2222/" | ||
| 40 | public_ipv4 = "203.0.113.10" | ||
| 41 | admin_account = "admin" | ||
| 42 | server_login = "ssh root@203.0.113.10" | ||
| 43 | ``` | ||
| 34 | In the control panel, **Create Droplet**: | ||
| 35 | |||
| 36 | | Field | What to choose | | ||
| 37 | |---|---| | ||
| 38 | | Region | wherever your team is | | ||
| 39 | | Image | Ubuntu 24.04 (LTS) x64. Debian 13 works too | | ||
| 40 | | Size | Basic, 2 vCPU / 4 GB / 80 GB. Raise it if you expect big pushes or LFS | | ||
| 41 | | Authentication | SSH key — the same one you put in the file | | ||
| 42 | | **Advanced options → user data** | paste the contents of `user-data.yaml` | | ||
| 43 | | Backups | on. It is a snapshot, not a backup of the instance — see [Backups](README.md#backups-are-still-your-job) | | ||
| 44 | | Monitoring | on. The free graphs are the quickest way to see a build filling the disk | | ||
| 45 | |||
| 46 | Then add a **Cloud Firewall** under Networking and apply it to the droplet. | ||
| 47 | DigitalOcean leaves a droplet open if you do not: | ||
| 48 | |||
| 49 | | Direction | Protocol | Port | Source | | ||
| 50 | |---|---|---|---| | ||
| 51 | | in | tcp | 22 | your team's addresses, for shell access | | ||
| 52 | | in | tcp | 2222 | your team's addresses. This is Git over SSH, the write transport | | ||
| 53 | | in | tcp | 80 | anywhere, for the certificate challenge | | ||
| 54 | | in | tcp | 443 | anywhere | | ||
| 55 | | out | any | any | anywhere, so the droplet can fetch its package and certificate | | ||
| 56 | |||
| 57 | Restricting 2222 to your team is the cheapest way to keep the write side private. | ||
| 58 | If you leave it open, Vole still authenticates every connection by key. | ||
| 44 | 59 | ||
| 45 | Open `web_url` after DNS resolves. The account named by `admin_account` is an | ||
| 46 | instance admin and has no passkey yet; issue one from your laptop: | ||
| 60 | ## Point the name at it | ||
| 61 | |||
| 62 | Create an `A` record for `forge.example.com` pointing at the droplet's IPv4 | ||
| 63 | address, in DigitalOcean's DNS or wherever the name lives. Vole cannot obtain a | ||
| 64 | certificate for a name that does not resolve to it yet, and it retries, so a record | ||
| 65 | created a few minutes after the droplet is fine. | ||
| 66 | |||
| 67 | ## Check it works | ||
| 68 | |||
| 69 | Open `https://forge.example.com`. The first request may take a few seconds while | ||
| 70 | the certificate is issued; if it fails, wait a minute and reload. | ||
| 71 | |||
| 72 | Issue a passkey for the admin account from your laptop: | ||
| 47 | 73 | ||
| 48 | 74 | ```sh | |
| 49 | 75 | vole admin --remote ssh://git@forge.example.com:2222 account invite admin | |
| 50 | 76 | ``` | |
| 51 | 77 | ||
| 78 | Then create a repository, and clone it: | ||
| 79 | |||
| 80 | ```sh | ||
| 81 | vole admin --remote ssh://git@forge.example.com:2222 repo create demo --owner admin | ||
| 82 | vole admin --remote ssh://git@forge.example.com:2222 acl set-default-role demo read | ||
| 83 | git clone ssh://git@forge.example.com:2222/demo.git | ||
| 84 | ``` | ||
| 85 | |||
| 86 | ## Adding a CI runner | ||
| 87 | |||
| 88 | Create a second droplet the same way, but give it the runner's file: | ||
| 89 | |||
| 90 | ```sh | ||
| 91 | ./render.sh runner > runner-user-data.yaml # FORGE_HOST=forge.example.com and VOLE_PACKAGE_URL | ||
| 92 | ``` | ||
| 93 | |||
| 94 | It needs no inbound ports except 22. It needs one more step per repository before | ||
| 95 | it takes work; see [CI runners](runner.md). | ||
| 96 | |||
| 52 | 97 | ## The settings worth changing | |
| 53 | 98 | ||
| 54 | Everything else has a working default. Pass these with `-var` or in a | ||
| 55 | `terraform.tfvars` file beside `main.tf`. | ||
| 99 | The values you pass to `render.sh`. Everything else has a working default. | ||
| 56 | 100 | ||
| 57 | | Setting | Default | What it does | | ||
| 101 | | Variable | Default | What it does | | ||
| 58 | 102 | |---|---|---| | |
| 59 | | `ssh_admin_cidrs` | everything | Addresses that may reach port 22 (server login) and 2222 (Git over SSH). Setting this to your team's range is the cheapest way to keep the write side private | | ||
| 60 | | `size` | `s-2vcpu-4gb` | 2 vCPU, 4 GB, 80 GB. `s-4vcpu-8gb` doubles all three if builds run on this machine | | ||
| 61 | | `region` | `nyc3` | `sfo3`, `ams3`, `fra1`, `blr1`, `syd1`, and others | | ||
| 62 | | `name` | `vole` | Name of the droplet, firewall, and SSH key | | ||
| 63 | | `enable_backups` | `true` | Weekly droplet backups, for about 20% of the droplet price | | ||
| 64 | | `tls_mode` | `acme` | `proxy` leaves TLS to a proxy you run instead, and serves plain HTTP on loopback | | ||
| 65 | | `instance_name` | `Vole` | What the landing page calls this forge | | ||
| 66 | | `runner` | `false` | Create a second machine that runs CI jobs. See [CI runners](runner.md): it needs one more step per repository before it takes work | | ||
| 67 | | `runner_type` | `s-2vcpu-4gb` | Droplet size for that machine. Builds want memory, so raise this before the forge's own size | | ||
| 68 | | `runner_labels` | `["linux"]` | What the runner advertises; a job with `runs-on` only comes to a machine carrying every label it asks for | | ||
| 69 | | `runner_network` | `bridge` | Whether a job that asks for a network gets one. Jobs that fetch dependencies need it | | ||
| 70 | |||
| 71 | The droplet reports CPU, memory, and disk to DigitalOcean's graphs, which is the | ||
| 72 | quickest way to see that a build filled the disk. | ||
| 73 | |||
| 74 | `terraform apply` is safe to run again: it reports no changes. It does not upgrade | ||
| 75 | Vole, and it does not touch the machine's data. | ||
| 76 | |||
| 77 | With `-var runner=true`, `terraform output runner_setup` prints the commands that | ||
| 78 | let the second machine take jobs. It needs one step per repository before it | ||
| 79 | receives any work; see [CI runners](runner.md). | ||
| 80 | |||
| 81 | DigitalOcean has no delete protection on droplets, so `terraform destroy` does | ||
| 82 | what it says. The instance's repositories and accounts are on that droplet. | ||
| 103 | | `HOSTNAME` | required | The name the instance answers on. It becomes `public_url`, and passkeys bind to it | | ||
| 104 | | `ADMIN_ACCOUNT` | required | The first account, made an instance admin. Permanent | | ||
| 105 | | `ADMIN_SSH_KEY` | required | That account's public key | | ||
| 106 | | `VOLE_PACKAGE_URL` | required | The `.deb` for the machine's architecture | | ||
| 107 | | `ADMIN_EMAIL` | empty | Contact address for the certificate authority | | ||
| 108 | | `INSTANCE_NAME` | `Vole` | What the landing page calls this forge | | ||
| 109 | | `TLS_MODE` | `acme` | `proxy` leaves TLS to a proxy you run, and serves plain HTTP on loopback | | ||
| 83 | 110 | ||
| 84 | 111 | ## If something is not right | |
| 85 | 112 | ||
| 86 | 113 | - **The browser shows a certificate warning.** DNS is not answering with the | |
| 87 | droplet's address yet, or ports 80 and 443 are not reachable. Check with | ||
| 88 | `dig +short forge.example.com`, then look at `journalctl -u vole | grep -i acme` | ||
| 89 | on the machine. | ||
| 114 | droplet's address yet, or ports 80 and 443 are blocked by the firewall. Check | ||
| 115 | `dig +short forge.example.com`, then `journalctl -u vole | grep -i acme`. | ||
| 90 | 116 | - **The droplet is up but the forge is not.** First-boot output is in | |
| 91 | `/var/log/vole-first-boot.log`, and the service log is `journalctl -u vole`. | ||
| 92 | The usual cause is a package URL that 404s or is for the other architecture. | ||
| 93 | - **`terraform apply` fails on credentials.** `DIGITALOCEAN_TOKEN` is unset or the | ||
| 94 | token is read-only. | ||
| 95 | - **Pushing is refused.** Port 2222 has to be reachable from where you push, and | ||
| 96 | the key has to belong to an account. `vole whoami --remote | ||
| 97 | ssh://git@forge.example.com:2222` says which account your key is. | ||
| 117 | `/var/log/vole-first-boot.log`; the service log is `journalctl -u vole`. The | ||
| 118 | usual cause is a package URL that 404s or is for the other architecture. | ||
| 119 | - **Nothing listens on 2222.** The firewall is not applied to the droplet, or its | ||
| 120 | source addresses do not include where you are pushing from. | ||
| 121 | - **Pushing is refused.** `vole whoami --remote ssh://git@forge.example.com:2222` | ||
| 122 | says which account your key is; if it is not the one you expected, the key was | ||
| 123 | not the one in `user-data.yaml`. | ||
| 98 | 124 | ||
| 99 | 125 | ## Next | |
| 100 | 126 | ||
| 101 | - [Deploying Vole](README.md) — backups, upgrades, and what first boot does | ||
| 127 | - [Deploying Vole](README.md) — what first boot does, backups, and upgrades | ||
| 102 | 128 | - [Packages](../packaging.md) — the `.deb`, the unit file, and upgrades on the machine | |
| 103 | 129 | - [Accounts and access](../accounts-and-access.md) — the first accounts, roles, and passkeys | |