Deploy on Oracle Cloud
Single node on an OCI Compute instance with OCI Database with PostgreSQL: the resources to create, the settings that matter, and the exact commands, step by step.
On this page
This is the one-node deployment mapped onto Oracle Cloud Infrastructure: one Compute instance running the image, one OCI Database with PostgreSQL system, one Load Balancer terminating TLS. Follow the numbered steps in order; each names the OCI resource, the settings that matter, and the command to run. The three-region layout adds nodes and balancers to this exact base later.
Resources
| OCI resource | Settings |
|---|---|
| VCN | one VCN with a public subnet (load balancer) and a private subnet (compute + database), a NAT gateway on the private subnet for outbound HTTPS |
| Compute instance | VM.Standard.E4.Flex, 2 OCPU / 8 GB, Oracle Linux 9 or Ubuntu 22.04, private subnet, Docker Engine + Compose v2 installed |
| Database with PostgreSQL | version 16, one primary node (add read replicas later for DR only), private endpoint in the private subnet, vector, uuid-ossp, pgcrypto enabled on the DB system |
| Load Balancer | flexible shape, public subnet, TLS listener on 443 with your certificate, backend set → compute instance port 8080 |
| Vault | one secret holding secrets.json (base64 of the file) |
| Security lists | LB → compute 8080; compute → database 5432; nothing else inbound. Ports 5000–5999 and 9000–9999 on the compute instance stay closed |
1. Database
Create the DB system with PostgreSQL 16. Before anything else open
Configuration → Extensions on the DB system and enable vector,
uuid-ossp and pgcrypto; an extension not enabled at the system level
cannot be created inside the database.
From the compute instance (the endpoint is private), run the bundle's provisioning script as the DB-system admin user:
psql "postgres://<admin>:<admin-pw>@<db-private-fqdn>:5432/postgres?sslmode=require" \
-v pw="<application-db-password>" -f provision.sql
It creates the systemprompt role and database and installs the three
extensions. The OCI certificate chains to a public root, so sslmode=require
works with no extra CA file. The application connection string:
postgres://systemprompt:<application-db-password>@<db-private-fqdn>:5432/systemprompt?sslmode=require
2. Compute instance
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Oracle Linux 9
sudo systemctl enable --now docker && sudo usermod -aG docker "$USER"
git clone https://github.com/systempromptio/systemprompt-customertimes.git
cd systemprompt-customertimes/deploy/self-host
echo "$GHCR_TOKEN" | docker login ghcr.io -u <github-user> --password-stdin
docker pull ghcr.io/systempromptio/systemprompt-customertimes:0.44.0
Outbound access needed through the NAT gateway: ghcr.io,
api.anthropic.com, both on 443.
3. Secrets and profile
Render once, on the compute instance or an operator machine:
./render-profile.sh --out ./profile \
--external-url https://ai.example.com \
--admin-email you@example.com \
--trusted-proxies 10.0.0.0/24 \
--database-url 'postgres://systemprompt:<pw>@<db-private-fqdn>:5432/systemprompt?sslmode=require' \
--anthropic-key sk-ant-... \
--image ghcr.io/systempromptio/systemprompt-customertimes:0.44.0
--trusted-proxies is the CIDR of the load balancer's subnet. Store the
result in Vault and place it on the instance:
oci vault secret create-base64 --region <region> --compartment-id ... --vault-id ... --key-id ... \
--secret-name customertimes-secrets --secret-content-content "$(base64 -w0 profile/secrets.json)"
sudo install -d -m 0750 -o 1000 -g 1000 /etc/customertimes/profile
sudo install -m 0644 -o 1000 -g 1000 profile/profile.yaml /etc/customertimes/profile/profile.yaml # per-deployment settings only; the model catalog is in the image
oci secrets secret-bundle get --region <region> --secret-id <ocid> --query 'data."secret-bundle-content".content' --raw-output \
| base64 -d | sudo install -m 0600 -o 1000 -g 1000 /dev/stdin /etc/customertimes/profile/secrets.json
Pass --region (for example eu-frankfurt-1) on every oci command. The CLI
otherwise falls back to the region in the active profile, which resolves to a
different tenancy endpoint and fails with a not-found or auth error even though
the OCID is correct.
--secret-content-content takes the base64 value itself, not a file reference:
file:///dev/stdin is rejected by the OCI CLI, so pass the encoded contents
inline via command substitution. On macOS, base64 -w0 is base64 with no
flags.
The container runs as uid 1000 and mounts /etc/customertimes/profile read-only.
4. Boot
export SYSTEMPROMPT_TAG=0.44.0 CUSTOMERTIMES_PROFILE_DIR=/etc/customertimes/profile
export DATABASE_URL='postgres://systemprompt:<pw>@<db-private-fqdn>:5432/systemprompt?sslmode=require'
docker compose -f docker-compose.node.yml pull
docker compose -f docker-compose.node.yml up -d
docker compose -f docker-compose.node.yml ps # wait for: healthy
docker-compose.node.yml is the app-only file: it needs DATABASE_URL for the
readiness probe and the same value inside secrets.json. If verify-node.sh
later reports the public site missing (older entrypoints skipped the render
on this node), run it once after healthy:
docker compose -f docker-compose.node.yml exec \
-e SYSTEMPROMPT_PROFILE=/app/.systemprompt/profiles/self-host/profile.yaml \
app systemprompt infra jobs run publish_pipeline
5. Load balancer
| Setting | Value |
|---|---|
| Listener | HTTPS 443, your certificate |
| Backend set | the compute instance, port 8080, policy round robin |
| Health check | HTTP, path /api/v1/health, status 200, response body regex healthy. Status alone admits a node that is still migrating |
| Headers | forward X-Forwarded-For and X-Forwarded-Proto; keep the Host header |
| Timeouts | idle timeout 300 s (streamed responses) |
| Rules | return 403 for /metrics and /api/v1/health/detail |
| Drain | when stopping the node, drain it in the backend set and wait 30 s before docker compose stop |
Point the DNS name in --external-url at the load balancer.
6. Verify and sign in
./verify-node.sh --url http://localhost:8080 \
--container "$(docker compose -f docker-compose.node.yml ps -q app)" --version 0.44.0
curl -fsS https://ai.example.com/api/v1/health
Every line of the checklist must pass. Then sign in at
https://ai.example.com/admin/login with the admin e-mail, register a passkey,
and send users to Downloads, served by this node.
Upgrades, backups, growth
- Upgrade: drain at the load balancer,
SYSTEMPROMPT_TAG=<new>thenpullandup -d, runverify-node.sh, re-enable. Migrations are forward-only; take an OCI PostgreSQL backup first. - Backups: enable automatic backups on the DB system and test a restore once before launch.
- More nodes: create a compute instance and load balancer per region, copy
/etc/customertimes/profileunchanged, add each region's balancer subnet to--trusted-proxiesand re-render, and boot nodes one at a time. All nodes use the same primary; read replicas are for disaster recovery only.