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

# Installation

> Deploy the Techulus Cloud control plane on your own infrastructure.

## Prerequisites

* Docker and Docker Compose
* A domain name with DNS configured
* Ports 80 and 443 available

You need three DNS records pointing to your server:

| Record                     | Purpose                         |
| -------------------------- | ------------------------------- |
| `your-domain.com`          | Control plane web UI            |
| `registry.your-domain.com` | Container image registry        |
| `logs.your-domain.com`     | Log aggregation (Victoria Logs) |

## Quick Start

Run the automated install script on a fresh server:

```bash theme={null}
curl -fsSL https://raw.githubusercontent.com/techulus/cloud/main/deployment/install.sh | bash
```

The script detects your OS, installs Docker, walks you through DNS and environment configuration, and starts all services.

## Manual Setup

Clone the repository and configure your environment:

```bash theme={null}
cd deployment
cp .env.example .env
openssl rand -hex 32
```

Edit `.env` with your values. Set `CONTROL_PLANE_UPDATER_TOKEN` to
the generated token.

For an external PostgreSQL database, keep:

```env theme={null}
COMPOSE_FILE=compose.production.yml
```

Set `TECHULUS_CLOUD_VERSION` to the release tag you are installing:

```env theme={null}
TECHULUS_CLOUD_VERSION=vX.Y.Z
```

Use `tip` only for rolling installs. Rolling installs do not show release
update prompts.

Then start the stack:

```bash theme={null}
docker compose -f compose.production.yml up -d --pull always --remove-orphans
```

To use the bundled PostgreSQL instead of an external database:

```bash theme={null}
sed -i.bak 's/^COMPOSE_FILE=.*/COMPOSE_FILE=compose.postgres.yml/' .env
docker compose -f compose.postgres.yml up -d --pull always --remove-orphans
```

Production hosts should also cap Docker container logs. The installer creates
`/etc/docker/daemon.json` with `json-file` rotation on fresh hosts. If Docker is
already configured, keep your existing daemon settings and add equivalent log
rotation manually.

## Manual upgrades

Use the one-click upgrade flow in **Settings** when possible. If the updater
cannot run, update the deployment from the host manually.

Back up your `.env` file and database before upgrading. Then fetch the Compose
files for the release, update `TECHULUS_CLOUD_VERSION`, pull images, and restart
the stack:

```bash theme={null}
cd /opt/techulus-cloud
TARGET_VERSION=vX.Y.Z
COMPOSE_FILE=$(grep -E '^COMPOSE_FILE=' .env | cut -d= -f2- || true)
if [ -z "$COMPOSE_FILE" ]; then
  COMPOSE_FILE=compose.production.yml
fi

cp .env ".env.backup.$(date +%Y%m%d%H%M%S)"
curl -fsSL "https://raw.githubusercontent.com/techulus/cloud/${TARGET_VERSION}/deployment/compose.production.yml" -o compose.production.yml
curl -fsSL "https://raw.githubusercontent.com/techulus/cloud/${TARGET_VERSION}/deployment/compose.postgres.yml" -o compose.postgres.yml

if grep -q '^TECHULUS_CLOUD_VERSION=' .env; then
  sed -i.bak "s/^TECHULUS_CLOUD_VERSION=.*/TECHULUS_CLOUD_VERSION=${TARGET_VERSION}/" .env
else
  echo "TECHULUS_CLOUD_VERSION=${TARGET_VERSION}" >> .env
fi

docker compose -f "$COMPOSE_FILE" pull
docker compose -f "$COMPOSE_FILE" up -d --remove-orphans
```

Rollback after migrations may require restoring the database backup before you
restart older images.

The Compose files include container health checks for visibility. Plain Docker
Compose reports unhealthy containers but does not restart them automatically, so
use the common commands below when investigating a self-hosted service.

## Environment Variables

### Required

| Variable             | Description                                                                        |
| -------------------- | ---------------------------------------------------------------------------------- |
| `ROOT_DOMAIN`        | Your domain (e.g., `cloud.example.com`)                                            |
| `ACME_EMAIL`         | Email for Let's Encrypt certificates                                               |
| `DATABASE_URL`       | PostgreSQL connection string (e.g., `postgres://user:pass@postgres:5432/techulus`) |
| `BETTER_AUTH_SECRET` | Secret key for authentication                                                      |
| `ENCRYPTION_KEY`     | 32 bytes as a 64-character hex string                                              |

