PLATINUM DOCS
Self-hosting

Installation

Start the control plane on one Linux machine with Docker Compose and prebuilt images.

The control plane runs on one Linux machine from prebuilt images: Postgres, migrations, the API server and Caddy for TLS. Nothing is compiled.

Check what you need

NeedDetail
A Linux machinelinux/amd64, with Docker Engine and the Compose plugin
A DNS name for the APIAn A record such as api.example.com pointing at this machine
Open ports80 and 443, for the API and the Let's Encrypt HTTP-01 challenge
A private addressThe address your sandbox hosts reach this machine on (VPC, VPN or tailnet)

Pick a version

Both images, ghcr.io/kortix-ai/platinum-api and ghcr.io/kortix-ai/platinum-web, always run at the same tag.

TagMoves?Use
X.Y.ZNeverProduction: pin one
latestTo each new releaseTrying the newest release
edgeTo every main commitFollowing development
sha-<12 hex>NeverOne exact main commit

A release tag is cut from a commit already built on main, so X.Y.Z and that commit's sha- tag are the same image. Use edge until a release you want exists.

Write the settings

setup.sh writes .env with mode 600 and generates every secret.

git clone https://github.com/kortix-ai/platinum.git
cd platinum/infra/compose
./setup.sh --domain api.example.com --email you@example.com --version X.Y.Z

Use your own hostname: setup.sh refuses the api.example.com placeholder.

Re-running setup.sh keeps every secret, and a flag you leave out keeps its current value. So it is safe to run again to change one setting.

Start the stack

Compose starts postgres, then migrate, then server, then caddy.

docker compose up -d
docker compose logs migrate        # both migration lanes, then "[migrate] done"
curl https://api.example.com/health

A failed migration stops the rollout: the server never starts against a schema it does not understand.

Create the bucket

With the bundled object store, create the bucket once after the first start.

docker compose exec s3 sh -c 'echo "s3.bucket.create -name platinum" | weed shell'

Skip this with --no-s3. See Object storage.

Firewall the shared ports

Postgres (:5432) and the bundled store (:9000) are published on --private-ip, because sandbox hosts read both. Allow only your hosts to reach them.

setup.sh prints a note when that address is public. Docker-published ports bypass ufw, so filter them in the DOCKER-USER chain or keep hosts on a private network. See Hardening.

Serve plain HTTP on a private network

With no domain, pass an http:// address instead. Caddy then serves plain HTTP.

./setup.sh --domain http://<private-ip> --version X.Y.Z

setup.sh writes PT_PUBLIC_API_URL, PT_AUTH_BASE_URL and PT_API_HOST for you in this mode. Use it on private networks only.

Build from source

For contributors, or to run unreleased changes:

./setup.sh --domain api.example.com --build
docker compose up -d --build

--build stamps the images with the checkout's commit, so re-run setup.sh after each pull. --no-build --version X.Y.Z goes back to the published images. The console build needs about 2 GB of RAM.

Check a stack

smoke.sh starts a throwaway stack from any tag and checks migrations (twice), health, the served installer, the host bundle and the console.

PLATINUM_VERSION=edge ./smoke.sh

Continue