CCHPCCHPDocs
↑↓↵pagefind

Chapter 0524 min read

Install from binaries

Run CCHP with systemd on one Linux host: prepare PostgreSQL and Dragonfly, extract the binaries, write the configuration, and start the services, then set up a reverse proxy, PgDog, and upgrades.

This page shows how to run CCHP on one Linux host without containers: you extract the binaries from the release image, manage each service with systemd, and provide PostgreSQL and Dragonfly yourself. Every step requires root privileges on the host.

If you just want CCHP running as quickly as possible, Install with Docker Compose is simpler. To run each role as a separate workload on k3s, see Install on k3s / k8s. For all installation methods, see Installation overview.

All services on this page run on the same host. A multi-host deployment requires the same shared data directory mounted on every host and the host’s private network address set for each service, so we recommend installing on k3s / k8s instead.

Components and ports#

Every CCHP service runs from the same executable, installed as /usr/local/bin/cchp, and the --role flag selects the service. Binary installation uses the following systemd units:

UnitCommand it runsPurpose
cchp-migrate.servicecchp db migrate (one-time)Runs the database migrations, and creates the restricted runtime role cchp_runtime and grants it privileges. The other three services run it first before they start
cchp-control.servicecchp api --role=control --addr=:8080Console and management API
cchp-gateway.servicecchp api --role=gateway --addr=:8081LLM gateway. Clients send their requests here
cchp-worker.servicecchp api --role=workerBackground jobs: outbox delivery, billing settlement, and scheduled tasks
cchp-pgdog.service (optional)pgdogPgDog connection pool. See “Use the PgDog connection pool (optional)”

The ports that each service listens on:

PortServiceListen addressPurpose
8080controlAll interfacesConsole and management API, placed behind the reverse proxy
8081gatewayAll interfacesLLM gateway, placed behind the reverse proxy
8082worker127.0.0.1/metrics and /readyz
8084gateway127.0.0.1/metrics
8085control127.0.0.1/metrics
8091gatewayAll interfacesInternal port: request-archive reads by the control plane and signed profiling requests
8092controlAll interfacesInternal port: signed profiling requests
8093workerAll interfacesInternal port: signed profiling requests
6432PgDog (optional)127.0.0.1Connection pool

Warning: 8080, 8081, and the internal ports 8091, 8092, and 8093 listen on all interfaces. Restrict access with the host firewall: open only the reverse proxy’s ports to the internet, and never expose the internal ports.

What you need#

ItemRequirement
Operating systemLinux with systemd, on amd64 or arm64. The binaries are statically linked and do not depend on the distribution
Permissionsroot: to install files and write to /etc/cchp and /etc/systemd/system
DockerAny machine that can pull images from ghcr.io. It is used only in step 3 and during upgrades, to extract the binaries
FFmpegRequired on the host that runs cchp-gateway.service, to provide ffmpeg and ffprobe. Without it, the gateway refuses to start
PostgreSQLVersion 18, with the TimescaleDB extension recommended (Docker Compose deployments use 2.31.0). You also need the extensions btree_gist and pg_stat_statements, which ship with PostgreSQL
DragonflyRequired in production mode. CCHP connects to it over the Redis protocol. Docker Compose deployments use Dragonfly v2.0.2
DiskAt least 26 GiB for CCHP’s local data on the file systems that hold /var/lib and /var/cache, plus the space for the system and the database
Toolsopenssl, to generate secrets and passwords

For the CPU, memory, and disk requirements of the whole machine, see Installation overview.

When it starts, each service adds up its own space caps on each file system and refuses to start if the total exceeds the file system’s capacity. At the default caps, the gateway’s node data takes about 14.9 GiB and its temporary directory about 8.3 GiB on a 4-core host, and the temporary directories of the control plane and the worker take 1 GiB each, for a total of about 25.2 GiB. That is why you reserve 26 GiB. Importing data from CC Hub needs extra space. See “CC Hub import components (optional)”.

Step 1: Create a system user and install FFmpeg#

All CCHP services run as the system user cchp:

bash
useradd --system --home-dir /var/lib/cchp --shell /usr/sbin/nologin cchp

Install FFmpeg on the host that runs the gateway. On Debian and Ubuntu, use the system package repositories. On RHEL-based distributions, install ffmpeg from RPM Fusion:

bash
apt install ffmpeg
ffprobe -version

Result: ffprobe -version prints version information.

Step 2: Prepare PostgreSQL and Dragonfly#

Install PostgreSQL#

Install PostgreSQL 18 and TimescaleDB by following the official PostgreSQL and TimescaleDB documentation. Then preload two libraries in postgresql.conf, and restart PostgreSQL for the change to take effect:

postgresql.confini
shared_preload_libraries = 'timescaledb,pg_stat_statements'

