Skip to main content

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:
RecordPurpose
your-domain.comControl plane web UI
registry.your-domain.comContainer image registry
logs.your-domain.comLog aggregation (Victoria Logs)

Quick Start

Run the automated install script on a fresh server:
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:
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:
COMPOSE_FILE=compose.production.yml
Set TECHULUS_CLOUD_VERSION to the release tag you are installing:
TECHULUS_CLOUD_VERSION=vX.Y.Z
Use tip only for rolling installs. Rolling installs do not show release update prompts. Then start the stack:
docker compose -f compose.production.yml up -d --pull always --remove-orphans
To use the bundled PostgreSQL instead of an external database:
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:
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

VariableDescription
ROOT_DOMAINYour domain (e.g., cloud.example.com)
ACME_EMAILEmail for Let’s Encrypt certificates
DATABASE_URLPostgreSQL connection string (e.g., postgres://user:pass@postgres:5432/techulus)
BETTER_AUTH_SECRETSecret key for authentication
ENCRYPTION_KEY32 bytes as a 64-character hex string

Control plane deployment

VariableDescription
COMPOSE_FILECompose file used by self-updates. Use compose.production.yml for external PostgreSQL or compose.postgres.yml for bundled PostgreSQL.
TECHULUS_CLOUD_VERSIONInstalled release tag. Use tip only for rolling installs without release prompts.
CONTROL_PLANE_UPDATER_TOKENRandom token used by the web app to call the internal updater service. Generate with openssl rand -hex 32.

Victoria Logs

VariableDescription
VL_USERNAMELogs service username
VL_PASSWORDLogs service password
VL_RETENTIONLog retention period (default: 7d)

Victoria Metrics

VariableDescription
VM_USERNAMEMetrics service username
VM_PASSWORDMetrics service password
VM_RETENTIONMetrics retention period (default: 30d)

Registry

VariableDescription
REGISTRY_USERNAMERegistry username for agents
REGISTRY_PASSWORDRegistry password for agents
REGISTRY_HTTP_SECRETInternal registry secret

Inngest

VariableDescription
INNGEST_SIGNING_KEYRequest verification key (prefix with signkey-prod-)
INNGEST_EVENT_KEYEvent API key

Control Plane Replicas

VariableDescription
WEB_REPLICASNumber 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)

VariableDescription
GITHUB_APP_IDGitHub App ID
GITHUB_APP_PRIVATE_KEYGitHub App private key (base64-encoded)
GITHUB_WEBHOOK_SECRETWebhook secret

Generating Secrets

# 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:
ServiceEndpoint
Webhttps://<ROOT_DOMAIN>
Registryhttps://registry.<ROOT_DOMAIN>
Logshttps://logs.<ROOT_DOMAIN>
PostgreSQLInternal only
InngestInternal 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

# 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.