Getting started
From an empty repository to your product's platform answering at its own domain, in seven steps. The
product here is shop; use your own name throughout. A site of only static files needs no server: see
A static platform.
You need:
- Go, and ko in your CI;
- a Hetzner Cloud project: its API token, and Object Storage keys for backups;
- two domains: your product's (
shop.example) on Cloudflare's DNS, where your platform's own names (its API atapi.shop.example, its dashboard and console) and your services' names live side by side, and one for your platform's targets zone on Hetzner's DNS (shoptargets.example), which names in other zones point at; - a container registry your CI pushes to (GitHub's,
ghcr.io, with a machine user's read-only token for your servers to pull with), and a key to scaledbox's download site for its own images (the runtime's andboxd's), from scaledbox's maintainer:SCALEDBOX_DOWNLOAD_KEYin your.env.
1. Create the product's repository
Your product's repository holds its platform: the module it builds from, its brand, its two CLIs and its services.
mkdir shop && cd shop && git init
go mod init github.com/you/shop
go get github.com/scaledbox/scaledbox@latest
go.mod now requires github.com/scaledbox/scaledbox at a version. That version is your platform's:
its server, its runtime and its components are that version's. To upgrade, bump it (see
Your platform).
2. Your brand
The brand is what people see: your CLIs' names, their environment variables, your API's address. In
brand.go:
package shop
import "github.com/scaledbox/scaledbox/brand"
var Brand = brand.Brand{
Name: "shop", // the CLIs: shop (your tenants') and shop-ops (yours)
Title: "Shop", // what people read: the dashboard, the console, emails
EnvPrefix: "SHOP", // SHOP_TOKEN, SHOP_API_URL
API: "https://api.shop.example", // your public API, the tenants' CLI's default
Image: "ghcr.io/you/shop-ops", // your operator's image, built by your CI
}
Assets adds your logo and email templates (an fs.FS, e.g. an embed.FS). A brand that isn't valid
stops both CLIs before they run, saying what to fix.
3. The two CLIs
Two mains, each one line:
// cmd/shop-ops/main.go: your operator's CLI and your platform's server
package main
import (
"github.com/scaledbox/scaledbox/ops"
"github.com/you/shop"
)
func main() { ops.Main(shop.Brand) }
// cmd/shop/main.go: your tenants' CLI
package main
import (
"github.com/scaledbox/scaledbox/tenants"
"github.com/you/shop"
)
func main() { tenants.Main(shop.Brand) }
shop-ops is yours and never leaves your machines and CI: sync, deploys, tokens, your tenants, and
the server's own processes. shop is what you give your customers: it holds a token of one tenant and
none of the operator's code (a single-mode product needs no cmd/shop). To add commands of your own to
shop-ops, build its root with ops.Command(shop.Brand), add to it, and run it with
ops.Execute(root). Then record what the mains import:
go mod tidy
Your servers run the image of shop-ops, which your CI builds with ko for each commit and pushes as
sha-<commit> to your brand's Image:
KO_DOCKER_REPO=ghcr.io/you/shop-ops ko build ./cmd/shop-ops --bare \
--platform linux/amd64,linux/arm64 --tags "sha-$GITHUB_SHA"
No Dockerfile, no Docker. The runtime's and boxd's images are scaledbox's, built by its CI and served
by its download site (scaledbox.dev/boxd, a read-only registry behind your platform's key), found by
the scaledbox version in your go.mod: sync pins boxd by digest with your key, and your cluster
pulls it with the same pull Secret as your own image.
4. The cluster file
cluster.toml declares your platform's servers: its cluster, its location, its nodes and their server
types, its backup bucket, its DNS zones (shop.example), its targets zone (shoptargets.example)
and your API's name (public_api: api.shop.example, your brand's API). Start from scaledbox's
cluster.example.toml, whose comments explain every setting. Your services share the zones with your
platform: every name of them but your platform's own (its API, dashboard, console and status page) may
be a service's, and your platform publishes its record itself (Domains).
cluster.toml is git-ignored, and so is .env, which holds your credentials: Hetzner's token and
Object Storage keys, Cloudflare's token for your platform's zone, your registry's and scaledbox's pull
keys, and Resend's key for emails. Never commit them, and never put them on a command line.
printf 'cluster.toml\n.env\n/shop-ops\n' >> .gitignore
The cluster file decides the mode (see Your platform): a cluster for your tenants makes it saas; none, single. Start single on one server; add nodes, and a cluster for your tenants, when you need them.
5. sync
Commit and push, and let your CI build that commit's image: sync deploys the image of the commit
shop-ops is built from, so build it from a clean, committed tree.
set -a; . ./.env; set +a # your credentials, in this shell only
go build -o shop-ops ./cmd/shop-ops
./shop-ops config check
./shop-ops sync --plan # what would change, with each server's monthly price; changes nothing
./shop-ops sync # shows the same plan, asks once, then applies it
sync creates your servers, installs k3s and your platform's components, and starts your server: its
API, dashboard, database and reconciler. It applies only from a terminal, after you confirm: there is
no --yes, because servers cost money. Run it again whenever the cluster file or your go.mod
changes; it changes only what differs.
6. Describe your services in scaledbox.toml
Your product's own code runs as services, each a box of the JavaScript modules your build makes. A web service with a database:
[app]
name = "shop"
[services.web]
dir = "dist/web" # its built modules and box.toml (bindings: [bindings.DB] kind = "database")
health = "/healthz" # answers 2xx when ready
memory = "256Mi"
[postgres.main] # the database env.DB reaches, in every environment
[release] # its release() handler runs the migrations before each release goes live
service = "web"
[env.prod.services.web]
domains = ["shop.example"]
replicas = 2
[env.staging.services.web]
domains = ["staging.shop.example"]
Every setting is in the scaledbox.toml reference.
7. Deploy
Build your services' modules (any bundler: see Deploying), then deploy them through your operator's CLI:
./shop-ops deploy --env prod
The CLI deploys nothing itself: it sends scaledbox.toml and the files your server lacks, and your
server reconciles the environment to the new release, compiling what is new. The first deploy creates
the environment. The release handler runs, then a box of each service's new release must answer its
health path before the release goes live; if not, the previous release keeps running and the deploy
fails with the reason.
Names in your platform's zones (shop.example, staging.shop.example) answer at once: your platform
publishes their records. A domain of another zone answers once its DNS records are in place:
./shop-ops domains
lists each name with the records to create at your DNS provider (see Domains).
Next
- Deploy from CI with a deploy token.
- Set secrets, read logs, size your services' instances.
- In saas mode, run your customers' apps with the tenants API, and release
shop, your tenants' CLI, for them to sign in through your page.
A static platform
A product of only static files (a homepage, docs, downloads) needs no server: no cluster file, no
sync, no image, no cmd/shop, nothing that bills. After steps 1 to 3, its scaledbox.toml serves
each service from Cloudflare:
[app]
name = "shop"
[services.site]
assets = "dist/site" # its files, as your build wrote them
domains = ["shop.example"] # in a zone of your Cloudflare account
provider = "cloudflare"
With no server to post to, its CI deploys it itself (shop-ops deploy --env prod), with a Cloudflare
token scoped to its zones and Workers: see
static services on Cloudflare. scaledbox's own site is
one.