If you did not install TimescaleDB, write only 'pg_stat_statements'. pg_stat_statements is used only for the Top statements view in System Management → Observability → Database, and CCHP runs without it.

Create the database and the owner role#

CCHP uses two database roles:

RolePurposeHow it is created
hub (owner)Owns the app database and all of its tables, and runs the migrations. Its connection string is written only in /etc/cchp/cchp-migrate.env, and the long-running services do not hold itYou create it in this step
cchp_runtimeThe restricted login that the three services use to connect to the database. It has none of the superuser, BYPASSRLS, create-database, or create-role attributes, and gets only the table privileges it needsCreated automatically by the migration

Create the owner role and the database as the PostgreSQL superuser. When the owner is a superuser, the migration completes the rest of the preparation automatically: it creates the extensions, creates cchp_runtime and grants it privileges, and grants the pg_read_all_settings membership:

bash
OWNER_PASS="$(openssl rand -hex 32)"
sudo -u postgres psql -v ON_ERROR_STOP=1 -v owner_pass="$OWNER_PASS" <<'SQL'
CREATE ROLE hub LOGIN SUPERUSER PASSWORD :'owner_pass';
CREATE DATABASE app OWNER hub;
SQL
OWNER_URL="postgres://hub:${OWNER_PASS}@127.0.0.1:5432/app?sslmode=disable"

Run steps 2 through 4 in the same root shell, because step 4 writes OWNER_URL into the configuration file. If PostgreSQL runs on another host, replace 127.0.0.1:5432 in the connection string with its address, and usually switch to sslmode=require.

Result: sudo -u postgres psql -Atc '\l app' shows the app database with hub as its owner.

If you can’t use a superuser#

If you can’t give the owner superuser privileges, for example on a managed database, the owner must have both BYPASSRLS and CREATEROLE, and must own the app database. Only a superuser can grant BYPASSRLS. A superuser must also create the extensions in the app database in advance:

sql
CREATE ROLE hub LOGIN CREATEROLE BYPASSRLS PASSWORD '<owner password>';
CREATE DATABASE app OWNER hub;
\c app
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

In this case, the migration can’t grant pg_read_all_settings. After the first migration in step 5 creates cchp_runtime, run the following statement once as a superuser. If you don’t run it, CCHP runs as usual, and only Top statements is unavailable:

sql
GRANT pg_read_all_settings TO cchp_runtime WITH ADMIN FALSE, INHERIT TRUE, SET FALSE;

Warning: The grant must include WITH ADMIN FALSE, INHERIT TRUE, SET FALSE. If the runtime role holds the membership with other options, or has joined other roles, CCHP refuses to start, and the error message gives the REVOKE and GRANT statements to run.

Prepare Dragonfly#

Install and start Dragonfly by following the official Dragonfly documentation. In production mode, CCHP must connect to Dragonfly and refuses to start if the connection fails.

  • The default connection URL is redis://127.0.0.1:6379/0. If your address is different, change DRAGONFLY_URL in step 4.
  • If you set a password, put it in the connection string, for example redis://:<password>@127.0.0.1:6379/0.
  • Do not expose the Dragonfly port to the internet: listen only on a local address, or set a password and restrict access with a firewall.

Step 3: Extract the binaries#

The CCHP binary, the migration tool, Atlas, and the migration files are all in the release image ghcr.io/cch-hq/cch-plus. The following commands use Docker on the installation host to extract them:

bash
docker pull ghcr.io/cch-hq/cch-plus:latest
id=$(docker create ghcr.io/cch-hq/cch-plus:latest)
install -d /usr/local/libexec/cchp /usr/local/share/cchp
docker cp "$id":/usr/local/bin/gateway /usr/local/bin/cchp
docker cp "$id":/usr/local/bin/migrate /usr/local/libexec/cchp/migrate
docker cp "$id":/usr/local/bin/atlas /usr/local/libexec/cchp/atlas
docker cp "$id":/migrations /usr/local/share/cchp/migrations
docker rm "$id"

latest is the latest stable release. To install a specific version, replace both occurrences of latest with the version number, for example 1.0.11.

If the installation host has no Docker, run the same copy commands on another machine that has Docker, and then copy the four items to the same paths on the installation host. If the two machines have different architectures, add --platform linux/amd64 or --platform linux/arm64 to both docker pull and docker create.

Path in the imageInstalled atContents
/usr/local/bin/gateway/usr/local/bin/cchpCCHP main program, shared by all services
/usr/local/bin/migrate/usr/local/libexec/cchp/migrateMigration tool, called only by cchp db migrate
/usr/local/bin/atlas/usr/local/libexec/cchp/atlasAtlas CLI at the version pinned by the image
/migrations/usr/local/share/cchp/migrationsMigration files