### Control plane deployment

| Variable                      | Description                                                                                                                               |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `COMPOSE_FILE`                | Compose file used by self-updates. Use `compose.production.yml` for external PostgreSQL or `compose.postgres.yml` for bundled PostgreSQL. |
| `TECHULUS_CLOUD_VERSION`      | Installed release tag. Use `tip` only for rolling installs without release prompts.                                                       |
| `CONTROL_PLANE_UPDATER_TOKEN` | Random token used by the web app to call the internal updater service. Generate with `openssl rand -hex 32`.                              |

### Victoria Logs

| Variable       | Description                          |
| -------------- | ------------------------------------ |
| `VL_USERNAME`  | Logs service username                |
| `VL_PASSWORD`  | Logs service password                |
| `VL_RETENTION` | Log retention period (default: `7d`) |

### Victoria Metrics

| Variable       | Description                               |
| -------------- | ----------------------------------------- |
| `VM_USERNAME`  | Metrics service username                  |
| `VM_PASSWORD`  | Metrics service password                  |
| `VM_RETENTION` | Metrics retention period (default: `30d`) |

### Registry

| Variable               | Description                  |
| ---------------------- | ---------------------------- |
| `REGISTRY_USERNAME`    | Registry username for agents |
| `REGISTRY_PASSWORD`    | Registry password for agents |
| `REGISTRY_HTTP_SECRET` | Internal registry secret     |

### Inngest

| Variable              | Description                                            |
| --------------------- | ------------------------------------------------------ |
| `INNGEST_SIGNING_KEY` | Request verification key (prefix with `signkey-prod-`) |
| `INNGEST_EVENT_KEY`   | Event API key                                          |

### Control Plane Replicas

| Variable       | Description                                                  |
| -------------- | ------------------------------------------------------------ |
| `WEB_REPLICAS` | Number of control plane web containers to run (default: `1`) |

When `WEB_REPLICAS` is greater than `1`, Traefik discovers the replicated web
containers through Docker and load balances requests for `<ROOT_DOMAIN>` across
them. Schema sync runs once from the dedicated `migrate` service before the
replicated web containers start, so scaling `WEB_REPLICAS` does not run
migrations from every replica.

### GitHub Integration (Optional)

| Variable                 | Description                             |
| ------------------------ | --------------------------------------- |
| `GITHUB_APP_ID`          | GitHub App ID                           |
| `GITHUB_APP_PRIVATE_KEY` | GitHub App private key (base64-encoded) |
| `GITHUB_WEBHOOK_SECRET`  | Webhook secret                          |

## Generating Secrets

```bash theme={null}
# Encryption key (64 hex characters)
openssl rand -hex 32

# Auth secret
openssl rand -hex 32

# Inngest signing key
echo "signkey-prod-$(openssl rand -hex 32)"

# Inngest event key
openssl rand -hex 16
```

## Services

Once running, the following services are available:

| Service    | Endpoint                         |
| ---------- | -------------------------------- |
| Web        | `https://<ROOT_DOMAIN>`          |
| Registry   | `https://registry.<ROOT_DOMAIN>` |
| Logs       | `https://logs.<ROOT_DOMAIN>`     |
| PostgreSQL | Internal only                    |
| Inngest    | Internal only                    |

Traefik handles TLS termination and automatic certificate renewal via Let's Encrypt.

## Database Migrations

The schema is synced automatically by the one-shot `migrate` service via `drizzle-kit push`. Non-destructive changes (adding tables, columns, indexes) are applied automatically. Destructive changes like dropping columns require manual intervention. If schema sync fails, `web` startup is blocked; inspect the failure with `docker compose logs migrate`.

## Common Commands

```bash theme={null}
# Check service status
docker compose -f compose.production.yml ps

# View logs
docker compose -f compose.production.yml logs -f

# Stop all services
docker compose -f compose.production.yml down --remove-orphans

# Update to the configured image references
docker compose -f compose.production.yml up -d --pull always --remove-orphans
```

Use versioned or digest-pinned image references for production updates when
possible. Mutable tags such as `latest` and `tip` are convenient, but they can
move between pulls.
