vectrum.cc · A self-hosted, multi-user GitHub built with Next.js. Repositories are real bare git repositories on disk, and you push to them with plain git over SSH or HTTPS.
docker push <host>/<owner>/<repo> (or <host>/<owner>/<repo>/<name> for more images per repository) with the same visibility, collaborators and API tokens as the code. Multi-platform images, cross-repository blob mounts, chunked uploads and the referrers API (signatures, SBOMs) are supported. Images are listed under the repository's Packages tab, where writers can delete versionscurl, and /releases/latest/download/<file> always links the newest versionC new issue, J/K move, Enter open, S/P/A/L set status, priority, assignee, labels.vectrum/workflows/*.yml (a subset of GitHub Actions syntax) run on push or by hand, on runners you register for one repository or for all of yours. Live logs, cancel and re-run, and CI status marks in the commit list/mcp and sign in with OAuth 2.1. You approve each one, read-only or read & write, for all repositories or selected ones. They can read and commit code, manage branches, issues, labels and releases, create repositories, and run and debug workflows. Manage connected apps under Settings → Connected appsRequires Node.js 22+ (24.7+ for post-quantum SSH key exchange) and git on the PATH.
bun install # also downloads the PocketBase server into bin/
bun dev
Open http://localhost:3000, sign up, and create a repository.
The data lives in PocketBase. Vectrum starts bin/pocketbase itself on 127.0.0.1:8090, with its data in data/pb_data and the schema from pb_migrations/; its superuser login is generated into data/pocketbase-superuser.json (also good for the PocketBase dashboard at http://127.0.0.1:8090/_/). Run bun run pocketbase:install if the binary is missing. To use a PocketBase server of your own instead, set PB_URL and the superuser credentials; it needs the migrations in pb_migrations/ (pocketbase serve --migrationsDir pb_migrations).
docker compose up -d --build
The image runs the web UI on port 3000 and the SSH server on port 2222, with all data in the vectrum-data volume (mounted at /data). Settings go under environment: in docker-compose.yml (see Configuration). To close registration, set SIGNUPS_DISABLED: "1" before you deploy, and create accounts from the command line instead:
docker compose exec -it vectrum node scripts/create-user.mjs <username> <email> [name]
It asks for the password (or reads VECTRUM_PASSWORD). Outside Docker, run node scripts/create-user.mjs the same way while Vectrum runs. Clone URLs show SSH_PORT, so publish SSH on the same port on the host, and set SSH_HOST to your domain. Put a TLS reverse proxy in front of port 3000 if the server is reachable from the internet, and set PUBLIC_URL to its address.
~/.ssh/id_ed25519.pub) under Settings → SSH keys.ssh -T -p 2222 git@localhostgit remote add origin ssh://git@localhost:2222/<you>/<repo>.git
git push -u origin main
Generate a token under Settings → API tokens and use it as the password when git asks (any username works):
git clone http://localhost:3000/<you>/<repo>.git
# Username: <you> Password: vec_…
Anonymous users can clone public repositories without a token.
Log in with an API token as the password (read & write to push), then tag and push images under a repository's name. Image names are lowercase:
docker login localhost:3000 -u <you> # Password: vec_…
docker build -t localhost:3000/<you>/<repo>:latest .
docker push localhost:3000/<you>/<repo>:latest
docker push localhost:3000/<you>/<repo>/worker:1.0 # more images in one repository
Anyone who can read the repository can pull; anonymous users can pull images of
public repositories. The repository has to exist before you push. Docker only
talks plain HTTP to localhost; anywhere else, serve Vectrum over HTTPS (or add
the host to Docker's insecure-registries). A reverse proxy in front of
Vectrum must pass large request bodies through (e.g. nginx client_max_body_size 0;).
Draft one under a repository's Releases tab, or upload files from a script with a read & write API token. Uploading to a pushed tag that has no release yet publishes one for it; uploading a file again replaces it:
git tag v1.0.0 && git push origin v1.0.0
curl -fT dist/app-linux-x64.tar.gz -u <you>:vec_… \
http://localhost:3000/<you>/<repo>/releases/download/v1.0.0/app-linux-x64.tar.gz
curl -X DELETE -u <you>:vec_… http://localhost:3000/<you>/<repo>/releases/download/v1.0.0/app-linux-x64.tar.gz
Downloads are GET /<owner>/<repo>/releases/download/<tag>/<file> (or
/releases/latest/download/<file> for the newest release that isn't a pre-release)
and support ranges, so interrupted downloads resume. Source code is at
/<owner>/<repo>/archive/refs/tags/<tag>.zip (or .tar.gz; branches and commits
work too). Private repositories need a token (-u <you>:vec_… or
-H "Authorization: Bearer vec_…"). To publish from a workflow, give the runner's
environment a token (workflows have no secrets) and curl the files in a job
that runs on: push: tags.
curl -fsSLO http://localhost:3000/vectrum-runner.mjs
node vectrum-runner.mjs --url http://localhost:3000 --token vrn_…
.vectrum/workflows/ci.yml and push:
name: CI
on:
push:
branches: [main]
workflow_dispatch: # adds a "Run workflow" button
jobs:
test:
runs-on: self-hosted # labels the runner must have
steps:
- run: npm ci
- name: Test
run: npm test
Supported: on: push (with branches, tags, paths and their -ignore forms) and workflow_dispatch; env at every level; jobs with runs-on, needs, timeout-minutes; steps with run, name, shell, working-directory, env, if: success()/failure()/always(), continue-on-error and timeout-minutes. The repository is checked out before the first step (uses: actions/checkout is accepted and skipped). Other uses: actions, ${{ }} expressions, matrices, services and secrets are not supported; steps get VECTRUM_* environment variables (VECTRUM_SHA, VECTRUM_REF_NAME, VECTRUM_REPOSITORY, …) instead.
Runners poll the server over HTTPS (/api/runner/*), so they need no inbound port. Every runner has the labels self-hosted, its OS (linux, macos, windows) and architecture (x64, arm64), plus any you give it. Each job gets a short-lived, read-only token that can only clone its own repository. Steps run as the user that started the runner, with no sandbox, so only run runners for repositories whose workflows you trust.
Vectrum is a remote MCP server at /mcp (Streamable HTTP). Add it to your agent, e.g. Claude Code:
claude mcp add --transport http vectrum http://localhost:3000/mcp
Then run /mcp in Claude Code and choose Authenticate. Your browser opens Vectrum, where you sign in and choose what the agent may do: read-only or read & write, for all repositories or selected ones. Other MCP clients work the same way: they find the OAuth endpoints from the 401 answer, register themselves, and send you to approve them. Settings → Connected apps lists what you approved, and disconnecting cuts access right away. Clients without OAuth can send an API token instead (Authorization: Bearer vec_…).
The tools, which check the same permissions as the web UI:
| Area | Tools |
|---|---|
| Account and repositories | get_me, list_repositories, search_repositories, get_repository, create_repository, update_repository |
| Code | list_branches, create_branch, list_files, read_file, commit_files (several files in one commit, optionally on a new branch, with an optimistic expected_head check), list_commits, get_commit (with its diff) |
| Issues | list_issues, get_issue, create_issue, update_issue, comment_on_issue, list_labels, create_label |
| Releases | list_releases, get_release, create_release (creates a missing tag at a branch or commit), update_release (also publishes drafts), delete_release |
| Actions | list_workflows, list_workflow_runs, get_workflow_run, get_job_logs, run_workflow, cancel_workflow_run, rerun_workflow_run |
Read-only connections only see the reading tools. Commits made by an agent are authored as you and start workflows like a push would. Nothing deletes repositories or issues; delete_release removes a release and its files but keeps the tag. Release files are uploaded with an API token (see Publishing releases).
| Variable | Default | Purpose |
|---|---|---|
PUBLIC_URL | request host | Canonical address, e.g. https://vectrum.cc. Used for clone URLs; https also makes session cookies Secure. Set it in production |
SECURE_COOKIES | — | Set to 1 to mark session cookies Secure without PUBLIC_URL |
DATA_ROOT | ./data | Where repositories, the database and the SSH host key live |
PB_PORT | 8090 | Port of the PocketBase server Vectrum runs |
PB_URL | — | Use this PocketBase server instead of running one |
PB_SUPERUSER_EMAIL, PB_SUPERUSER_PASSWORD | generated | Superuser login; required with PB_URL |
PB_BINARY | bin/pocketbase | PocketBase executable to run |
SSH_PORT | 2222 | Port of the built-in SSH server |
SSH_LISTEN_HOST | 0.0.0.0 | Interface the SSH server binds to |
SSH_HOST | request host | Host name shown in SSH clone URLs, e.g. vectrum.cc |
SSH_DISABLED | — | Set to 1 to not start the SSH server |
SIGNUPS_DISABLED | — | Set to 1 to close registration (add users with scripts/create-user.mjs) |
Data layout:
data/
pb_data/ PocketBase: users, keys, tokens, repos, issues, releases, workflow runs and logs
pocketbase-superuser.json the generated PocketBase superuser login
ssh_host_ed25519_key generated on first start
hooks/ server-side git hooks for every push, rewritten on start
repos/<owner>/<repo>.git bare git repositories
registry.db container image manifests, tags and blob records (SQLite)
registry/blobs/sha256/… container image layers, shared by all repositories
registry/uploads/ image uploads in progress
releases/<repo>/<release>/… release files, named by their record id
The registry keeps its records in SQLite rather than PocketBase so that storing a blob and pruning unused ones never interleave. A blob is deleted once no manifest of any repository references it (blobs pushed in the last 24 hours are kept so pushes in progress don't lose their layers).
PUBLIC_URL. Host and X-Forwarded-* headers are client-controlled, so they never decide cookie security, and only stand in for PUBLIC_URL in development.- are refused by a pre-receive hook, such refs are ignored when reading, and every revision argument follows --end-of-options.localhost), one-time codes that expire in 5 minutes, access tokens that last an hour, and refresh tokens that rotate on every use (presenting an old one revokes the grant). Only hashes of codes and tokens are stored. The consent page can't be framed, and it shows where it will send you, because anyone can register an application under any name.application/octet-stream attachments with nosniff and a sandboxing CSP. Uploads signed in with the browser session must come from the site's own origin; scripts use tokens.nosniff and a sandboxing CSP, so repository HTML/SVG can't run scripts in the app's origin.lib/db.ts: starting and connecting to PocketBase; pb_migrations/: the schemalib/users.ts, lib/auth.ts: accounts and sessions (PocketBase auth records and tokens)lib/repos.ts: repository records, permissions (repoAccess), collaboratorslib/issues.ts, lib/issue-meta.ts: issue tracker data and its shared vocabulary (statuses, priorities); UI in components/issues/lib/keys.ts, lib/tokens.ts: SSH keys and API tokenslib/oauth.ts, lib/oauth-rules.ts: the OAuth 2.1 authorization server for agents (app/oauth/*, app/.well-known/*); lib/mcp/*: the MCP endpoint (app/mcp) and its toolslib/git.ts, lib/git-write.ts: reading from and committing to bare repositorieslib/git-http.ts, lib/ssh-server.ts: git transports (started from instrumentation.ts)lib/registry.ts, lib/registry-http.ts: container registry storage and the OCI distribution API, served under /v2lib/releases.ts, lib/release-http.ts: releases and their files, and the download, upload and source archive routeslib/workflows.ts, lib/ci.ts, lib/runs.ts, lib/runners.ts: workflow parsing, starting runs on push, the job queue and runner registration; app/api/runner/* is the runner API and public/vectrum-runner.mjs the runnerapp/actions/*: server actions, each re-checking the session and permissions