Result: /usr/local/bin/cchp help prints the list of subcommands.

Step 4: Write the configuration files#

All services share the environment file /etc/cchp/cchp.env. The owner credentials that the migration uses are kept in a separate file, /etc/cchp/cchp-migrate.env, which only cchp-migrate.service reads.

First, create a working directory to keep all of the following files in:

bash
install -d -m 0700 /root/cchp-install
cd /root/cchp-install

Create the following two files in the working directory:

cchp.envini
APP_ENV=production
 
# Migration tool and migration files installed in step 3
ATLAS_BIN=/usr/local/libexec/cchp/atlas
CCH_MIGRATE_BIN=/usr/local/libexec/cchp/migrate
CCH_MIGRATIONS_DIR=/usr/local/share/cchp/migrations
 
# DATABASE_URL is written by the install commands below
DATABASE_URL=
CCHP_DATABASE_RUNTIME_ROLE=cchp_runtime
DRAGONFLY_URL=redis://127.0.0.1:6379/0
 
# Secrets, written by the install commands below
CCH_TOKEN_PEPPER=
CCH_MASTER_KEY=
DRAGONFLY_KEY_SECRET=
 
# PostgreSQL data directory, used to read the database's disk capacity
CCHP_DATABASE_STORAGE_SOURCE=filesystem
CCHP_DATABASE_STORAGE_PATH=/var/lib/postgresql/18/main
 
# Public URL. Change it to the HTTPS URL after you set up HTTPS
CCH_PUBLIC_BASE_URL=http://localhost:8080
# Same as CCH_PUBLIC_BASE_URL
CCH_CONTROL_PLANE_ALLOWED_ORIGINS=http://localhost:8080
CCH_GATEWAY_PUBLIC_URL=http://localhost:8081
 
# Audit logs are stored only in PostgreSQL
CCH_SIEM_MODE=disabled
 
# For LAN testing only; see "Reduced-security deployment (LAN testing)"
CCH_INSECURE_DEPLOYMENT=false

CCHP_DATABASE_STORAGE_PATH is the PostgreSQL data directory. /var/lib/postgresql/18/main is the default on Debian and Ubuntu. On other systems, look it up with sudo -u postgres psql -Atc 'SHOW data_directory' and replace the value. For what each variable means, see “Configuration reference”.

Then install the two files, and write the database connection strings and the secrets:

bash
: "${OWNER_URL:?OWNER_URL is not set, see step 2}"
install -d -m 0750 -g cchp /etc/cchp
install -m 0640 -g cchp cchp.env /etc/cchp/cchp.env
install -m 0600 cchp-migrate.env /etc/cchp/cchp-migrate.env
sed -i -E '/^(DATABASE_URL|CCH_TOKEN_PEPPER|CCH_MASTER_KEY|DRAGONFLY_KEY_SECRET)=/d' /etc/cchp/cchp.env
sed -i -E '/^(ATLAS_DATABASE_URL|CCHP_DATABASE_RUNTIME_PASSWORD)=/d' /etc/cchp/cchp-migrate.env
RTPASS="$(openssl rand -hex 32)"
printf 'DATABASE_URL=postgres://cchp_runtime:%s@127.0.0.1:5432/app?sslmode=disable\n' "$RTPASS" >> /etc/cchp/cchp.env
printf '%s=%s\n' ATLAS_DATABASE_URL "$OWNER_URL" CCHP_DATABASE_RUNTIME_PASSWORD "$RTPASS" >> /etc/cchp/cchp-migrate.env
for key in CCH_TOKEN_PEPPER CCH_MASTER_KEY DRAGONFLY_KEY_SECRET; do
  printf '%s=%s\n' "$key" "$(openssl rand -hex 32)" >> /etc/cchp/cchp.env
done

The two sed commands delete the empty placeholder lines in the two files, and the commands after them write the generated values. If PostgreSQL is not on this host, also change the address and sslmode in DATABASE_URL.

Warning: Run the commands above only once, at the first installation. CCH_MASTER_KEY decrypts the saved provider keys. If you generate a new key after the first start, the saved provider keys can no longer be decrypted. Back up the two files under /etc/cchp/ to a safe location.

Check the names of the variables that have values (the values are not shown):

bash
grep -oE '^(DATABASE_URL|CCH_TOKEN_PEPPER|CCH_MASTER_KEY|DRAGONFLY_KEY_SECRET)=.' /etc/cchp/cchp.env | cut -d= -f1
grep -oE '^(ATLAS_DATABASE_URL|CCHP_DATABASE_RUNTIME_PASSWORD)=.' /etc/cchp/cchp-migrate.env | cut -d= -f1

