Deploy on a managed host
The five install steps on a server that Laravel Forge, Ploi, RunCloud or a similar platform manages, in the platform's own screens and with no terminal. About ten minutes on a site that already exists. Forge is the worked example; other platforms use different labels for the same things.
Before you start
- A site created as Other or Custom application, never Laravel, Symfony, WordPress or another framework template, with Website isolation switched on. Isolation gives the site its own operating-system user for when several installations share a server; it does not replace separate servers when strong isolation is required.
- An HTTPS certificate for the site's hostname, issued in the platform's certificate settings. If the final DNS record is not ready, use the platform's temporary HTTPS domain and move to the final domain later. Don't configure company login against a temporary domain.
- Node 24.19.0 or newer within 24.x on the server, as shown in the platform's server settings.
/home/forge/capacity.example.com stands for your site's folder. forge is the site's user; a site with website isolation uses its own user, so use that name in every /home/forge path and in CAPACITYLENS_DB. The commands name release 0.73.0-alpha.1, the first with an archive; for a newer release, use its version.
The five steps
Deploy script: create the data folder, then download, unpack and switch the
currentlink to the release.bashmkdir -p /home/forge/capacitylens-data cd /home/forge/capacity.example.com curl -fsSLO https://github.com/Kevinjohn/capacitylens/releases/download/v0.73.0-alpha.1/capacitylens-0.73.0-alpha.1.tar.gz tar -xzf capacitylens-0.73.0-alpha.1.tar.gz && ln -sfn capacitylens-0.73.0-alpha.1 currentRun the deploy once. If the platform offers no deploy script for a site without a repository, run the same lines once in the site's command runner instead. These lines do not start or restart the server: step 4 starts it, and after every later deploy you restart the background process yourself. To upgrade later, follow Upgrade.
Data folder:
/home/forge/capacitylens-data, outside the release so upgrades keep it. The deploy script's first line creates it. The database, the audit log and the scheduled backups (in itsbackupsfolder) live here. Never put the database under a release folder orcurrent/: release folders are replaced and deleted.Environment: run
initonce in the site's command runner (Forge: the site's Commands panel, which starts in the site's folder), with your address:bashnode current/server/dist/index.mjs init --public-url https://capacity.example.com --db /home/forge/capacitylens-data/capacitylens.dbIt prints five lines, with the session secret and the Owner's one-time setup token generated. Paste them into the site's environment editor, and store the setup token in a password manager before you save. The server refuses to start while the address, the secret or the token is empty, and names the missing one. Never commit the secret or the token, or paste them into deployment logs, support messages or issue reports.
The editor saves the site's
.env, one level abovecurrent, so every release reads the same file. KeepNODE_ENV=productionon the API process: the line frominitsets it, and production mode turns on scheduled backups, structured logs, deep health and rate limiting by default.For a single-company installation, add nothing else and leave these unset:
VITE_CAPACITYLENS_DEMO,CAPACITYLENS_SEED_DEMO,CAPACITYLENS_MULTI_ACCOUNT,CAPACITYLENS_BOOTSTRAP_TOKENandCAPACITYLENS_ALLOW_OPEN_SIGNUP. If the platform already sends an HSTS header, addCAPACITYLENS_HTTPS=0to avoid a duplicate.Quote values pasted from elsewhere
Node cuts an unquoted
.envvalue short at#, and the server then starts with the wrong secret instead of failing. The secretsinitgenerates are safe unquoted. Wrap anything pasted from elsewhere, such as a company-login client secret, in single quotes:dotenvCAPACITYLENS_GOOGLE_CLIENT_SECRET='the value exactly as issued'Background process (daemon) and nginx: create one background process that runs as the site's user, with the directory
/home/forge/capacity.example.com/current, one process, a stop wait of more than 10 seconds and this command:bashnode --env-file=../.env server/dist/index.mjsThe server needs those seconds to finish in-flight requests and backup work when it stops. Record the platform's name for the process (Forge: its Supervisor group, such as
daemon-1234567).Then, in the site's nginx file, replace the generated
location /block with these locations, which send every request to the server. Keep the platform's other generated lines, such as includes, TLS directives and Forge's# FORGE CONFIGcomments, so the platform can keep managing certificates. Do not add a secondlocation /: it fails the platform's configuration test.nginxclient_max_body_size 6m; location / { proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 130s; } # These request lines carry single-use invitation, sign-in and password-reset secrets. location ~ ^/api/invites/[^/]+/(accept|signup|preview)$ { access_log off; proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 130s; } location ~ ^/api/auth/(callback|oauth2/callback)/ { access_log off; proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 130s; } location ~ ^/(invite|reset-password)/ { access_log off; proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 130s; }The first location is the whole proxy. Keep the three
access_log offlocations: without them, anyone who can read the access log could use the links in it to accept an invitation or reset a password.Save through the platform, which tests and reloads nginx. If it does not test first, run this and reload only after it succeeds:
bashsudo nginx -tThe server listens on
127.0.0.1by default, so it trusts the forwarded headers nginx sets, and rate limits and audit records show the real visitor address. Keep it on loopback; TLS and networking covers the alternatives.Open the address and check the first-Owner screen loads. Use it only once the address is HTTPS and matches
CAPACITYLENS_PUBLIC_URLexactly. If you are also the Owner, continue with Set up your company and the setup token from step 3; otherwise, hand over.
One process per database
Exactly one CapacityLens process may use a database. Switch off any zero-downtime deploy that starts the new release beside the old one, and restart the background process after the deploy script instead.
After the first start
Open https://capacity.example.com/api/health, with your own address. Expect "ok":true, "db":true, "audit":"ok" and a backup whose status is "ok"; it reads "pending" until the snapshot the server takes as it starts has finished. Treat degraded, or pending that lasts longer than one backup interval (60 minutes by default), as a failure; Monitoring and health checks defines the alerts.
If the request fails, read the background-process log and fix the first startup error before editing nginx. Some command runners show their own temporary output-file error after a command finishes; that does not prove the server failed. To rule nginx out, run this in the command runner:
curl -fsS http://127.0.0.1:8787/api/healthOpen the address in a private window: the first-Owner screen must load, and refreshing a nested route must return the app rather than an nginx 404.
Set the platform's post-deployment or uptime health URL to https://capacity.example.com/api/health. Use the public HTTPS address, not the loopback port, so the check proves DNS, TLS, nginx, the server and the database together.
Hand over to the first Owner
If you are not the Owner, send them the address and Set up your company, and send the setup token separately and privately. The Owner then creates their own sign-in and company.
Once the Owner can sign in, remove CAPACITYLENS_SETUP_TOKEN from the environment, restart the background process and check health. Destroy the transferred copy unless your recovery policy needs a protected record. First-Owner setup is open only while the database holds no users and a token is set, so erasing the sole identity reopens it while the token remains. Verify and hand over has the full checklist.
Move to the final domain
Skip this if the site already uses its final domain.
- Add the final domain to the site and create the DNS record the platform asks for. Once DNS resolves to this server, issue the certificate and check the new address in a browser. Keep the temporary domain enabled for now.
- Change
CAPACITYLENS_PUBLIC_URLto the final address: scheme and hostname only, with no path or trailing slash. Change the platform's health-check URL at the same time. - Restart the background process and check
/api/healthon the new address. Existing sessions belong to the old address, so everyone signs in again. - Ask the Owner to sign in on the final domain and make one safe change. Then disable the temporary domain, or make it redirect to the final address rather than serve a second copy.
Add company login
Add company login only once the final address is stable. Before changing sign-in settings, stop the background process and keep a complete recovery bundle off the server: the database with its -wal and -shm files, and the current and rotated audit logs. A database file copied by itself is not a complete recovery point. Start the background process again and check /api/health.
Then follow Set up Google or Microsoft sign-in. The callback origin must match CAPACITYLENS_PUBLIC_URL character for character. Sign-in is configured per installation; enabling it on one does not change another on the same server.
Upgrade
Upgrades has the full procedure. On a managed host:
Read the target release's
CHANGELOG.mdentry for schema changes or operator actions. Confirm health is"ok"and a recent restore test exists.Take a fresh snapshot. There is no "snapshot now" button: the server writes a verified snapshot when it starts, and every
CAPACITYLENS_BACKUP_INTERVAL_MINminutes after that. Restart the background process, wait for the attempt to finish, then confirm the backup folder contains the new verified file. Or use the latest scheduled snapshot if it is only minutes old and nothing important has been written since. List the backup folder newest first:bashls -lt /home/forge/capacitylens-data/backups | head -3Filenames carry a UTC timestamp:
capacitylens-utc-20260911-140233-118.dbwas written at 14:02:33 UTC on 11 September 2026. Copy the newest one off the server. Run this on your own machine:bashscp forge@your-server:/home/forge/capacitylens-data/backups/capacitylens-utc-YYYYMMDD-HHMMSS-sss.db .Change the version in the deploy script's
curlandtarlines, run the deploy, then restart the background process. The restart stops the old release before the new one starts.Check health, sign in, confirm the company opens, read existing data and make one safe change that survives a refresh. Check the process log for restart loops, migration failures or security errors, and confirm a new scheduled backup succeeds.
Keep the previous release folder and the snapshot until those checks pass. Then delete older release folders and archives from the site folder, never the one current points at.
Never copy the live database file
Do not cp or scp capacitylens.db while the server is running. SQLite keeps recent writes in a separate -wal file, so the copy can be missing data or inconsistent, and you won't find out until the restore fails. Copy an integrity-checked snapshot from the backup folder, or stop the server first. Backups and restore covers both.
Roll back
With a schema change, switching code alone is not a rollback: never start the old release against a database the new one has migrated. Put back the capacitylens-pre-migration-vN-to-vM.db snapshot the new release wrote in the backup folder. Everything written since the upgrade is lost. Without a schema change there is no such snapshot; skip steps 2 to 4.
Stop the background process in the platform's daemon screen.
In the command runner, preserve the upgraded database:
bashrollback_dir="/home/forge/capacitylens-data/rolled-back-$(date -u +%Y%m%d-%H%M%S)" && mkdir -m 700 "$rollback_dir" && cp -p /home/forge/capacitylens-data/capacitylens* "$rollback_dir"/List the pre-migration snapshots:
bashls -l /home/forge/capacitylens-data/backups/capacitylens-pre-migration-*.dbCopy the one this upgrade wrote over the database, and remove the upgraded database's
-waland-shmfiles:bashcp /home/forge/capacitylens-data/backups/capacitylens-pre-migration-vN-to-vM.db /home/forge/capacitylens-data/capacitylens.db && rm -f /home/forge/capacitylens-data/capacitylens.db-wal /home/forge/capacitylens-data/capacitylens.db-shmPoint
currentback at the previous release. ReplaceX.Y.Zwith the previous version:bashln -sfn capacitylens-X.Y.Z currentChange the version in the deploy script's
curlandtarlines back toX.Y.Z, so the next deploy does not reinstate the new release. Undo any environment change the new release needed.Start the background process and check
/api/health, then sign in and confirm the company opens.
Run recovery commands
The release holds no pnpm, tsx or development packages. Recovery commands written as pnpm --filter capacitylens-server ... run from a separate maintenance checkout, never from current/:
- Check out the release serving production, or for a migration rehearsal the target release, in a protected folder outside every release folder. Run
pnpm install --frozen-lockfilethere. - Stop the background process before any command that writes to the database. Pass the database, backup and evidence paths as absolute paths; don't copy them into the checkout.
- Run the command, complete its verification, then start the background process and check it.
- Delete the checkout when you are done. Never point nginx or the background process at it, and never start a second server from it against the production database.
When something goes wrong has the command-specific safeguards.
Keep it running
- Continuously: poll the public
/api/healthaddress. - Weekly: review deep health, process restarts, disk space, audit size and backup freshness.
- Monthly: restore a snapshot to a temporary location and complete a restore test.
- Every release: follow Upgrade.
- After a domain or sign-in change: test sign-in and session revocation in a private browser window.
For process status and logs, use the platform's daemon status and log view. Monitoring and health checks explains every health field and alert.
Each installation on a server needs its own site user, domain, data folder, secrets and port. For a second installation, add PORT=8788 (or another free port) to its environment and use that port in its nginx locations. Installations never share sessions, identities, secrets or databases. Site users isolate files and permissions, not CPU, memory or the kernel: watch memory, disk space and restart loops, and move an installation to its own server if it can exhaust resources another needs.
Keep a private record per installation: domains, site user and folder, port, process name, release version, data and backup paths, latest backup and restore test, company-login callback when enabled, and who is responsible for releases. Keep secret values in a password manager or the platform's secret store, not in this record.
Run your own changes
To run CapacityLens from a clone with your own changes, use the development guide. There is no separate managed-host build route.