Self-hosting on Oracle Cloud
Run Diffbridge on the OCI Always Free tier with Terraform and Docker Compose.
Diffbridge runs as one Docker image in two roles (api and worker) next to Postgres and Caddy. On
OCI's Always Free tier that all fits on a single Ampere A1 VM (arm64); the image is built
multi-arch, so it runs as-is.
| Piece | OCI Always Free resource | Notes |
|---|---|---|
| App + worker + Postgres + Caddy | VM.Standard.A1.Flex, 2 OCPU / 12 GB (up to 4 / 24 free) |
Docker Compose |
| Packages, snapshots, rollback data | Object Storage bucket (S3-compatible API) | 20 GB free; or keep blobs on the boot volume (STORAGE_DRIVER=fs) |
| TLS | Caddy + Let's Encrypt | automatic |
| Backups | pgbackup sidecar, nightly deploy/backup-to-object-storage.sh into a separate rapidforge-backups bucket, weekly restore test |
7 days on the VM, 35 days off-box; see Operations |
1. Build and push the image
The GitHub Actions workflow (.github/workflows/ci.yml) tests, then builds linux/arm64,linux/amd64
and pushes ghcr.io/<you>/rapidforge. To build by hand:
docker buildx build --platform linux/arm64,linux/amd64 -t ghcr.io/your-org/rapidforge:latest --push .
2. Provision with Terraform
cd deploy/terraform/oci
cp terraform.tfvars.example terraform.tfvars # fill in OCIDs, SSH key, domain
terraform init
terraform apply
Terraform creates the VCN, a public subnet (22/80/443), the Object Storage buckets (the app's, with
versioning, and one for backups, with retention rules), an S3 "customer secret key", and the A1
instance. cloud-init installs Docker, writes /opt/rapidforge/.env with generated secrets, schedules
the nightly backup and weekly restore test, and starts the stack.
- Point your domain's A record at
terraform output public_ip. Caddy issues the certificate once DNS resolves. - Back up the encryption key:
terraform output -raw encryption_key. Without it, stored org tokens can't be decrypted. - If you see Out of host capacity, A1 capacity is temporarily exhausted in that AD — set
availability_domain_indexto 1 or 2, or retry later. - Secrets are passed via instance metadata on first boot only (
ignore_changes = [metadata]). Rotate them in/opt/rapidforge/.envanddocker compose up -d.
3. Without Terraform (any VM)
git clone https://github.com/your-org/rapidforge && cd rapidforge/deploy
cp .env.example .env && $EDITOR .env
docker compose up -d
On OCI Ubuntu images, open the host firewall too:
sudo iptables -I INPUT 6 -p tcp --dport 80 -j ACCEPT && sudo iptables -I INPUT 6 -p tcp --dport 443 -j ACCEPT && sudo netfilter-persistent save.
4. Operations
- Upgrade:
docker compose pull && docker compose up -d. To do this on every push tomain, add repository secretsDEPLOY_HOST,DEPLOY_USER(defaultubuntu) andDEPLOY_SSH_KEY(a private key authorised on the VM), and optionally the variablesDEPLOY_PATH(default/opt/rapidforge) andRAPIDFORGE_URL(enables a health check after the rollout). Thedeployjob inci.ymlthen SSHes in once tests and the image build pass, pulls the image for that commit from GHCR, retags it asrapidforge:local(what the VM'sRAPIDFORGE_IMAGEpoints at) and runsdocker compose up -d. The VM must be able todocker pullfrom GHCR (log in once with a read-only token if the package is private). Migrations run automatically on start, forward-only. - More throughput:
docker compose up -d --scale worker=2. Workers coordinate through Postgres advisory locks, so each org still runs one deployment at a time. - Backups, restores, monitoring and a go-live checklist: see Operations. Without Terraform, copy
deploy/*.shto/opt/rapidforgeand add the two cron lines from there. - Logs:
docker compose logs -f api worker(structured JSON). - Health:
GET /healthzchecks the DB connection (the container's DockerHEALTHCHECK);GET /healthz?deep=1also checks file storage and the worker, for uptime monitoring.
Deploying without GitHub Actions
GitHub's free plan includes 2,000 Actions minutes a month. When they run out (or you'd rather not wait for CI), deploy from your own computer:
npm run deploy:local # type check, tests, build on the VM, restart, wait for health
npm run deploy:local -- --dry-run # show the steps and touch nothing
It deploys the committed HEAD of main. The tests run on your computer first; the image is built
on the VM itself with git archive HEAD | ssh ... docker build, so the build is native arm64 (no
emulation) and you don't need Docker on your Mac or Windows PC, only git, ssh and Node. The
same compose steps as the CI deploy job then recreate the containers, and the script waits for
/healthz?deep=1.
Settings (environment variables, all optional): DEPLOY_HOST (default
opc@rapidforge.mokahub.duckdns.org), DEPLOY_PATH (/opt/rapidforge), DEPLOY_SSH_KEY_FILE
(~/.ssh/rapidforge_deploy), RAPIDFORGE_URL.
To keep GitHub free, set an Actions budget of $0 under Settings → Billing → Budgets: when the
minutes run out, workflows stop instead of billing, and you use deploy:local until the reset.