Administration & hosting

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_index to 1 or 2, or retry later.
  • Secrets are passed via instance metadata on first boot only (ignore_changes = [metadata]). Rotate them in /opt/rapidforge/.env and docker 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 to main, add repository secrets DEPLOY_HOST, DEPLOY_USER (default ubuntu) and DEPLOY_SSH_KEY (a private key authorised on the VM), and optionally the variables DEPLOY_PATH (default /opt/rapidforge) and RAPIDFORGE_URL (enables a health check after the rollout). The deploy job in ci.yml then SSHes in once tests and the image build pass, pulls the image for that commit from GHCR, retags it as rapidforge:local (what the VM's RAPIDFORGE_IMAGE points at) and runs docker compose up -d. The VM must be able to docker pull from 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/*.sh to /opt/rapidforge and add the two cron lines from there.
  • Logs: docker compose logs -f api worker (structured JSON).
  • Health: GET /healthz checks the DB connection (the container's Docker HEALTHCHECK); GET /healthz?deep=1 also 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.

Something unclear or missing on this page?