Result: The first command prints DATABASE_URL and the names of the three secrets, one per line. The second prints ATLAS_DATABASE_URL and CCHP_DATABASE_RUNTIME_PASSWORD.

Step 5: Install the systemd units and start the services#

Save the following four unit files in the working directory:

cchp-migrate.serviceini
[Unit]
Description=CCHP database migration (schema, data reconciliation, runtime role grants, row level security)
After=network-online.target postgresql.service cchp-pgdog.service
Wants=network-online.target
 
[Service]
Type=oneshot
RemainAfterExit=yes
User=cchp
Group=cchp
EnvironmentFile=/etc/cchp/cchp.env
EnvironmentFile=/etc/cchp/cchp-migrate.env
Environment=CCH_SCRATCH_DIR=/var/cache/cchp/migrate
CacheDirectory=cchp/migrate
CacheDirectoryMode=0700
ExecStart=/usr/local/bin/cchp db migrate
TimeoutStartSec=744min
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

Install the units, and enable and start the three services:

bash
install -m 0644 cchp-migrate.service cchp-control.service cchp-gateway.service cchp-worker.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now cchp-control cchp-gateway cchp-worker

The dependencies in the units determine the start order:

  1. All three services declare Requires= and After= cchp-migrate.service, so systemd runs the migration first. systemctl returns only after the migration finishes.
  2. After the migration succeeds, cchp-migrate.service stays in the active (exited) state, and the three services start next. If the migration fails, none of the three services start.
  3. cchp-migrate.service has no [Install] section and does not need to be enabled separately. At every boot, it runs first, along with the three services.
  4. The migration can run for up to 744 minutes (TimeoutStartSec), which covers the time budget of all migration phases.

Result: The command returns without errors.

Step 6: Check that the services are ready#

bash
systemctl status cchp-migrate cchp-control cchp-gateway cchp-worker --no-pager
journalctl -u cchp-migrate --no-pager | grep 'database observability'
curl -fsS http://127.0.0.1:8080/readyz
curl -fsS http://127.0.0.1:8081/readyz
curl -fsS http://127.0.0.1:8082/readyz

Result: cchp-migrate shows active (exited), and the other three show active (running). All three /readyz requests succeed.

The database observability line in the migration log reports the two prerequisites for Top statements, for example database observability: pg_read_all_settings membership=granted, pg_stat_statements extension=created. What each value means:

ValueMeaning
presentAlready met before the migration
grantedThe migration granted the membership
correctedThe migration revoked a membership with the wrong options and granted it again
createdThe migration created the extension
not_permittedThe migration account lacks the privileges. A superuser needs to run the SQL that the setup wizard shows
unavailablepg_stat_statements is not installed on the server

Step 7: Complete the setup wizard#

  1. Get the one-time setup token from the control plane log:

    bash
    journalctl -u cchp-control | grep 'CCHP setup token'

    If you can’t find the token in the log, see “The setup token is not in the log” under “Frequently asked questions”.

  2. Open http://localhost:8080. You can sign in only on an HTTPS URL or a loopback address, so if you connect from another computer, set up SSH port forwarding first, and then open http://localhost:8080 in your local browser:

    bash
    ssh -L 8080:127.0.0.1:8080 -L 8081:127.0.0.1:8081 root@cchp.example.com

    To open the console directly by its domain, complete “Set up a reverse proxy and HTTPS” first.

  3. On the Enter the setup token page, paste the token into Setup token, and click Verify and continue.

  4. Complete the seven steps in order: Site, Administrator, Two-step, Email, Sign-in, Database, and Organization. You can skip Two-step and Email. For how to fill in each step, see Step 6: Enter the setup token and Step 7: Complete the setup wizard in Install with Docker Compose. The steps are the same for all three installation methods.

  5. In the Database step (the page title is Check database observability): if the owner is a superuser and pg_stat_statements is preloaded, the check passes right away. Otherwise, the page shows the reason and the SQL that a superuser needs to run. You can also click Skip, handle it later in System Management.

  6. In the Organization step, fill in Organization name and Team name, choose Prepaid or Postpaid under Organization type, and click Create and continue. Creating the first organization completes the installation.

Result: The browser opens the console. There are no providers yet, so the gateway returns a no-route error for proxied requests. Later sections show how to create providers and LLM keys.

Configuration reference#

cchp.env#

