Run Vellum in hosted mode
This guide shows you how to start Vellum with the hosted profile. The hosted profile is for an instance that more than one team reaches over a public address.
Know the status first
Section titled “Know the status first”The hosted profile has limited evidence. Read these limits before you use it.
- The automated tests exercise the hosted profile. One manual run used
docker-compose.hosted.example.ymlon one machine, with theX-Forwarded-Protoheader set by hand in place of a reverse proxy that ends TLS. No run used a real reverse proxy or a real certificate. - In that run, a user of one workspace sent 18 requests for the projects, settings, secrets, agent tokens, exports and MCP tools of another workspace. Each request got
403or “Project not found”. The automated tests cover isolation for each module; no test covers every endpoint. - The automated tests run the sign-in with GitHub against a stand-in for GitHub, not against GitHub itself. Sign-in with Google has no test.
Use the local profile for real work. Local and hosted profiles explains the difference.
Know what the profile changes
Section titled “Know what the profile changes”local |
hosted |
|
|---|---|---|
| Master key | Vellum creates master.key in the data directory if you give none. |
You must give the key. |
| Session cookie | Secure only when the request came over HTTPS. | Always Secure. Vellum__SecureCookies=false is refused. |
Strict-Transport-Security header |
Not sent. | Sent with each response to a request that came over HTTPS. |
AllowedHosts |
Each host name is accepted. | You must list the public host names. |
| First account | The setup code from the log, or Vellum__SetupToken. |
Vellum__SetupToken only. |
| Self-registration after setup | Off | On |
| URLs on private networks in settings | Allowed | Refused |
Plain http in a settings URL |
Allowed | Refused, except for a private host when private network URLs are allowed |
| Private code to an AI provider | A workspace can allow it. | Refused until the instance administrator allows it. |
Prepare the values
Section titled “Prepare the values”-
Make a master key. Keep it in a secret store. Without this key Vellum cannot read the stored credentials.
Terminal window openssl rand -base64 32 -
Make a setup token of 16 characters or more.
Terminal window openssl rand -hex 16 -
Decide the public host name, for example
vellum.example.com. -
Put a reverse proxy that terminates HTTPS in front of Vellum. Vellum listens on plain HTTP on port
8080. See Configure the reverse proxy. -
Find the address that Vellum sees for the proxy. That address is the value of
Vellum__TrustedProxies.
| Variable | Value in the hosted profile |
|---|---|
Vellum__Profile |
hosted |
ConnectionStrings__DefaultConnection |
The Postgres connection string. Required. |
Vellum__MasterKey or Vellum__MasterKeyFile |
Required. The base64 form of 32 random bytes, or a file that holds that value. Set one of them. |
Vellum__SetupToken |
Required while no user exists. At least 16 characters. |
AllowedHosts |
Required. The public host names, separated by ;. No *, no name that contains *, no 0.0.0.0, :: or [::], no empty entry. |
Vellum__TrustedProxies |
The addresses or the CIDR ranges of your reverse proxies, separated by ;. Vellum reads the scheme and the address of the client from these senders only. |
Vellum__SecureCookies |
Leave it out. false is refused. |
Database__MigrateOnStartup |
Default true. Set false only when a separate step applies the migrations. |
Bootstrap environment lists each variable.
Configure the reverse proxy
Section titled “Configure the reverse proxy”Let nothing but the proxy reach port 8080 of Vellum.
- Make the proxy pass the
Hostheader on unchanged. The value must be one of the names inAllowedHosts. - Make the proxy set
X-Forwarded-Prototo the scheme that the browser used. - Make the proxy set
X-Forwarded-Forto the address of the client, or append that address to the header. - Set
Vellum__TrustedProxiesto the address that Vellum sees for the proxy, for example10.0.0.5. For more than one proxy, separate the entries with;. An entry can be a CIDR range, for example172.20.0.0/16. Name the narrowest range that holds your proxy.
Vellum reads the two headers only from a sender that Vellum__TrustedProxies names. It ignores them from each other sender. So a client cannot select the address that the rate limits count, and a client cannot claim HTTPS.
Vellum reads only the last hop of X-Forwarded-For. That is the value that your proxy wrote.
Without Vellum__TrustedProxies, Vellum ignores the two headers. All clients of the proxy then share one rate limit, and Vellum sends no Strict-Transport-Security header.
Vellum refuses to start when an entry is no address and no range, or when a range holds every address.
Do not set ASPNETCORE_FORWARDEDHEADERS_ENABLED. Vellum does not need it. The web framework reads it and then trusts the two headers from every sender.
Start with the hosted Compose example
Section titled “Start with the hosted Compose example”The repository has the file docker-compose.hosted.example.yml. It starts Vellum in the hosted profile with a Postgres database. It publishes the app port on 127.0.0.1 only and does not publish the database port. The container has a read-only root file system, runs as a user that is not root and has no Linux capability.
-
Copy the file.
-
Write the required values to an env file, for example
hosted.env. Compose stops with a message when one of them is missing.Variable Value VELLUM_MASTER_KEYThe master key. Passed to Vellum as Vellum__MasterKey.VELLUM_SETUP_TOKENThe setup token. Passed as Vellum__SetupToken.VELLUM_HOSTSThe public host names, separated by ;. Passed asAllowedHosts.VELLUM_POSTGRES_PASSWORDThe password of the database user. VELLUM_TRUSTED_PROXIESThe addresses or the CIDR ranges of your reverse proxies, separated by ;. Passed asVellum__TrustedProxies. -
Set the optional values that you need.
Variable Default Value VELLUM_BIND127.0.0.1The address on the machine that publishes the app port. VELLUM_PORT8080The app port on the machine. VELLUM_SUBNET172.30.250.0/24The address range of the Compose network. VELLUM_PREVIOUS_MASTER_KEYempty The old key during a rotation. OTEL_EXPORTER_OTLP_ENDPOINTempty The address of an OTLP collector. Empty means that telemetry is off. -
Start the services.
Terminal window docker compose -f docker-compose.hosted.example.yml --env-file hosted.env up -d
The file gives the Compose network a fixed address range, so that the value of VELLUM_TRUSTED_PROXIES stays correct after a restart.
- For a proxy that runs on the Docker host, Vellum sees the gateway of the network. With the default range the gateway is
172.30.250.1. SetVELLUM_TRUSTED_PROXIES=172.30.250.1. - For a proxy that is a service of the same Compose project, Vellum sees an address in
172.30.250.0/24.
If Docker answers Pool overlaps with other one on this address space, another network uses the range. Set VELLUM_SUBNET to a free range. The gateway is the first address of that range. Name it in VELLUM_TRUSTED_PROXIES.
The file passes only the variables that it lists. To set another variable of Vellum, add it to the environment of the vellum service.
Start one instance with docker run
Section titled “Start one instance with docker run”docker run -d --name vellum \ -p 127.0.0.1:8080:8080 \ -e Vellum__Profile=hosted \ -e "ConnectionStrings__DefaultConnection=Host=<db-host>;Database=vellum;Username=<db-user>;Password=<db-password>" \ -e Vellum__MasterKeyFile=/run/secrets/vellum-master-key \ -e "Vellum__SetupToken=<setup-token>" \ -e AllowedHosts=vellum.example.com \ -e "Vellum__TrustedProxies=<proxy-address>" \ -v /srv/vellum/master.key:/run/secrets/vellum-master-key:ro \ "<vellum-image>"<vellum-image> is the image that you build from the Dockerfile of the repository. The app user in the container must be able to read the key file. <proxy-address> is the address that Vellum sees for your reverse proxy.
The image runs as a user that is not root. docker run does not add the other limits of the Compose files. To add them, use the options --read-only, --tmpfs /tmp, --cap-drop ALL and --security-opt no-new-privileges:true.
Complete the setup
Section titled “Complete the setup”- Read the log:
docker compose logs vellum. A refused start gives one line that starts withVellum did not start.and states the fix. See Vellum does not start. - Open
https://vellum.example.com. - Enter the setup token in the setup screen and create the first account. That account is the instance administrator.
- Remove
Vellum__SetupTokenfrom the environment after setup. Vellum needs it only while no user exists. - Decide whether people can register. In the hosted profile registration is open after setup. The instance administrator changes this with Let people register themselves in the instance settings: see Configure settings.
Configure sign-in with GitHub
Section titled “Configure sign-in with GitHub”This step is optional. Without it, people sign in with an email address and a password.
- At GitHub, create an OAuth app.
- Set the callback URL of the OAuth app to
https://vellum.example.com/api/auth/external/callback. Use your public host name. - Set
Authentication__GitHub__ClientIdandAuthentication__GitHub__ClientSecretto the client ID and the client secret of the OAuth app. - Start Vellum again.
Vellum asks GitHub for the scope user:email. It reads the account ID, the display name and the email addresses of the account. It uses the access token for these reads and does not store it.
Vellum accepts only the primary email address of the account, and only when GitHub verified it. An account with no such address gets 400 with the title The provider gave no verified email address.
The path /api/auth/external/callback is the only path that you register at GitHub. After Vellum checks the answer of GitHub, it sends the browser to /api/auth/external/complete. That endpoint creates the account or the session.
A first sign-in with GitHub creates an account, if registration is open. Vellum does not link GitHub to an account that exists. A sign-in with the email address of such an account gets 409.
On an instance with no client ID, GET /api/auth/external/GitHub answers 404. The sign-in screen shows the button on each instance.
Setup and sign-in problems lists the answers of a sign-in that fails.
Run more than one instance
Section titled “Run more than one instance”Each instance must have the same database, the same master key and the same AllowedHosts.
What the instances share through the database:
- The keys that sign the session cookie. They are in Postgres, sealed with the master key. A session from one instance is valid on each other instance.
- The migrations. Each instance takes a Postgres advisory lock before it applies migrations, so instances that start together do not collide.
- The repository scans. Each instance runs a scan worker. A worker claims one scan with
FOR UPDATE SKIP LOCKED, so two workers never take the same scan. The worker holds a lease of 60 seconds and renews it while it runs. When an instance stops, the lease ends and another worker takes the scan again. Each claim raises a token, and a write with an old token is refused. A scan whose worker stopped two times in a row is not run a third time.
What the instances do not share:
- The rate limits. The limits for a caller with no credential and the limits per caller count in the memory of one instance. With three instances a client can get three times the limit. Security model gives the numbers.
- A stress run. It executes in the request that starts it, on the instance that got the request. It is not a background job.
- The bound on stress runs at the same time. The instance policy Stress runs at the same time applies to each instance by itself. With three instances and a value of 4, the deployment can do 12 runs at the same time.
To run the scan worker on some instances only, set Import__Worker__Enabled=false on the other instances. Import__Worker__Parallelism sets the number of scans that one instance runs at the same time, from 1 to 32.
The local profile with a generated master.key does not fit more than one machine, because the key file is on one volume. Give the key through the environment.
Health
Section titled “Health”Vellum has three health endpoints. Each needs no sign-in and gives no detail.
| Endpoint | Answers 200 when |
Use it for |
|---|---|---|
GET /health |
The app reaches its database. | A load balancer that has one check, and the container health check. |
GET /health/ready |
The app reaches its database. | The readiness probe of an orchestrator. |
GET /health/live |
The process answers. It does not ask the database. | The liveness probe of an orchestrator. |
/health and /health/ready answer 503 when the app does not reach its database.
Do not use /health or /health/ready as a liveness probe. An orchestrator restarts a container when its liveness probe fails. A restart does not repair a database outage.
The container health check runs dotnet Vellum.dll health, which calls /health.
Telemetry
Section titled “Telemetry”Telemetry is off by default. To send traces, metrics and logs to an OTLP collector, set OTEL_EXPORTER_OTLP_ENDPOINT and start again. See Diagnostics and logs.