> ## Documentation Index
> Fetch the complete documentation index at: https://www.rootset.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Towbar

> Deploy Rootset through your existing Towbar installation.

Use your existing Towbar installation to run the Rootset API, worker, UI and a managed PostgreSQL database. You also need an S3 source and SMTP service as described in [Configuration](/self-hosting/configuration).

## Deployment files

Copy the YML files listed into your deployment repository in the same folder structure and update them as required based on the instructions below. Include the PostgreSQL initialization script when using the bundled datastore.

<Tree>
  <Tree.File name="towbar.yml" />

  <Tree.Folder name=".towbar" defaultOpen>
    <Tree.Folder name="config" defaultOpen>
      <Tree.File name="postgres-init.sh" />
    </Tree.Folder>

    <Tree.Folder name="datastores" defaultOpen>
      <Tree.File name="rootset-postgres.datastore.yml" />
    </Tree.Folder>

    <Tree.Folder name="services" defaultOpen>
      <Tree.File name="rootset-api.service.yml" />

      <Tree.File name="rootset-worker.service.yml" />

      <Tree.File name="rootset-web.service.yml" />
    </Tree.Folder>
  </Tree.Folder>
</Tree>

Replace the IP address, TLS mode and domains as required. Add the required environment variables to runtime secrets based on your installation. The example includes commented-out backup variables; refer to [Configuration](/self-hosting/configuration) for the settings supported by your Rootset version.

<CodeGroup>
  ```yaml towbar.yml theme={"system"}
  # yaml-language-server: $schema=https://towbar.dev/schemas/repository.v2.json
  version: 2
  buildServer: null
  environments:
    production: {}
  ```

  ```yaml .towbar/services/rootset-api.service.yml theme={"system"}
  # yaml-language-server: $schema=https://towbar.dev/schemas/app.v2.json
  id: rootset-api
  name: Rootset API
  deployment:
    type: image
    image: ghcr.io/avgeek-oss/rootset-api:0.2.3
  autoDeploy: false
  rollout:
    type: recreate
    maintenanceMode: true
    reason: Keep one instance per service during an upgrade.
  container:
    network: rootset
    port: 4310
    resources:
      cpus: 0.5
      memory: 1g
    entrypoint: /bin/sh
    command:
      - -ec
      - |
        export ROOTSET_HOST=0.0.0.0
        export ROOTSET_ARTIFACT_TEMP_DIR=/var/lib/rootset/artifact-spool
        export NODE_OPTIONS="--max-old-space-size=768 --max-http-header-size=196608"
        exec node dist/api.mjs
    volumes:
      - name: artifact-spool
        mountPath: /var/lib/rootset/artifact-spool
        initialData: image
  health:
    type: http
    path: /health/ready
    timeoutSeconds: 180
  hooks:
    preDeploy:
      entrypoint: node
      command: [dist/migrate.mjs]
      timeoutSeconds: 300
  secrets:
    preDeploy: [ROOTSET_DATABASE_URL]
    runtime:
      - ROOTSET_DATABASE_URL
      - ROOTSET_API_URL
      - ROOTSET_WEB_URL
      - ROOTSET_SECRET
      - ROOTSET_ENCRYPTION_KEY
      - ROOTSET_S3_ENDPOINT
      - ROOTSET_S3_BUCKET
      - ROOTSET_S3_ACCESS_KEY
      - ROOTSET_S3_SECRET_KEY
      - ROOTSET_SMTP_HOST
      - ROOTSET_SMTP_PORT
      - ROOTSET_SMTP_SECURE
      - ROOTSET_SMTP_USER
      - ROOTSET_SMTP_PASSWORD
      - ROOTSET_EMAIL_FROM
  ingress:
    type: proxy
  domains:
    primary: rootset-api.example.com
  tls:
    mode: direct
  environments:
    production:
      server: 192.0.2.10
  ```

  ```yaml .towbar/services/rootset-worker.service.yml theme={"system"}
  # yaml-language-server: $schema=https://towbar.dev/schemas/app.v2.json
  id: rootset-worker
  name: Rootset worker
  deployment:
    type: image
    image: ghcr.io/avgeek-oss/rootset-worker:0.2.3
  autoDeploy: false
  rollout:
    type: recreate
    maintenanceMode: true
    reason: Keep one instance per service during an upgrade.
  container:
    network: rootset
    port: 4310
    resources:
      cpus: 1
      memory: 2g
    entrypoint: /bin/sh
    command:
      - -ec
      - |
        export ROOTSET_ARTIFACT_TEMP_DIR=/var/lib/rootset/artifact-spool
        export NODE_OPTIONS="--max-old-space-size=1536"
        exec node dist/worker.mjs
    volumes:
      - name: artifact-spool
        mountPath: /var/lib/rootset/artifact-spool
        initialData: image
  # No listener or public route. Towbar requires a container port even for command health.
  health:
    type: command
    command: [node, dist/worker-health.mjs]
    timeoutSeconds: 180
  hooks:
    preDeploy:
      entrypoint: node
      command: [dist/migrate.mjs]
      timeoutSeconds: 300
  secrets:
    preDeploy: [ROOTSET_DATABASE_URL]
    runtime:
      - ROOTSET_DATABASE_URL
      - ROOTSET_API_URL
      - ROOTSET_WEB_URL
      - ROOTSET_SECRET
      - ROOTSET_ENCRYPTION_KEY
      - ROOTSET_S3_ENDPOINT
      - ROOTSET_S3_BUCKET
      - ROOTSET_S3_ACCESS_KEY
      - ROOTSET_S3_SECRET_KEY
      - ROOTSET_SMTP_HOST
      - ROOTSET_SMTP_PORT
      - ROOTSET_SMTP_SECURE
      - ROOTSET_SMTP_USER
      - ROOTSET_SMTP_PASSWORD
      - ROOTSET_EMAIL_FROM
      # Optional: enable database backups on the worker.
      # - ROOTSET_BACKUP_S3_ENDPOINT
      # - ROOTSET_BACKUP_S3_REGION
      # - ROOTSET_BACKUP_S3_ACCESS_KEY
      # - ROOTSET_BACKUP_S3_SECRET_KEY
      # - ROOTSET_BACKUP_ENCRYPTION_KEY
  environments:
    production:
      server: 192.0.2.10
  ```

  ```yaml .towbar/services/rootset-web.service.yml theme={"system"}
  # yaml-language-server: $schema=https://towbar.dev/schemas/app.v2.json
  id: rootset-web
  name: Rootset UI
  deployment:
    type: image
    image: ghcr.io/avgeek-oss/rootset-web:0.2.3
  autoDeploy: false
  rollout:
    type: recreate
    maintenanceMode: true
    reason: Keep one instance per service during an upgrade.
  container:
    network: rootset
    port: 4300
    resources:
      cpus: 0.25
      memory: 1g
    entrypoint: /bin/sh
    command:
      - -ec
      - |
        export NODE_OPTIONS="--max-old-space-size=768 --max-http-header-size=196608"
        exec node apps/rootset-web/server.js
  health:
    type: http
    path: /
    timeoutSeconds: 180
  secrets:
    runtime: [ROOTSET_PUBLIC_API_ORIGIN]
  ingress:
    type: proxy
  domains:
    primary: rootset.example.com
  tls:
    mode: direct
  environments:
    production:
      server: 192.0.2.10
  ```

  ```yaml .towbar/datastores/rootset-postgres.datastore.yml theme={"system"}
  # yaml-language-server: $schema=https://towbar.dev/schemas/resource.v2.json
  id: rootset-postgres
  name: Rootset PostgreSQL
  type: postgres
  image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
  autoDeploy: false
  container:
    port: 5432
    network: rootset
    networkAlias: rootset-postgres
    resources:
      cpus: 1
      memory: 4g
    volumes:
      - name: data
        mountPath: /var/lib/postgresql
    configFiles:
      - source: .towbar/config/postgres-init.sh
        mountPath: /docker-entrypoint-initdb.d/10-rootset.sh
        mode: "0555"
  health:
    type: command
    command: [pg_isready, -U, rootset, -d, rootset]
    timeoutSeconds: 180
  secrets:
    runtime: [POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD]
  environments:
    production:
      server: 192.0.2.10
  ```

  ```bash .towbar/config/postgres-init.sh theme={"system"}
  #!/bin/sh
  set -eu
  [ "$POSTGRES_USER" = postgres ] && [ "$POSTGRES_DB" = rootset ] || {
    echo "Set POSTGRES_USER=postgres and POSTGRES_DB=rootset for initial setup" >&2
    exit 1
  }
  psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set ON_ERROR_STOP=1 \
    --set password="$POSTGRES_PASSWORD" <<'SQL'
  SELECT format('CREATE ROLE rootset LOGIN PASSWORD %L NOSUPERUSER NOBYPASSRLS', :'password') \gexec
  ALTER ROLE postgres PASSWORD NULL;
  ALTER DATABASE rootset OWNER TO rootset;
  GRANT CREATE ON SCHEMA public TO rootset;
  SQL
  ```
