CT-Ops
Install CT-Ops
Production-ready CT-Ops installation guidance for a single-host Docker deployment.
This guide takes a new CT-Ops deployment from an empty host to a working web UI, healthy ingest service, and an enrolled agent. Use the release-bundle install for normal server deployments. Build from source only when you are developing CT-Ops itself or need to modify the images.
Deployment shape
The standard install runs CT-Ops as a single Docker Compose stack.
| Component | Role | Network exposure |
|---|---|---|
nginx | HTTPS entrypoint for the browser UI and proxied terminal WebSockets. | Public 80 and 443, or your configured NGINX_HTTP_PORT / NGINX_HTTPS_PORT. |
web | Next.js UI and API. | Compose network only. |
ingest | Agent gRPC, health, JWKS, and terminal WebSocket backend. | Public 9443 for agents; HTTP stays internal or proxied. |
db | PostgreSQL and TimescaleDB. | Compose network only. |
solr | Apache Solr search index for CT-Ops-wide global search. | Compose network only. |
migrate | One-shot database migration job. | Runs before app services. |
Agents need two routes back to the CT-Ops server: HTTPS to the web UI URL for
agent binary downloads and updates, and TCP 9443 to ingest for registration,
mTLS, heartbeats, checks, inventory, and tasks.
Prerequisites
| Requirement | Notes |
|---|---|
| Linux host with Docker Engine and Docker Compose v2 | The release bundle is designed for Docker Compose. |
| DNS name for the CT-Ops web UI | Strongly recommended before enrolling remote agents. |
| TLS certificate for the web UI | The installer can start with a self-signed certificate, but production should use a certificate from your CA. |
| Open firewall ports | Browser users need HTTPS. Agents need ingest 9443. |
| Persistent disk | Keep the install directory, database volume, TLS files, licence keys, and .env backed up. |
No local Go, Node.js, or pnpm installation is required for the release-bundle path.
Install from the release bundle
Run the installer from the server that will host CT-Ops.
curl -fsSL https://forgejo.carrtech.dev/carrtech/ct-ops/raw/branch/main/install.sh | bash
cd ct-ops
To pin a specific release instead of taking the latest:
CT_OPS_VERSION=v0.3.0 curl -fsSL https://forgejo.carrtech.dev/carrtech/ct-ops/raw/branch/main/install.sh | bash
cd ct-ops
The installer downloads a small release bundle from Forgejo, verifies the
published SHA-256 checksum, and unpacks docker-compose.yml, start.sh,
.env.example, licence public-key material, and support-data collection tools.
First run
Run start.sh once to create .env from .env.example.
./start.sh
The first run intentionally exits after creating .env. Edit the file before
starting the stack.
$EDITOR .env
At minimum, set the public URL values to your real deployment name.
BETTER_AUTH_URL=https://ct-ops.example.com
BETTER_AUTH_TRUSTED_ORIGINS=https://ct-ops.example.com
AGENT_DOWNLOAD_BASE_URL=https://ct-ops.example.com
REQUIRE_EMAIL_VERIFICATION=true
If nginx cannot bind to 80 or 443, set alternate host ports before the
second run and include the external HTTPS port in every public URL.
NGINX_HTTP_PORT=8080
NGINX_HTTPS_PORT=8443
BETTER_AUTH_URL=https://ct-ops.example.com:8443
BETTER_AUTH_TRUSTED_ORIGINS=https://ct-ops.example.com:8443
AGENT_DOWNLOAD_BASE_URL=https://ct-ops.example.com:8443
If CT-Ops is behind a VM, LXC, Incus, NAT gateway, or cloud load balancer,
forward the external HTTPS port and 9443 to the CT-Ops host. Agents must be
able to reach both from their own network.
Second run
Start the stack.
./start.sh
On this run, start.sh:
- generates
BETTER_AUTH_SECRETwhen it is blank - generates
POSTGRES_PASSWORDwhen it is blank - generates the browser TLS certificate at
deploy/tls/server.{crt,key}when missing - generates the ingest gRPC certificate at
deploy/dev-tls/server.{crt,key}when missing - stamps digest-pinned image references into the compose environment when supplied by the release bundle
- pulls images
- runs migrations
- starts the web, ingest, nginx, database, and Solr services
Check service health.
docker compose ps
All long-running services should become healthy. If a service is unhealthy,
inspect it before creating users or agents.
docker compose logs --tail=200 web
docker compose logs --tail=200 ingest
docker compose logs --tail=200 nginx
docker compose logs --tail=200 db
docker compose logs --tail=200 solr
.env reference
Anything marked security-critical affects authentication, trust boundaries,
privileged endpoints, or credentials. Treat those values like passwords and keep
.env out of tickets, chat, and screenshots.
Public URL and auth
| Variable | Required | Default | Detail |
|---|---|---|---|
BETTER_AUTH_URL | Yes | https://localhost | Public browser URL with no trailing slash. Include a non-standard port when one is used. This drives auth callbacks and session cookie settings. |
BETTER_AUTH_TRUSTED_ORIGINS | Yes | https://localhost | Comma-separated list of origins allowed to complete auth flows. Include every legitimate browser origin. |
REQUIRE_EMAIL_VERIFICATION | Recommended | true | Requires local email/password users to verify their email before sign-in. Set false only when the deployment intentionally allows unverified local users. |
BETTER_AUTH_SECRET | Generated if blank | blank | Random secret used for auth session signing and LDAP bind-password encryption input. Do not reuse between environments. Rotating it invalidates active sessions and can affect stored LDAP credentials. |
Agent download and terminal connectivity
| Variable | Required | Default | Detail |
|---|---|---|---|
AGENT_DOWNLOAD_BASE_URL | Yes for remote agents | https://localhost | Public HTTPS URL agents use for binary downloads and self-update. It must be reachable from every agent host. |
INGEST_WS_URL | No | empty | Browser WebSocket URL for terminal sessions. Leave empty to route through bundled same-origin nginx. Set an absolute wss://... URL only when bypassing nginx intentionally. |
AGENT_DOWNLOAD_BASE_URL covers HTTPS downloads only. The agent gRPC address is
supplied when creating or installing the agent and normally uses the same host
with port 9443, for example ct-ops.example.com:9443.
Reverse proxy and ports
| Variable | Required | Default | Detail |
|---|---|---|---|
CT_OPS_TRUST_PROXY_HEADERS | No | true in the release bundle | Trusts X-Forwarded-For and X-Real-IP for rate-limit client IPs. Leave true only when a trusted proxy overwrites client-supplied forwarding headers and direct access to the web container is blocked. |
NGINX_HTTP_PORT | No | 80 | Host port bound by bundled nginx for HTTP redirect traffic. |
NGINX_HTTPS_PORT | No | 443 | Host port bound by bundled nginx for HTTPS browser traffic. Include this port in the public URL variables when it is not 443. |
Database
| Variable | Required | Default | Detail |
|---|---|---|---|
POSTGRES_PASSWORD | Yes | generated if blank | Password for the bundled PostgreSQL user. If changed after the database is initialized, also change the password inside PostgreSQL. |
POSTGRES_USER | No | ctops | PostgreSQL username used by the compose stack. |
POSTGRES_DB | No | ctops | PostgreSQL database name used by the compose stack. |
Search
| Variable | Required | Default | Detail |
|---|---|---|---|
CT_OPS_SEARCH_BACKEND | No | solr in release bundles | Search backend for the global CT-Ops search modal. Customer bundles use bundled Apache Solr. Local development may use postgres. |
SOLR_URL | No | http://solr:8983/solr/ct_ops_search | Internal URL of the bundled Solr core. Do not expose Solr directly to the network. |
CT_OPS_SEARCH_REINDEX_INTERVAL_MINUTES | No | 30 | How often CT-Ops rebuilds the Solr index in the background. Search requests use the latest completed index and do not trigger reindexing. |
SOLR_HEAP | No | 1024m | JVM heap for the Solr container. Increase for larger estates with many indexed hosts, CVEs, incidents, certificates, and CMDB records. |
PostgreSQL remains the source of truth. The Solr ct_ops_search core stores a | |||
| derived index for fast prefix, partial, and typo-tolerant search across hosts, | |||
| CVEs, service accounts, certificates, Project ToDo’s, future calendar events, | |||
| ITIL incidents, ITIL services, and ITIL CMDB records. The index lives | |||
in the solr_data Docker volume and is rebuilt from Postgres on web-service | |||
| startup and then every 30 minutes by default. |
When CT-Ops writes data that contributes to global search, the web service also listens for PostgreSQL change notifications and queues a debounced Solr refresh for the affected instance. The periodic rebuild remains as a safety fallback for shared catalogue changes or missed notifications. Recurring Operations Calendar events are indexed by their next future occurrence, so recurring planning meetings remain searchable after the original series start date has passed.
Licensing
| Variable | Required | Default | Detail |
|---|---|---|---|
LICENCE_REVOCATION_URL | No | CarrTech revocation endpoint | Signed revocation bundle URL for paid licence JWTs. Leave unset for connected installs. Use an empty value only for fully air-gapped installs that accept expiry-only revocation behavior until upgraded or reconnected. |
The current CarrTech licence verifier public key is baked into the web image and
mounted from ./licence-keys/current.pem in customer bundles. Saved licences
keep using the verifier key captured when they were activated.
Integrations
| Variable | Required | Default | Detail |
|---|---|---|---|
ANSIBLE_API_URL | No | empty | URL for a separately deployed legacy Ansible API. Leave blank unless that provider is enabled and reachable from CT-Ops. |
CT_AUTOMATION_URL | No | empty | Optional default URL for a separately deployed CT-Automation server. Most installs should configure this in Settings -> Integrations -> Automation. |
INGEST_CT_CVE_INVENTORY_PUSH_TIMEOUT | No | 5m | Timeout for ingest-side CT-CVE inventory snapshot pushes. Increase for large inventories or slower CT-CVE matching. |
Load testing
| Variable | Required | Default | Detail |
|---|---|---|---|
CT_OPS_LOADTEST_ADMIN_KEY | No | disabled | Bearer credential for the privileged /api/admin/hosts/bulk-delete cleanup endpoint. Leave unset except in load-test environments. Use at least 32 random bytes and never reuse across deployments. |
Image overrides
| Variable | Required | Default | Detail |
|---|---|---|---|
ENVIRONMENT | No | production behavior | Leave unset for production and customer installs. CarrTech development installs may set DEV; connected upgrades then follow the bundle/dev-X.Y.Z customer-bundle channel and refresh release-owned CT-Ops image refs to matching dev-X.Y.Z tags instead of production digest pins. The upgrade UI shows the active environment plus current and target image refs, with development versions shown as dev-X.Y.Z and production installs shown as vX.Y.Z. |
WEB_IMAGE | No | release-pinned image | Override the web image only when deliberately testing or recovering with a different published image. |
INGEST_IMAGE | No | release-pinned image | Override the ingest image only when deliberately testing or recovering with a different published image. |
Password Manager integration
| Variable | Required | Default | Detail |
|---|---|---|---|
PASSWORD_MANAGER_DB_PASSWORD | No | generated if blank | Dedicated database password for bundled Password Manager services. |
CT_OPS_INSTANCE_ID | No | generated | Stable CT-Ops instance identifier used by launch assertions. |
PASSWORD_MANAGER_CT_OPS_ISSUER | No | derived from CT-Ops URL | Issuer value for CT-Ops launch assertions. |
PASSWORD_MANAGER_CT_OPS_AUDIENCE | No | ct-password-manager | Audience value expected by the Password Manager API. |
PASSWORD_MANAGER_CT_OPS_PRODUCT | No | ct-password-manager | Product claim for launch assertions. |
PASSWORD_MANAGER_CT_OPS_ED25519_PRIVATE_KEY | No | generated if blank | Base64 PKCS#8 DER Ed25519 private key used to sign launch assertions. Keep secret. |
PASSWORD_MANAGER_CT_OPS_ED25519_PUBLIC_KEY | No | generated with private key | Base64 SPKI DER Ed25519 public key matching the private key. |
PASSWORD_MANAGER_TRUSTED_ORIGINS | No | derived from CT-Ops trusted origins | Trusted browser origins for Password Manager launch flows. |
PASSWORD_MANAGER_SESSION_COOKIE_SECURE | No | true | Keep true for HTTPS deployments. |
Create the first admin account
Open your configured URL, for example https://ct-ops.example.com. If you are
still using the self-signed certificate generated by the installer, your browser
will warn until you replace deploy/tls/server.{crt,key} with a certificate
from your own CA.
- Click Register.
- Create the first user.
- Complete the standalone instance setup flow.
- The first account becomes
super_admin.
First agent
Use the dedicated CT-Ops Agents guide for production agent creation and rollout. It covers enrolment tokens, agent configuration, tags, global agent defaults, Debian and Red Hat based Linux installation, Windows installation, verification, updates, and uninstall behavior.
Build from source
Use this path when developing CT-Ops or validating changes before release.
git clone https://forgejo.carrtech.dev/carrtech/ct-ops.git ct-ops
cd ct-ops
make dev-tls
cp apps/web/.env.example apps/web/.env.local
docker compose -f docker-compose.single.yml up -d
docker compose -f docker-compose.single.yml exec web sh -c "cd /app && node_modules/.bin/drizzle-kit migrate"
make agent
For local agent execution and config details, use the CT-Ops Agents guide.
Operations
From a release-bundle install directory, run ./generate_support_data when
opening a support request. It creates
ct-ops-support-data-<timestamp>.tar.gz with redacted settings, Docker status,
recent logs, host information, file metadata, and TLS certificate fingerprints.
Review the archive before attaching it to a support request.
Stop or remove the stack:
# Stop the stack and keep volumes
docker compose down
# Stop the stack and remove volumes
docker compose down -v
Use down -v only when you intend to remove CT-Ops data from this host.
Troubleshooting
| Symptom | Checks |
|---|---|
| Browser cannot reach CT-Ops | Confirm docker compose ps, nginx health, firewall rules, DNS, and NGINX_HTTPS_PORT. |
| Login or registration redirects fail | Check BETTER_AUTH_URL and BETTER_AUTH_TRUSTED_ORIGINS exactly match the browser origin. Include non-standard ports. |
| Agents cannot download binaries | Check AGENT_DOWNLOAD_BASE_URL from the agent host with curl -vk https://.../api/agent/latest. |
| Agents cannot register or heartbeat | Confirm port 9443 is reachable from the agent host and the ingest container is healthy. |
certificate signed by unknown authority on agents | Use the install bundle or configure the agent ca_cert_file; use --tls-skip-verify only for disposable development. |
| Database password changed but DB rejects it | Update the password inside PostgreSQL as well as .env, or restore the original .env value. |
| Migrations fail | Wait for db to become healthy, then inspect docker compose logs migrate web db. |