8.3 KiB
Hosting a LeVCS instance on a VPS
This walkthrough takes you from "I have a VPS" to "the LeVCS source code lives on my LeVCS instance" using the artifacts in this directory.
The instance terminates HTTP, not TLS, so you'll run it behind a reverse proxy (Caddy or nginx). Federation requests are signed at the application layer, so the proxy is just transport security + rate limiting — there's no auth handoff between layers.
Architecture (one-liner)
laptop --HTTPS--> Caddy/nginx (TLS) --HTTP--> levcs-instance (127.0.0.1:7117)
|
v
/var/lib/levcs
What's in this directory
| File | Purpose |
|---|---|
instance.toml.example |
Annotated config — copy to /etc/levcs/instance.toml. |
levcs-instance.service |
systemd unit. Runs as a levcs user, hardened. |
Caddyfile.example |
Caddy reverse-proxy block (auto-TLS via Let's Encrypt). |
nginx.conf.example |
nginx alternative (use if you already run nginx). |
VPS-side install
1. Build the binary
On a build host (the VPS itself or a beefier dev machine cross-compiled to its target), build a release binary:
cargo build --release -p levcs-instance --bin levcs-instance
The binary lands at target/release/levcs-instance. Copy it to
/usr/local/bin/levcs-instance on the VPS.
2. Create the service user and directories
sudo useradd --system --home /var/lib/levcs --shell /usr/sbin/nologin levcs
sudo install -d -o levcs -g levcs -m 0755 /var/lib/levcs
sudo install -d -o root -g root -m 0755 /etc/levcs
3. Drop the config in place
sudo cp deploy/instance.toml.example /etc/levcs/instance.toml
sudo $EDITOR /etc/levcs/instance.toml
The defaults (full storage, builtin handlers only, no mirrors, listen
on 127.0.0.1:7117) are correct for a single-VPS install. Change root
only if /var/lib/levcs doesn't suit your filesystem layout.
4. Install the systemd unit
sudo cp deploy/levcs-instance.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now levcs-instance
sudo systemctl status levcs-instance
Verify it's listening:
curl -fsS http://127.0.0.1:7117/health
# {"status":"ok"}
5. Reverse proxy
Option A — Caddy (recommended)
If Caddy isn't installed yet:
sudo apt install caddy # or your distro's package
Edit Caddyfile.example, replace levcs.example.com with your real
hostname, then either drop it in as /etc/caddy/Caddyfile or import it
from your existing one. Reload Caddy:
sudo cp deploy/Caddyfile.example /etc/caddy/Caddyfile
sudo $EDITOR /etc/caddy/Caddyfile
sudo systemctl reload caddy
Caddy will fetch a Let's Encrypt cert automatically on the first request. Confirm:
curl -fsS https://levcs.example.com/health
# {"status":"ok"}
Option B — nginx (if you already run it)
If your VPS already runs nginx (e.g. fronting Forgejo), use a server
block alongside the existing ones rather than introducing Caddy. Make
sure you have a TLS cert for the new hostname (certbot:
sudo certbot --nginx -d levcs.example.com).
sudo cp deploy/nginx.conf.example /etc/nginx/sites-available/levcs
sudo $EDITOR /etc/nginx/sites-available/levcs
sudo ln -s ../sites-available/levcs /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
The example block handles both 80→443 redirect and the proxy itself.
The location regex (/levcs/v1 and /health) ensures everything else
returns 404 — there is no web UI yet, and the instance shouldn't appear
to host one.
6. Firewall
Open 80 and 443 on the VPS, keep 7117 closed from the public internet:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 7117 stays closed — only Caddy/nginx talk to it.
Laptop-side bootstrap
Now make the LeVCS source code itself a LeVCS repository hosted on the new instance. This is the dogfood claim: you're running the protocol on your own code from this point on.
1. Build the CLI locally
cargo build --release -p levcs-cli --bin levcs
sudo install -m 0755 target/release/levcs /usr/local/bin/levcs
2. Generate or import an identity key
If you don't already have a LeVCS key:
levcs key generate --label primary
levcs key list
The label is yours to choose (alice, primary, your handle — anything).
This key is your authority membership credential; it signs every
authority object and every push.
3. Init the repo locally
From inside the LeVCS source tree:
levcs init --key primary
levcs track --all
levcs commit -m "initial import"
init writes a .levcs/ directory next to your source, with a genesis
authority object that names your key as the sole Owner. The repo_id is
the BLAKE3 hash of that authority — globally unique by construction.
4. Point the local repo at the VPS instance
levcs instance --set https://levcs.example.com/levcs/v1
levcs instance --info
The --set value is what the federation client uses for every push and
pull on this repo.
5. First push (auto-init)
levcs push refs/branches/main
Behind the scenes the client tries /push first, gets a 404 (the repo
doesn't exist on the instance yet), then falls back to /init with the
genesis authority object and re-pushes. Output looks like:
repo not yet on instance; initialising
pushed 1 ref(s)
That's it. The repo is now hosted on the VPS, available to anyone over HTTPS:
curl -fsS https://levcs.example.com/levcs/v1/repos/<repo_id>/info | jq
6. (Optional) Verify the server-side state
SSH to the VPS and look at what got persisted:
sudo ls /var/lib/levcs/
sudo ls /var/lib/levcs/<repo_id>/.levcs/refs/branches/
sudo cat /var/lib/levcs/<repo_id>/.levcs/refs/authority/current
You should see your repo_id directory, a main ref pointing at your
head commit hash, and a current-authority pointer matching the genesis.
Operating notes
Logs
sudo journalctl -u levcs-instance -f # live
sudo journalctl -u levcs-instance -p warning # warnings + errors only
The TraceLayer middleware emits one line per HTTP request at
info level — method, path, status, latency. Bump to debug via
Environment=RUST_LOG=debug in the systemd unit when diagnosing.
Backups
The instance is filesystem-only. A consistent backup is just a
snapshot of /var/lib/levcs. Per-push atomicity is per-object plus a
serializing per-repo mutex (see crates/levcs-instance/src/lib.rs),
which means a snapshot taken at any moment is internally consistent
even without quiescing the service. rsync --link-dest for incremental
hardlink snapshots works well.
Storage growth
There's no automatic GC on the instance. Run levcs gc from a client
that has the repo locally; instance-side GC is a future feature. For
now, "GC" on the VPS is "delete entire <repo_id>/ directories of repos
you no longer want."
Updating the binary
sudo systemctl stop levcs-instance
sudo install -m 0755 target/release/levcs-instance /usr/local/bin/levcs-instance
sudo systemctl start levcs-instance
The on-disk format is content-addressed and forward-compatible — there's no migration step between versions. If a future release introduces an incompatible change, the release notes will say so.
Mirrors
To set up a read-only mirror of a repo from another instance, add a
[[mirrors]] block to /etc/levcs/instance.toml:
[[mirrors]]
repo_id = "abcd1234..."
source = "https://other.example/levcs/v1"
mode = "full"
poll_interval = "5m"
writeback = false
Then sudo systemctl restart levcs-instance. The background poller
spawns at startup; journalctl -u levcs-instance will show the sync
activity.
What's missing (workflow honesty)
The protocol surface this instance exposes is just refs + objects + auth. There is no web UI, issue tracker, PR/review surface, comment thread, notification hub, or branch-protection layer yet. If you're migrating from Forgejo for the LeVCS source, you'll lose:
- The web frontend for browsing code, blame, and history.
- Issues and PR discussions (and any Forgejo-specific automations).
- CI integration (no webhooks yet — you'd need to poll the refs from your CI).
That's the spec gap that comes next. This deployment is the substrate the workflow layer will sit on top of.