</CodeGroup>

## Runtime secrets

For the bundled PostgreSQL datastore, set:

```dotenv theme={"system"}
POSTGRES_USER=postgres
POSTGRES_DB=rootset
POSTGRES_PASSWORD=your-generated-password
```

The initialization script creates an ordinary `rootset` database owner. It runs only on an empty datastore volume. Do not delete an existing volume to force initialization.

On **both API and worker**, add the database URL, public URLs, application secrets, S3 source and SMTP settings from [Configuration](/self-hosting/configuration). Use `rootset-postgres:5432` as the database host when using the bundled datastore.

Also add `ROOTSET_DATABASE_URL` to the **pre-deploy secrets** of both services so their migration hooks can connect. On the **UI**, add only `ROOTSET_PUBLIC_API_ORIGIN`, set to the public API URL.

If you use an existing PostgreSQL server, omit the datastore and initialization script, and supply its ordinary database-owner URL instead.

## Sync and deploy

Connect the deployment repository in Towbar, map the `production` environment to your branch, and sync the manifests.

1. Deploy PostgreSQL and wait for readiness, or check your existing database connection.
2. Deploy the worker and wait for its migration hook and heartbeat check.
3. Deploy the API and wait for `/health/ready`.
4. Deploy the UI and open its public URL.
5. Claim the installation with your configured application secret and create the first administrator.

The examples keep deployment manual with `autoDeploy: false`. Keep all three application images on the same release version. Follow [Upgrades](/self-hosting/upgrades) when updating an existing installation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.