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:

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

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.