VariableDescription
APP_ENVKeep it as production. In this mode, a service refuses to start if a required variable is missing
ATLAS_BIN, CCH_MIGRATE_BIN, CCH_MIGRATIONS_DIRPaths to Atlas, the migration tool, and the migration files installed in step 3
DATABASE_URLConnection string of the runtime role, written in step 4. With PgDog, it points to 127.0.0.1:6432
CCHP_DATABASE_RUNTIME_ROLEName of the runtime role. It must match the user in DATABASE_URL and can’t be the owner role
DRAGONFLY_URLDragonfly connection string. CCHP refuses to start if the connection fails
CCH_TOKEN_PEPPERSecret used to compute the hashes of tokens such as API keys. If you change it, tokens that were already issued fail verification
CCH_MASTER_KEY64-hex-digit (32-byte) master key that encrypts the saved provider keys. Do not change it after the first start
DRAGONFLY_KEY_SECRETComputes HMAC digests of the sensitive content in key names written to Dragonfly, and also signs internal requests between gateway nodes
CCHP_DATABASE_STORAGE_SOURCE, CCHP_DATABASE_STORAGE_PATHThe daily storage retention job and the hourly storage check read the capacity of the file system that holds the PostgreSQL data directory, without accessing the files in it. If PostgreSQL runs on another host, change the source to prometheus, and set CCHP_DATABASE_STORAGE_PROMETHEUS_URL, CCHP_DATABASE_STORAGE_CAPACITY_QUERY, and CCHP_DATABASE_STORAGE_AVAILABLE_QUERY. Each of the two queries returns a number of bytes
CCH_PUBLIC_BASE_URLThe URL where you open the console. Sign-in cookies, cross-site request checks, passkeys, third-party sign-in callbacks, and email links all use it. It must be an HTTPS URL or a loopback address (e.g., http://localhost:8080), and it can’t contain a path. With CCH_INSECURE_DEPLOYMENT turned on, it can be an http URL on the LAN
CCH_CONTROL_PLANE_ALLOWED_ORIGINSOrigins that are allowed to call the gateway API from a browser. Set it to the same URL as CCH_PUBLIC_BASE_URL. CCHP refuses to start if it is empty
CCH_GATEWAY_PUBLIC_URLThe gateway URL that the console shows to users
CCH_SIEM_MODEdisabled means that audit logs are stored only in PostgreSQL and no remote copy is sent. Do not delete this line: if it is empty, records in the audit outbox stay pending forever
CCH_INSECURE_DEPLOYMENTFor LAN testing only. true turns off every requirement on HTTPS, domains, and private addresses. The default is false; any other value makes the service exit at startup. See Reduced-security deployment (LAN testing)

cchp-migrate.env#

VariableDescription
ATLAS_DATABASE_URLConnection string of the owner role. It must connect directly to PostgreSQL, not through PgDog, because the migration needs session-level advisory locks and runs DDL. If the owner is not a superuser and does not have BYPASSRLS, the migration refuses to run
CCHP_DATABASE_RUNTIME_PASSWORDPassword of the runtime role. Every migration creates the runtime role or resets its password, so the owner needs CREATEROLE. If you change it, also change the password in DATABASE_URL in cchp.env

Variables in the unit files#

The following variables are set in each unit’s Environment= lines, because each service has its own values. Do not put them in cchp.env: values in cchp.env override Environment= in the units. To change them, add an override with systemctl edit <unit-name>.

VariableUnitDescription
CCH_SCRATCH_DIREvery unitEach service’s private temporary directory, /var/cache/cchp/<unit>, which is cleared at every start
CCH_ARCHIVE_INTERNAL_URLgatewayThe advertised address of the gateway’s internal port 8091. In production mode, the gateway refuses to start without it
CCH_NODE_INTERNAL_URLcontrol, workerThe advertised addresses of the internal ports 8092 and 8093 of the control plane and the worker. In production mode, they refuse to start without it
CCH_REPLAY_OWNER_PROXY_NODESgatewayGateway node table, where %H is the host name. Required in production mode
CCH_ALLOW_EMPTY_ROUTING_BOOTSTRAPgatewayLets the gateway start before any providers exist. Until you add a provider, proxied requests return a no-route error
PATHworkerAdds the directory of the CC Hub import components before the system paths

Files and directories#

PathContentsPermissions
/usr/local/bin/cchpCCHP main programInstalled in step 3
/usr/local/libexec/cchp/The migration tools migrate and atlas; the optional CC Hub import components dump/Installed in step 3; for dump/, see “CC Hub import components (optional)“
/usr/local/share/cchp/migrations/Migration filesInstalled in step 3
/etc/cchp/cchp.envEnvironment file shared by all services0640, group cchp
/etc/cchp/cchp-migrate.envMigration credentials, read only by cchp-migrate.service0600, owned by root
/var/lib/cchp/data/Shared data: plugin packages, export files, audit archives, CC Hub uploads, and job result filesCreated by systemd StateDirectory, 0700
/var/lib/cchp/node/Gateway node data: request archives, price snapshots, and job staging. It must be on a local disk, because SQLite WAL can’t be used on NFSCreated by systemd StateDirectory, 0700
/var/cache/cchp/<unit>/Each service’s temporary directoryCreated by systemd CacheDirectory, 0700

Set up a reverse proxy and HTTPS#

To open the console from another computer, you must use HTTPS. Put the console and the gateway behind TLS reverse proxies on two separate domains, for example cchp.example.com for the console and api.cchp.example.com for the gateway.

In binary installation, the control plane listens on local port 8080 and the gateway on 8081, the same as in a Docker Compose deployment, so the reverse proxy configuration is also the same: set up Caddy or Nginx by following Step 5: Set up a reverse proxy and HTTPS in Install with Docker Compose.

Then change the three public URLs to HTTPS URLs, and restart the three services:

/etc/cchp/cchp.envini
CCH_PUBLIC_BASE_URL=https://cchp.example.com
CCH_CONTROL_PLANE_ALLOWED_ORIGINS=https://cchp.example.com
CCH_GATEWAY_PUBLIC_URL=https://api.cchp.example.com
bash
systemctl restart cchp-control cchp-gateway cchp-worker

Result: Opening https://cchp.example.com in a browser shows the sign-in page, and the gateway URL that users see in the console is https://api.cchp.example.com.

Behind a reverse proxy, the client IP in request logs and audit records is the reverse proxy’s address by default. To identify the real source IP, see Every client IP in request logs is the same address in the Install with Docker Compose FAQ.

Use the PgDog connection pool (optional)#

PgDog is a connection pool (in transaction mode) that runs between CCHP and PostgreSQL. Each PostgreSQL backend connection caches the partitions and indexes it has accessed, so the more connections there are, the more memory they use. PgDog pools the connections of the three services, with at most 24 server connections to PostgreSQL. A single-host installation also works fine without PgDog.

Run all of the following commands in the working directory /root/cchp-install.

  1. Download the PgDog v0.1.60 Linux binary, install it as /usr/local/bin/pgdog, and create a system user to run it:

    bash
    pkg="pgdog-v0.1.60-$(uname -m)-unknown-linux-gnu"
    curl -fsSLO "https://github.com/pgdogdev/pgdog/releases/download/v0.1.60/${pkg}.tar.gz"
    tar xzf "${pkg}.tar.gz"
    install -m 0755 "${pkg}/pgdog" /usr/local/bin/pgdog
    useradd --system --shell /usr/sbin/nologin pgdog
    install -d /etc/pgdog
  2. Save the PgDog configuration as /etc/pgdog/pgdog.toml. It listens only on a local address:

    /etc/pgdog/pgdog.tomltoml
    [general]
    host = "127.0.0.1"
    # Number of server connections to PostgreSQL
    default_pool_size = 24
    min_pool_size = 2
    prepared_statements_limit = 4096
    # Replace server connections about every 5 minutes to free PostgreSQL's caches
    server_lifetime = 300000
    server_lifetime_jitter = 60000
     
    [[databases]]
    name = "app"
    host = "127.0.0.1"

    default_pool_size must be smaller than PostgreSQL’s max_connections minus the direct sessions (LISTEN, scheduled-task locks, and migrations) and the connections of other clients.

  3. Save the PgDog users file as /etc/pgdog/users.toml. Leave CHANGE_ME as it is. The next command replaces it with the runtime role’s password:

    /etc/pgdog/users.tomltoml
    # Must match the user, database name, and password in DATABASE_URL
    [[users]]
    name = "cchp_runtime"
    database = "app"
    password = "CHANGE_ME"
    bash
    chown root:pgdog /etc/pgdog/users.toml
    chmod 0640 /etc/pgdog/users.toml
    RTPASS="$(sed -n 's/^CCHP_DATABASE_RUNTIME_PASSWORD=//p' /etc/cchp/cchp-migrate.env)"
    sed -i "s/CHANGE_ME/${RTPASS}/" /etc/pgdog/users.toml
  4. Save the unit file as /etc/systemd/system/cchp-pgdog.service, and then enable and start it:

    /etc/systemd/system/cchp-pgdog.serviceini
    [Unit]
    Description=PgDog connection pooler for CCHP
    After=network-online.target postgresql.service
    Wants=network-online.target
    Before=cchp-control.service cchp-gateway.service cchp-worker.service
     
    [Service]
    User=pgdog
    Group=pgdog
    ExecStart=/usr/local/bin/pgdog --config /etc/pgdog/pgdog.toml --users /etc/pgdog/users.toml
    Restart=on-failure
    RestartSec=2
    # After SIGINT, PgDog waits up to 60 seconds for in-flight transactions to finish
    KillSignal=SIGINT
    TimeoutStopSec=65
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    PrivateTmp=true
     
    [Install]
    WantedBy=multi-user.target
    bash
    systemctl daemon-reload
    systemctl enable --now cchp-pgdog

    The units of the three services do not declare a dependency on cchp-pgdog.service, so you must enable it separately.

  5. Save the original direct connection URL as DATABASE_DIRECT_URL, change DATABASE_URL to PgDog’s port 6432, and then restart the three services. DATABASE_DIRECT_URL is used for LISTEN and for the session locks of scheduled tasks, which can’t go through a transaction-mode connection pool:

    bash
    grep '^DATABASE_URL=' /etc/cchp/cchp.env | sed 's/^DATABASE_URL=/DATABASE_DIRECT_URL=/' >> /etc/cchp/cchp.env
    sed -i '/^DATABASE_URL=/s|@127.0.0.1:5432/|@127.0.0.1:6432/|' /etc/cchp/cchp.env
    systemctl restart cchp-control cchp-gateway cchp-worker

The migration always connects directly to PostgreSQL with ATLAS_DATABASE_URL from cchp-migrate.env, and never goes through PgDog.

Result: systemctl status cchp-pgdog shows active (running), and /readyz succeeds for all three services.

CC Hub import components (optional)#

To import CC Hub data in the console, the worker needs the dump decoder. If you don’t use CC Hub import, skip this section.

The decoding script is built into the CCHP binary, so you only need to install the Python environment that it uses. Before you install, make sure that:

  • python3 is version 3.11 or later (check with python3 --version) and has the venv module. On Debian and Ubuntu, install the module with apt install python3-venv.
  • The host can reach PyPI.

Install the environment to /usr/local/libexec/cchp/dump, and check that the dependencies can be imported:

bash
python3 -m venv /usr/local/libexec/cchp/dump
/usr/local/libexec/cchp/dump/bin/python3 -m pip install \
  pendulum==3.2.0 pgdumplib==4.0.0 pglast==8.5 python-dateutil==2.9.0.post0 \
  six==1.17.0 toposort==1.10 tzdata==2026.5
ln -sfn python3 /usr/local/libexec/cchp/dump/bin/cchub-dump-python
/usr/local/libexec/cchp/dump/bin/cchub-dump-python -c 'import pgdumplib, pglast'

Result: The last command prints nothing and reports no errors.

cchp-worker.service already adds /usr/local/libexec/cchp/dump/bin to PATH, and the worker finds cchub-dump-python there, so you don’t need to restart the worker after the installation. This directory is a Python virtual environment and can’t be moved to another location. After you upgrade the host’s Python, delete this directory and run the commands above again.

Importing also needs enough disk space:

  • When the control plane receives an upload, the file system that holds /var/cache/cchp/control needs as much free space as the size of the uploaded file.
  • When the worker processes an export file of size S with N usage records, the file system that holds /var/cache/cchp/worker needs about S + N × 2.5 KiB of free space. For example, a 50 GB pg_dump with 96 million rows needs about 300 GB.

Backup#

WhatLocationRecommendation
Configuration and secrets/etc/cchp/cchp.env, /etc/cchp/cchp-migrate.envMust be backed up. The master encryption key and the token hashing secret are stored only here
DatabaseThe PostgreSQL app databaseMust be backed up. Export it with pg_dump
Node data such as request archives/var/lib/cchp/node/Back it up separately from the database
Shared data/var/lib/cchp/data/Plugin packages, export files, audit archives, CC Hub uploads, and job result files

Export the database:

bash
sudo -u postgres pg_dump -Fc -d app > "/root/cchp-$(date +%F).dump"

Result: A backup file named cchp- plus the date is created under /root.

Before you back up the node data, stop the gateway so that you don’t copy half-written files:

bash
systemctl stop cchp-gateway
tar czf "/root/cchp-node-$(date +%F).tar.gz" -C /var/lib/cchp/node .
systemctl start cchp-gateway

For what to watch for when you restore the database, see Backup and restore in Install with Docker Compose.

Upgrade#

Migrations run only forward and can’t be rolled back. Before you upgrade, back up the database and the two files under /etc/cchp/ as described in “Backup”.

bash
docker pull ghcr.io/cch-hq/cch-plus:latest
id=$(docker create ghcr.io/cch-hq/cch-plus:latest)
 
sudo -u postgres pg_dump -Fc -d app > "/root/cchp-$(date +%F).dump"
 
systemctl stop cchp-control cchp-gateway cchp-worker cchp-migrate
 
rm -rf /usr/local/share/cchp/migrations
docker cp "$id":/usr/local/bin/gateway /usr/local/bin/cchp
docker cp "$id":/usr/local/bin/migrate /usr/local/libexec/cchp/migrate
docker cp "$id":/usr/local/bin/atlas /usr/local/libexec/cchp/atlas
docker cp "$id":/migrations /usr/local/share/cchp/migrations
docker rm "$id"
 
systemctl start cchp-control cchp-gateway cchp-worker

docker pull gets the latest stable release. To upgrade to a specific version, replace both occurrences of latest with the version number, for example 1.0.11. What each command does:

  1. Pull the new image and create the container first, to shorten the downtime. If you run only docker create, it uses the old latest image that is already on the host.
  2. Stop cchp-migrate along with the services, so that the migration runs again at the next start. Stopping the gateway waits for in-flight requests to finish, for up to 930 seconds (about 15 minutes).
  3. Delete the old directory before you copy the migration files. If the target directory already exists, docker cp copies the new directory into the old one instead of replacing it.
  4. When you start the three services, systemd runs the migration first. When you upgrade across several versions, one migration applies all the missing versions in order. If the migration is slow, systemctl start keeps waiting. You can watch the progress in another terminal with journalctl -fu cchp-migrate.

If the unit files or cchp.env change in the new version, update these files according to this page, and run systemctl daemon-reload before you start the services. If you installed the CC Hub import components, delete /usr/local/libexec/cchp/dump and run their installation commands on this page again.

Result: Current version on System Management → About shows the new version number, and /readyz succeeds for all three services.

To go back to an older version, first restore the database from the backup you made before the upgrade. Then replace latest in the commands above with the older version number, for example 1.0.11, swap the four items back with the same steps, and start the services. Migrations run only forward, so swapping the files back alone can’t undo the changes that the new version made to the database.

View logs#

Each service writes its logs to standard output and standard error, one JSON object per line, and journald collects them:

bash
journalctl -u cchp-migrate -b
journalctl -u cchp-control -f
journalctl -u cchp-gateway --since "1 hour ago"
journalctl -u cchp-worker -f
journalctl -u cchp-pgdog -f

Frequently asked questions#

A service fails to start, and the log says the public URL is invalid#

If CCH_PUBLIC_BASE_URL is neither an HTTPS URL nor a loopback address (for example http://203.0.113.10:8080), the error in the log contains invitation public base URL is invalid. If the URL contains a path, the error is public origin must not contain a path. Change it to http://localhost:8080 and connect through SSH port forwarding, or set up the reverse proxy first and change it to an HTTPS URL without a path. Then run systemctl restart cchp-control cchp-gateway cchp-worker.

The gateway fails to start, and the log shows ffprobe is not on PATH#

FFmpeg is not installed on the host. Install it as described in step 1, and then run systemctl restart cchp-gateway.

The migration fails, and the log shows must be SUPERUSER or BYPASSRLS#

The owner role in ATLAS_DATABASE_URL is not a superuser and does not have BYPASSRLS. Run ALTER ROLE hub BYPASSRLS; as a superuser, and then run systemctl start cchp-control cchp-gateway cchp-worker to run the migration again and start the services.

The services refuse to start with holds role memberships that cchp does not accept#

The runtime role cchp_runtime has joined other roles, or the grant options of pg_read_all_settings are wrong, for example because GRANT pg_read_all_settings TO cchp_runtime; was run without options. Run the REVOKE statements from the error message as a superuser, and then grant the membership again as written in step 2.

After an upgrade, /readyz returns 503 and lists missing_migrations#

The new version’s migrations have not run yet. A common cause is restarting only the three services: cchp-migrate is still in the active (exited) state, and restarting the services does not run it again. Run systemctl restart cchp-migrate. The three services declare Requires=cchp-migrate.service, so they restart along with it and start after the migration finishes.

None of the three services start after a reboot#

cchp-migrate.service is ordered only after postgresql.service, and it does not retry automatically after a failure. If PostgreSQL runs some other way or on another host, the database may not be ready yet at boot, and when the migration fails, none of the three services start. Check the cause with journalctl -u cchp-migrate -b, and after the database is ready, run systemctl start cchp-control cchp-gateway cchp-worker.

Startup fails, and the log shows storage_volume_too_small#

The sum of the space caps of the subdirectories exceeds the file system’s capacity. The error message lists the volume, the capacity, each cap, and the shortfall. Enlarge the file systems that hold /var/lib and /var/cache, and then restart the affected services.

The setup wizard shows “You opened a different address”#

The address open in your browser is different from CCH_PUBLIC_BASE_URL. Open exactly the same address as CCH_PUBLIC_BASE_URL, or change CCH_PUBLIC_BASE_URL and run systemctl restart cchp-control.

The setup token is not in the log#

Print the current token as the service user, with the service’s environment variables:

bash
sudo -u cchp sh -c 'set -a; . /etc/cchp/cchp.env; exec cchp identity setup-token'

If the token has already been used but the installation is not complete, this command generates a new token. Running systemctl restart cchp-control also prints a new token in the log. If an administrator has already been created, the command fails with installation already has users. In that case, sign in directly.