This page covers a real production deployment with versola-cli’s vps target: a Linux server with a PostgreSQL it connects to (native on the same machine, or a managed or remote one), on which the CLI runs Versola’s services, their secrets store (OpenBao) and a reverse proxy in front of them. If you only want to try Versola out, see Installation instead — this page is for standing up (or redeploying) the real thing. Every versola command used here is described in the CLI reference.
What the CLI sets up for you
On vps, versola-cli runs everything except the database:
central,auth,edge— Docker containers withnetwork_mode: host, each bound to127.0.0.1.centrallistens on127.0.0.1:28090; the proxy fronts it on127.0.0.1:8090, the addressauth,edgeand the admin console call, so thatrolloutcan replacecentralwithout an outage.- OpenBao — the secrets store every secret lives in. The CLI starts it and, on the first run, initializes and configures it by itself.
- The reverse proxy — the official
nginximage, with its whole configuration generated by the CLI. It routes your domain toauth/edge, serves the admin console at/central/admin/, and — unless you already run your own web server — takes ports 80/443 and gets a Let’s Encrypt certificate itself.
What stays yours:
- PostgreSQL — a native install on the server or a managed/remote service, not a container. The CLI never creates or owns it; you create the role, database and schemas once (the CLI writes the SQL for the role to a file).
The vps target also starts a small internal certificate authority (step-ca, with a one-shot mtls-init container) that backs mutual TLS between edge and auth; it stays on loopback and needs nothing from you.
All of it runs as one Docker Compose project (versola-vps), so versola status, down and uninstall cover the whole deployment. Each service runs one container by default; versola replica adds more copies of auth or edge (see Scaling out).
Prerequisites
- A Linux VPS with Docker and the Docker Compose v2 plugin.
- A PostgreSQL that the server can reach. Versola’s CI runs against PostgreSQL 18; older versions are not verified.
- Native on the same machine, with superuser access (
sudo -u postgres psql) to create Versola’s role, database and schemas; or - managed or remote (a cloud database, a separate database host): an address reachable from the server, a role that can create schemas in its database, and, if its TLS certificate is signed by a CA that Java doesn’t trust by default, that CA’s certificate file (PEM). See Step 3 and Step 4.
- Native on the same machine, with superuser access (
- A domain for Versola (e.g.
id.example.com) with a DNSArecord (andAAAA, if the server has IPv6) pointing at the server. - Either ports 80 and 443 free (Versola’s proxy takes them and handles TLS), or your own web server already on 80/443 that will forward the domain to Versola — see Step 2.
versola-cliinstalled on the VPS itself. Thevpsmemory check reads/proc/meminfoon the machine it runs on, and inexternalmode the proxy listens on the server’s127.0.0.1only.- Enough free memory:
doctorrequires 1 GiB available if Versola is already running (a redeploy), 3 GiB for a cold start, plus 256 MiB if OpenBao isn’t running yet, and about 4 GiB of free disk. Each service’s JVM takes a fixed 512 MiB heap by default (see Configuration).
Step 0 — install or update versola-cli
curl -fsSL https://raw.githubusercontent.com/versolauth/versola-cli/main/install.sh | sh
export PATH="$HOME/.local/bin:$PATH" # the installer can't change the current shell
The CLI goes to ~/.local/bin. If that directory wasn’t on your PATH yet, the export line makes versola usable in this shell; also add it to your shell’s startup file (~/.bashrc, or ~/.zshrc for zsh), as the installer suggests, so new terminals find it too.
If it’s already installed, versola upgrade checks the latest release and replaces the binary in place (it refuses to touch a dev build from source). The installer verifies the download against the release’s checksums.txt. See the CLI reference for pinning a CLI version and for supported platforms.
Step 1 — run doctor
versola doctor --target vps
Confirms Docker is reachable, the Compose v2 plugin is present, and there’s enough free memory and disk space (thresholds are in the CLI reference). configure/migrate/up run the same checks themselves, so this step is optional but gives you the full picture up front.
Step 2 — choose how Versola is exposed
The --proxy flag of configure/bootstrap decides where Versola’s proxy listens.
--proxy nginx (default) — Versola owns 80/443
For a server that runs nothing else on the web ports. The proxy listens on 80 and 443; with an https:// --auth-url it obtains and renews a Let’s Encrypt certificate itself (nginx’s own ACME module — no certbot), and redirects HTTP to HTTPS.
- For TLS,
--auth-urlmust be exactlyhttps://<domain>— no port, no path, not an IP address orlocalhost. (A plainhttp://URL also works, without a certificate — for testing only.) - DNS must already point at the server, and ports 80/443 must be reachable from the internet;
configurefails early, naming the port, if something already uses one. - Certificates are kept in the
versola-acme-vpsDocker volume, so a redeploy doesn’t request a new one. - For a test deployment add
--acme-staging: the certificate then comes from Let’s Encrypt’s staging environment — untrusted by browsers, but without production’s rate limits.
--proxy external — behind your own web server
For a server that already serves other sites on 80/443 with its own TLS. Versola’s proxy then listens only on 127.0.0.1:2821 (plain HTTP), and your web server forwards the domain to it and keeps handling TLS. It has to run on the same host and pass the right headers — for nginx:
server {
server_name id.example.com;
listen 443 ssl;
# ssl_certificate ... — your existing TLS setup
client_max_body_size 8m;
location / {
proxy_pass http://127.0.0.1:2821;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
Host— passed through as-is. Versola doesn’t trust it: the issuer, redirect URLs and the DPoPhtuall come from--auth-url, not from request headers.X-Forwarded-For— trusted only from127.0.0.1; Versola restores the real client address from it and uses it for rate limiting.X-Forwarded-Proto— the scheme the browser used; standard for a proxy that terminates TLS (Versola itself takes the scheme from--auth-url).client_max_body_size 8m— the admin console uploads themes and keys; your server must allow the same body size as Versola’s proxy.
All of Versola’s routing (which path goes to auth and which to edge, the admin console) is inside Versola’s proxy — your web server forwards everything for the domain, nothing more.
The domain also has to resolve from the server itself (central calls auth at that address): without DNS for it central logs UnknownHostException, and nothing answers on the domain until your web server does.
Step 3 — configure
versola configure vps 0.7.2 \
--auth-url https://id.example.com \
--postgres-host 127.0.0.1:5432
# add --proxy external for Step 2's second option
For a managed or remote database, give its address, the database name and the CA file:
versola configure vps 0.7.2 \
--auth-url https://id.example.com \
--postgres-host db.internal.example.com:5432 \
--postgres-db versola \
--postgres-ca ~/postgres-ca.pem
--postgres-host:host:portof the database. For a cluster, use the address that follows a failover (usually a read-write alias), not one node’s name.--postgres-db: the database name,authwhen omitted. The three services use it, each in its own schema.--postgres-ca: a PEM file with the CA that signed the database’s certificate. A database on a loopback address is not verified; every other one is, and in production the services refuse to start when the certificate can’t be validated (PKIX path building failed). This flag is how the services get the CA. The CLI copies the file into the bundle and keeps a copy next to its state, so you don’t need the original afterwards.- The loopback exemption is about the address you write:
127.0.0.1(orlocalhost,[::1]) is not verified, but any other host name is, even one that resolves to this same machine. The name then has to be in the certificate’s subject alternative names (a native Postgres’s default self-signed certificate is valid for the machine’s hostname only), and a self-signed or private CA has to be given with--postgres-ca. --postgres-dband--postgres-caneedversola-cli0.2.10 or later and aversola-toolsfrom 0.7.1. Both are remembered for laterconfigureruns,rolloutandsecrets plan; give them again only to change them.
--auth-url must be exactly the origin browsers use — scheme and host (plus a port only if it’s not the default one, and never in nginx mode). The CLI rejects paths and query strings (and lowercases the host), because a mismatch would silently break redirects and passkeys.
configure checks the machine, pulls ghcr.io/versolauth/versola-tools:<version> to generate this release’s configuration and admin console, writes the proxy’s configuration, and resolves every secret against OpenBao. It starts none of Versola’s own services and touches no database, so it’s safe to run repeatedly. Like migrate and up, on vps it asks for confirmation first; there is no --yes flag for these prompts.
OpenBao is set up automatically. On the first run configure starts the versola-openbao-vps container and provisions it: initialization with a single unseal key, unseal, a KV v2 secrets engine, AppRole authentication, and a policy limited to Versola’s own secrets. It keeps two files in ~/.versola/openbao/:
vps.json— the AppRole credentials the CLI reads and writes secrets with.configureprints only the path to this file, never the values;vps-admin.json— OpenBao’s root token and unseal key. OpenBao comes back sealed after every restart; before it logs in (configure,rollout,secrets plan,secrets test,secrets login) the CLI unseals it with this key, sending the key only to a local address, andconfigurerecreates the container if it was stopped. Copy both files somewhere safe, off the server — neither can be recovered if lost.
Every secret — signing keys, session keys, the Postgres password, the admin bootstrap password — is generated once, stored in OpenBao, and reused by every later configure. Each release lists its secrets in a schema that comes with it: a secret that is new in a later release is generated on the upgrade to it, while the keys your data depends on (signing keys, the keys of password hashes and refresh tokens, …) are generated only on the first install — if one of them goes missing later, configure stops with nothing written instead of silently replacing it. The services never talk to OpenBao at runtime: configure writes the resolved values next to the generated configuration, and OpenBao only needs to be up while you run it.
Prefer to run OpenBao yourself? See Managing OpenBao yourself.
Step 4 — create the Postgres role, database and schemas
On the first configure the Postgres password is generated, so the role can only be created after that run. configure never prints the password: it writes the SQL that gives Versola’s role that password to ~/.versola/openbao/vps-postgres-role.sql (readable only by you) and prints the command to run it:
sudo -u postgres psql < ~/.versola/openbao/vps-postgres-role.sql
The file creates the role versola_app if it doesn’t exist and otherwise sets its password and allows login. It holds the password’s SCRAM-SHA-256 verifier, not the password itself, so the password doesn’t reach Postgres’s server log either. It needs psql 10 or newer and a POSIX shell (the server’s, not PowerShell). Use < rather than psql -f: the postgres user can’t open a file in your home directory, your shell can.
Then, as the Postgres superuser, create the database and one schema per service:
CREATE DATABASE auth OWNER versola_app;
\c auth
CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION versola_app;
CREATE SCHEMA IF NOT EXISTS central AUTHORIZATION versola_app;
CREATE SCHEMA IF NOT EXISTS edge AUTHORIZATION versola_app;
Run them one at a time — pasting a block that mixes psql meta-commands (\c) with SQL leads to confusing parse errors. This is the simplest topology: one database with a schema per service, and it is what the vps target generates (all three connection URLs point at one database, auth by default, each with its own currentSchema). To use another database name, pass --postgres-db <name> to configure or bootstrap and use that name in the CREATE DATABASE and \c lines above. The standard topology for larger setups is a database per service, where auth, central and edge each own a database and their own migration history; the vps target does not generate that, so you would have to change the connection URLs yourself. Either way don’t share a schema: each service keeps its migration history inside its own schema, and two services in one schema fail with FlywayValidateException.
The file is written only on that first run; later runs reuse the password stored in OpenBao.
Postgres TLS
Every connection the services open to Postgres uses sslmode=verify-full unless the JDBC URL sets its own sslmode: the database’s certificate must chain to a trusted CA and match the host name in the URL. The generated configuration writes sslmode=disable when --postgres-host is a loopback address (the native Postgres in this guide), so this matters when you point Versola at a remote database.
If the database’s certificate is not signed by a CA the JVM already trusts, pass the CA bundle with --postgres-ca (Step 3). The file must contain a PEM certificate. The CLI copies it into the bundle (postgres-ca/ca.crt), mounts it read-only into central, auth, edge and the migration container, and writes it as postgres.ssl-root-cert in the three configurations, so versola migrate verifies the database with the same CA.
A URL that does not verify (sslmode=disable, allow, prefer, require, or ssl=false) logs a warning at startup and, when the configuration has env = "prod", stops the service from starting. Loopback hosts (localhost, 127.0.0.1, [::1]) are exempt from both. See Configuration for the full rules.
Managed or remote Postgres
There is no superuser, so the role and database come from your provider’s console or CLI instead: create a user versola_app that owns a database (named as you passed to --postgres-db). Set its password to the one configure generated. It is POSTGRES_PASSWORD in auth.secrets.env of the newest bundle in ~/.versola/active/; configure never prints it. Or feed the SQL file from the previous step to psql connected as an administrator role of the cluster. Then create the schemas as that user:
BUNDLE=$(ls -dt "$HOME"/.versola/active/bundle-* | head -1)
export PGHOST=db.internal.example.com PGPORT=5432 PGDATABASE=versola PGUSER=versola_app \
PGSSLMODE=verify-full PGSSLROOTCERT="$HOME/postgres-ca.pem"
export PGPASSWORD=$(grep '^POSTGRES_PASSWORD=' "$BUNDLE/auth.secrets.env" | cut -d= -f2-)
psql -c "CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION versola_app"
psql -c "CREATE SCHEMA IF NOT EXISTS central AUTHORIZATION versola_app"
psql -c "CREATE SCHEMA IF NOT EXISTS edge AUTHORIZATION versola_app"
The PG* variables are what the psql, pg_dump and pg_restore commands below use. Some providers apply a new role password a few seconds late, especially behind a connection pooler: password authentication failed right after the change deserves one retry.
A pooler in transaction mode cannot carry the LISTEN connection that central and edge need, and the CLI does not generate postgres.notifications-url. Point --postgres-host at the direct (non-pooled) address, or read Configuration.
Step 5 — back up the database (redeploys)
Migrations cannot be rolled back. On a fresh install there’s nothing to back up yet; before every later migrate, take a dump:
(umask 077; sudo -u postgres pg_dump -Fc auth > ~/versola-backup-$(date +%Y%m%d-%H%M%S).dump)
# managed or remote database, with the PG* variables from Step 4:
(umask 077; pg_dump -Fc > ~/versola-backup-$(date +%Y%m%d-%H%M%S).dump)
pg_dump runs as postgres but writes to stdout, so the file is created by your own shell — in your home directory, owned by you, readable only by you (umask 077: the dump holds password hashes and encrypted secrets). Store a copy somewhere durable, not only on the server.
Step 6 — migrate
versola migrate
Applies each service’s own schema migrations (auth, central and edge each own their schema) using one throwaway container that ships all three services’ migrations and exits when done. No server starts. The run is recorded under ~/.versola, which status then shows. The services never migrate on their own startup — they validate their schema and refuse to start against an out-of-date one.
Also available:
versola migrate --service auth # migrate one service at a time
versola migrate --dry-run # check without applying
Since 0.7.0, a migration of auth makes auth_conversations, authorization_codes and pushed_authorization_requests UNLOGGED. It takes an exclusive lock and rewrites those small tables, so logins on a running older version wait while it runs; run migrate at low traffic. After a Postgres crash, in-flight sign-ins and unredeemed authorization codes are lost and users sign in again; sessions and refresh tokens are not affected. See Configuration.
--dry-run connects to the real database and lists the migrations that would be applied; it applies nothing and does not ask for confirmation. The dry run is performed by the versola-tools image of the version you configured. In releases up to and including 0.6.2 it reports migrations that are merely pending as a validation failure (Detected resolved migration not applied to database). That is a bug of those releases, not a problem with your database; plain versola migrate applies the migrations normally. The fix shipped in 0.7.0.
Step 7 — up
versola up
Starts the stack and waits until it’s actually serving: central first (auth and edge sync their configuration from it), then the proxy’s front of central (the address auth and edge call it at), then auth and edge, and finally waits for the whole proxy and checks that a request through it reaches auth. central itself starts only after step-ca and the one-shot mtls-init container have produced the certificates it reads at startup (0.7.1 and later); without them it exits.
If auth or edge ever start before central is listening, they wait two minutes for it, exit with code 1 and are restarted by Docker (restart: unless-stopped); the restarted process starts normally. versola up avoids this by starting central first.
In nginx mode with HTTPS, if the certificate isn’t issued in about two minutes (DNS not propagated yet, port 80 not reachable), up prints a warning instead of failing: the services are up, and nginx keeps retrying the certificate; check docker logs versola-proxy.
On success it prints the address, the admin login and the file holding ADMIN_BOOTSTRAP_PASSWORD. That password is temporary, valid for 24 hours: the first login with it asks you to set a permanent one. If it expires before you log in, restart auth (docker restart versola-auth) for a fresh 24-hour one. In external mode it also reminds you to point your web server at 127.0.0.1:2821.
Or all at once: bootstrap
versola bootstrap vps <version> --auth-url … --postgres-host … [--proxy external] runs Steps 3, 6 and 7 in one go. On a first run that generates the Postgres password, it stops before migrating (with a non-zero exit code) so you can run the SQL from Step 4 — then continue with versola migrate and versola up.
Step 8 — verify
versola status
Deployed version: 0.7.2 (target: vps)
Migrations applied: 2026-10-14 17:11:51
Replicas: auth x1 (slot 1), edge x1 (slot 1)
NAME IMAGE … STATUS
versola-auth ghcr.io/versolauth/versola-auth:0.7.2 … Up 2 minutes
versola-central ghcr.io/versolauth/versola-central:0.7.2 … Up 3 minutes
versola-edge ghcr.io/versolauth/versola-edge:0.7.2 … Up 2 minutes
versola-openbao-vps openbao/openbao:2.5.4 … Up 10 minutes
versola-proxy nginx:1.30-alpine … Up 1 minute
(The table is docker compose ps for the deployment, shortened here; it also lists versola-step-ca.)
Then from outside:
AUTH_URL=https://id.example.com # exactly the --auth-url you passed to configure
curl -s -o /dev/null -w "%{http_code}\n" "$AUTH_URL/.well-known/openid-configuration" # 200
and log in to the admin console at <auth-url>/central/admin/ before considering the deploy done.
Redeploying later
Moving to another version is the same three steps with the new version number and the same flags as the first deployment — configure doesn’t remember --auth-url, --postgres-host, --proxy, --acme-staging or --setup-openbao (it does keep --postgres-db and --postgres-ca). In particular keep --proxy external if you used it: without it the proxy falls back to the default nginx mode and configure stops on your web server’s ports 80/443.
Pre-1.0: Versola does not guarantee compatibility before 1.0.0. Configuration, database schema and APIs can change between 0.x releases without a migration path. Back up your database before upgrading.
Versola publishes no upgrade guide between 0.x releases. Read the release notes of the version you are moving to, and expect that a database created by an older release may not be migrated: the schema migrations were squashed in 0.7.0, so a database created by an earlier release is not upgraded in place and 0.7.x needs a fresh database. Treat a move between 0.x versions as a redeploy you can undo only from a backup.
versola upgrade # keep the CLI current
# back up the database (Step 5)
versola secrets plan vps <new-version> # which secrets configure will keep, create, or stop on
# drop --proxy external if your first deployment used the default nginx mode
versola configure vps <new-version> --auth-url https://id.example.com --postgres-host 127.0.0.1:5432 --proxy external
versola migrate
versola up
configure regenerates the configuration, proxy and admin console from that release; secrets are reused from OpenBao, and configure stops without writing anything if a secret that stored data depends on looks lost (see Secrets). versola secrets plan shows read-only which secrets would be kept or created. configure and migrate don’t touch the running services, so the old version keeps serving while migrations run — the only interruption is up recreating the containers. That also means a migration has to work against the schema the old version expects for as long as it runs; Versola does not check this for you. With more than one replica of auth or edge (versola replica add), configure refuses — use versola rollout below.
Without stopping: versola rollout
With versola-cli 0.3.1 or later, versola rollout <version> --assume-compatible replaces the running version one replica at a time instead of recreating everything at once: it prepares the new version next to the old one, runs the migrations, moves central through a temporary second replica (the admin console stays available), then replaces each auth and edge replica behind a temporary extra replica. --dry-run shows the plan and changes nothing; if it is cut off, run the same command again; versola rollout --abort goes back to the previous version without recreating the replicas that never left it. --assume-compatible is your confirmation that the migrations are additive and the running version works on the new schema — releases before 1.0 don’t promise that, so read the release notes first. It checks the new version’s secrets the same way configure does, and before a temporary replica starts it checks free memory (about 1.13 GiB) and spare Postgres connections. --no-surge does without the extra replica; central is then restarted in place, as it is on the first rollout of a deployment made before the proxy fronted central, and the admin console is unavailable until it is ready. While a rollout is unfinished, configure, up and replica refuse. Take a database backup (Step 5) and copy ~/.versola/active/state.json before starting. Details, the checks it makes and the short outages it still has: the versola-cli README.
Rolling back
While a versola rollout is unfinished, versola rollout --abort goes back to the previous version (migrations and secrets stay). The rest of this section covers a finished rollout and a schema the previous version can’t run on.
Migrations are not reversible: there are no down-migrations, so the only way to undo one is to restore the dump from Step 5. Whether an older version can run on a newer schema is not guaranteed in 0.x; do not rely on it.
To go back, restore the database before starting the previous version — never start it against a schema only the new release understands:
versola down # nothing may write while restoring
sudo -u postgres psql -c "DROP DATABASE auth;"
sudo -u postgres psql -c "CREATE DATABASE auth OWNER versola_app;"
sudo -u postgres pg_restore -d auth < ~/versola-backup-<timestamp>.dump # the file is opened by your shell, not by postgres
# drop --proxy external if your first deployment used the default nginx mode
versola configure vps <previous-version> --auth-url https://id.example.com --postgres-host 127.0.0.1:5432 --proxy external
versola migrate # no-op: the dump already has the previous schema
versola up
versola down also stops OpenBao, which runs in the same Compose project; the next configure starts and unseals it again. If you need the previous CLI, install it from the releases page. On a managed or remote database you can’t drop the database you are connected to, and there is no superuser. Drop Versola’s three schemas instead (other schemas in that database stay) and restore the dump into it; the dump recreates the schemas:
versola down
psql -c "DROP SCHEMA auth CASCADE; DROP SCHEMA central CASCADE; DROP SCHEMA edge CASCADE;"
pg_restore --no-owner -d "$PGDATABASE" < ~/versola-backup-<timestamp>.dump
versola configure vps <previous-version> --auth-url https://id.example.com --postgres-host db.internal.example.com:5432
versola migrate
versola up
If you added replicas of auth or edge, remove them first (versola replica remove auth|edge <n>) — configure refuses while a service has more than one. Anything written between the dump (Step 5) and the rollback is lost, which is why the dump is taken right before migrate.
Scaling out
versola replica add <auth|edge> [count] starts more copies of auth or edge behind Versola’s reverse proxy, which balances requests between them; versola replica remove stops them again. central is not replicated, and a service can have at most 9 replicas. The deployment must be running (versola up) first, and the replica count is recorded, so versola up brings back the same replicas. Details and flags are in the CLI reference.
Managing OpenBao yourself
If you run your own OpenBao setup, pass --setup-openbao to configure/bootstrap: the CLI then never uses OpenBao’s admin API, and you provision it and store AppRole credentials yourself. Unsealing after a restart is then also yours.
The order: run versola configure vps … --setup-openbao once — it starts the versola-openbao-vps container and then stops with “no OpenBao credentials stored”, which is expected — do the steps below, store the credentials with versola secrets login, and run the same configure again. TLS is disabled on this OpenBao (it listens only on 127.0.0.1:8200), but the bao CLI defaults to https, so every command needs BAO_ADDR:
# 1. Initialize — save BOTH values printed; neither is recoverable if lost.
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 versola-openbao-vps \
bao operator init -key-shares=1 -key-threshold=1
# 2. Unseal — again after every container restart.
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 versola-openbao-vps \
bao operator unseal <unseal key>
# 3–4. KV v2 and AppRole.
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao secrets enable -path=secret kv-v2
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao auth enable approle
# 5. A policy limited to Versola's own secrets.
docker exec -i -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao policy write versola-vps - <<'EOF'
path "secret/data/versola/vps/*" {
capabilities = ["create", "read", "update"]
}
EOF
# 6. An AppRole bound to it (a long-lived credential for an unattended tool).
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao write auth/approle/role/versola-vps \
token_policies="versola-vps" token_ttl=1h token_max_ttl=4h \
secret_id_ttl=0 token_num_uses=0
# 7. The credentials the CLI needs.
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao read auth/approle/role/versola-vps/role-id
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao write -f auth/approle/role/versola-vps/secret-id
Then store them — the secret ID is asked for in a masked prompt, so it never lands in shell history:
versola secrets login vps http://127.0.0.1:8200 <role-id>
Writing the policy from Windows? Save the HCL file as ASCII, not the default UTF-8 — a UTF-8 BOM breaks OpenBao’s parser with
illegal char at 1:1. Write it locally,docker cpit in, thenbao policy write versola-vps /path/inside/container.hcl.
Changing a stored secret
Secrets live at secret/versola/vps/{auth,central,edge}; the utils client key is kept apart at secret/versola/vps/utils. See Secrets for what each one protects, which ones cannot be rotated, and how configure treats stored values. Use bao kv patch — kv put replaces the whole path and would wipe every other key there — with the root token from ~/.versola/openbao/vps-admin.json:
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao kv patch -mount=secret versola/vps/auth SOME_KEY='<value>'
An existing value in OpenBao always wins, so this is how you replace one. Then run versola configure vps <current-version> with your usual flags and versola up. POSTGRES_PASSWORD is shared by all three services and must be identical under auth, central and edge — configure refuses to continue if they disagree.
Not every secret can be rotated this way. The signing key pairs and the keys that protect stored data — PASSWORDS_SECRET (password hashes), REFRESH_TOKENS_SECRET and EDGE_TOKEN_ENC_KEY (refresh tokens), CLIENT_SECRETS_SECRET (client secrets), CENTRAL_RESOURCE_SECRET — make that data unreadable when replaced: users can’t log in, tokens and sessions are rejected. Change them only on a deployment whose data you are willing to lose. The private key of the utils client lives apart, at secret/versola/vps/utils.
Troubleshooting
configure fails because port 80 or 443 is in use. Something (usually a web server) already listens there. Either stop it, or use --proxy external behind it.
OpenBao doesn’t start after a reboot, or is sealed. Your running services aren’t affected — they don’t need OpenBao at runtime. The next command that logs in to it (configure, rollout, secrets plan, secrets test, secrets login) unseals it with the saved key, and configure recreates the container if it was stopped. With --setup-openbao there is no saved key: unseal it yourself (bao operator unseal).
“container name already in use”. A container with one of Versola’s fixed names exists under another Compose project. Check with docker ps and docker compose ls, stop and remove those containers by name, then rerun.
502 through your web server (external mode). Check that the proxy answers locally: curl -H 'Host: id.example.com' http://127.0.0.1:2821/.well-known/openid-configuration. If it does, the problem is in your web server’s forwarding; if it doesn’t, check versola status and docker logs versola-proxy.
The admin console shows 404 right after up. Reload once — the first request right after startup can hit a service that’s still warming up.
A service crash-loops with PKIX path building failed (or certificate_unknown). The database’s certificate is signed by a CA Java doesn’t trust, and the services verify it. Pass the CA with --postgres-ca <file> to configure (Step 3) and run versola up again. Don’t edit the generated files by hand: the next configure replaces them.
password authentication failed right after setting the role’s password. On a managed Postgres the new password can take a few seconds to apply; retry once. If it persists, the database’s password differs from POSTGRES_PASSWORD in OpenBao: set the database’s to that value.
up warns that the certificate isn’t issued (nginx mode). Let’s Encrypt has to reach the server on port 80 under your domain. Check that the DNS record points at the server’s public IP, that any CDN or proxy in front of the record (for example a “proxied” Cloudflare record) is off while the certificate is issued, and that your firewall lets 80 and 443 in. Try --acme-staging first to avoid production rate limits.
configure stops with the secrets plan has N problem(s). Nothing was written. Run versola secrets plan vps <version> to see which secret and why: lost — a key your data depends on is missing from OpenBao (restore it from a backup); unknown-state — OpenBao holds secrets this machine has no record of (typically uninstall left OpenBao’s volume behind, or another machine deployed), and a key your data depends on is missing from it (remove the volume for a truly fresh install, or name the missing secrets in --generate-missing if fresh values are fine); vault-empty-with-state — this machine deployed before but OpenBao is empty (are you pointing at the right one?). --generate-missing NAME,... lets the named secrets be generated anyway — only when the old value is gone for good.
image not found. Check the version format — tags have no leading v: <version>, not v<version>. Published versions are listed on the versola-tools package page.
Next steps
- CLI reference — every command and flag used on this page.
- Secrets — what lives in OpenBao and how to rotate it.
- Observability — health endpoints, metrics and logs of the running stack.