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.

ComponentRoleNetwork exposure
nginxHTTPS entrypoint for the browser UI and proxied terminal WebSockets.Public 80 and 443, or your configured NGINX_HTTP_PORT / NGINX_HTTPS_PORT.
webNext.js UI and API.Compose network only.
ingestAgent gRPC, health, JWKS, and terminal WebSocket backend.Public 9443 for agents; HTTP stays internal or proxied.
dbPostgreSQL and TimescaleDB.Compose network only.
solrApache Solr search index for CT-Ops-wide global search.Compose network only.
migrateOne-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

RequirementNotes
Linux host with Docker Engine and Docker Compose v2The release bundle is designed for Docker Compose.
DNS name for the CT-Ops web UIStrongly recommended before enrolling remote agents.
TLS certificate for the web UIThe installer can start with a self-signed certificate, but production should use a certificate from your CA.
Open firewall portsBrowser users need HTTPS. Agents need ingest 9443.
Persistent diskKeep 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_SECRET when it is blank
  • generates POSTGRES_PASSWORD when 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

VariableRequiredDefaultDetail
BETTER_AUTH_URLYeshttps://localhostPublic 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_ORIGINSYeshttps://localhostComma-separated list of origins allowed to complete auth flows. Include every legitimate browser origin.
REQUIRE_EMAIL_VERIFICATIONRecommendedtrueRequires local email/password users to verify their email before sign-in. Set false only when the deployment intentionally allows unverified local users.
BETTER_AUTH_SECRETGenerated if blankblankRandom 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

VariableRequiredDefaultDetail
AGENT_DOWNLOAD_BASE_URLYes for remote agentshttps://localhostPublic HTTPS URL agents use for binary downloads and self-update. It must be reachable from every agent host.
INGEST_WS_URLNoemptyBrowser 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

VariableRequiredDefaultDetail
CT_OPS_TRUST_PROXY_HEADERSNotrue in the release bundleTrusts 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_PORTNo80Host port bound by bundled nginx for HTTP redirect traffic.
NGINX_HTTPS_PORTNo443Host port bound by bundled nginx for HTTPS browser traffic. Include this port in the public URL variables when it is not 443.

Database

VariableRequiredDefaultDetail
POSTGRES_PASSWORDYesgenerated if blankPassword for the bundled PostgreSQL user. If changed after the database is initialized, also change the password inside PostgreSQL.
POSTGRES_USERNoctopsPostgreSQL username used by the compose stack.
POSTGRES_DBNoctopsPostgreSQL database name used by the compose stack.
VariableRequiredDefaultDetail
CT_OPS_SEARCH_BACKENDNosolr in release bundlesSearch backend for the global CT-Ops search modal. Customer bundles use bundled Apache Solr. Local development may use postgres.
SOLR_URLNohttp://solr:8983/solr/ct_ops_searchInternal URL of the bundled Solr core. Do not expose Solr directly to the network.
CT_OPS_SEARCH_REINDEX_INTERVAL_MINUTESNo30How often CT-Ops rebuilds the Solr index in the background. Search requests use the latest completed index and do not trigger reindexing.
SOLR_HEAPNo1024mJVM 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

VariableRequiredDefaultDetail
LICENCE_REVOCATION_URLNoCarrTech revocation endpointSigned 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

VariableRequiredDefaultDetail
ANSIBLE_API_URLNoemptyURL for a separately deployed legacy Ansible API. Leave blank unless that provider is enabled and reachable from CT-Ops.
CT_AUTOMATION_URLNoemptyOptional default URL for a separately deployed CT-Automation server. Most installs should configure this in Settings -> Integrations -> Automation.
INGEST_CT_CVE_INVENTORY_PUSH_TIMEOUTNo5mTimeout for ingest-side CT-CVE inventory snapshot pushes. Increase for large inventories or slower CT-CVE matching.

Load testing

VariableRequiredDefaultDetail
CT_OPS_LOADTEST_ADMIN_KEYNodisabledBearer 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

VariableRequiredDefaultDetail
ENVIRONMENTNoproduction behaviorLeave 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_IMAGENorelease-pinned imageOverride the web image only when deliberately testing or recovering with a different published image.
INGEST_IMAGENorelease-pinned imageOverride the ingest image only when deliberately testing or recovering with a different published image.

Password Manager integration

VariableRequiredDefaultDetail
PASSWORD_MANAGER_DB_PASSWORDNogenerated if blankDedicated database password for bundled Password Manager services.
CT_OPS_INSTANCE_IDNogeneratedStable CT-Ops instance identifier used by launch assertions.
PASSWORD_MANAGER_CT_OPS_ISSUERNoderived from CT-Ops URLIssuer value for CT-Ops launch assertions.
PASSWORD_MANAGER_CT_OPS_AUDIENCENoct-password-managerAudience value expected by the Password Manager API.
PASSWORD_MANAGER_CT_OPS_PRODUCTNoct-password-managerProduct claim for launch assertions.
PASSWORD_MANAGER_CT_OPS_ED25519_PRIVATE_KEYNogenerated if blankBase64 PKCS#8 DER Ed25519 private key used to sign launch assertions. Keep secret.
PASSWORD_MANAGER_CT_OPS_ED25519_PUBLIC_KEYNogenerated with private keyBase64 SPKI DER Ed25519 public key matching the private key.
PASSWORD_MANAGER_TRUSTED_ORIGINSNoderived from CT-Ops trusted originsTrusted browser origins for Password Manager launch flows.
PASSWORD_MANAGER_SESSION_COOKIE_SECURENotrueKeep 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.

  1. Click Register.
  2. Create the first user.
  3. Complete the standalone instance setup flow.
  4. 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

SymptomChecks
Browser cannot reach CT-OpsConfirm docker compose ps, nginx health, firewall rules, DNS, and NGINX_HTTPS_PORT.
Login or registration redirects failCheck BETTER_AUTH_URL and BETTER_AUTH_TRUSTED_ORIGINS exactly match the browser origin. Include non-standard ports.
Agents cannot download binariesCheck AGENT_DOWNLOAD_BASE_URL from the agent host with curl -vk https://.../api/agent/latest.
Agents cannot register or heartbeatConfirm port 9443 is reachable from the agent host and the ingest container is healthy.
certificate signed by unknown authority on agentsUse the install bundle or configure the agent ca_cert_file; use --tls-skip-verify only for disposable development.
Database password changed but DB rejects itUpdate the password inside PostgreSQL as well as .env, or restore the original .env value.
Migrations failWait for db to become healthy, then inspect docker compose logs migrate web db.