Each stage of every deploy and rollback is saved to SQLite, so the history survives a restart. A deploy killed halfway is marked interrupted, with the stage it reached. Read the history docs
sling /historyCommit/
CommitStartedStatusTook
D9c41d0edeploy · Nov 08 16:52health_checking22s
D53a2b2fdeploy · Nov 08 16:46live34s
D399076adeploy · Nov 08 16:46interrupted2s
R67dd979rollback · Nov 08 16:45live33s
Dfeb2e72deploy · Nov 08 16:45failed2s
Da07c3e5deploy · Nov 08 15:12rolled_back41s
D67dd979deploy · Nov 08 14:30live35s
D5d02b6fdeploy · Nov 08 14:02superseded0s
De3b8f41deploy · Nov 08 13:58live36s
De3b8f41deploy · Nov 08 13:57skipped0s
D2c9a7d0deploy · Nov 07 18:20live34s
you/app/maindeploy21.7shealth_checking
MBTime out slow health checks9c41d0e
Clonegit checkout --detach1.4s
Builddocker build19.2s
ProbeGET /health1.1s
SwitchPOST /loadQueued
Watch30s graceQueued
$git clone --no-checkout https://github.com/you/app /tmp/paas-builds/9c41d0eCloning into '/tmp/paas-builds/9c41d0e'...remote: Enumerating objects: 412, done.remote: Counting objects: 100% (412/412), done.remote: Compressing objects: 100% (231/231), done.Receiving objects: 100% (412/412), 188.20 KiB, done.Resolving deltas: 100% (164/164), done.$git checkout --detach 9c41d0eHEAD is now at 9c41d0e Time out slow health checks$git rev-parse HEAD9c41d0e matches the commit asked for$docker build -t paas-app:9c41d0e /tmp/paas-builds/9c41d0e#1 [internal] load build definition from Dockerfile#2 [internal] load metadata for docker.io/library/node:22-alpine#3 [1/5] FROM docker.io/library/node:22-alpine#4 [2/5] WORKDIR /app#5 [3/5] COPY package*.json ./#6 [4/5] RUN npm ci --omit=dev#7 [5/5] COPY . .#8 exporting to image#8 naming to docker.io/library/paas-app:9c41d0e doneBuilt paas-app:9c41d0e$docker run -d --name app-9c41d0e --network sling-net -p 127.0.0.1::3000 --memory 256m --cpus 0.5 -e APP_VERSION=9c41d0e paas-app:9c41d0e$docker port app-9c41d0e 3000127.0.0.1:49731Waiting for http://127.0.0.1:49731/health to pass health checks...+0.5s container running, GET /health: connection refused+1.0s container running, GET /health: connection refused+1.5s container running, GET /health: connection refused+2.0s container running, GET /health: 503 Service Unavailable+2.5s container running, GET /health: 503 Service Unavailable+3.0s container running, GET /health: 200 OK (1 of 3)+3.5s container running, GET /health: 200 OK (2 of 3)needs 3 in a row before 30s$curl -X POST localhost:2019/load (upstream app-9c41d0e:3000)Switching traffic: "app-53a2b2f:3000" -> "app-9c41d0e:3000"Verifying through the proxy (http://127.0.0.1:8080)...GET /health 200, X-Sling-Upstream: app-9c41d0e:3000 (1 of 3)GET /health 200, X-Sling-Upstream: app-9c41d0e:3000 (2 of 3)GET /health 200, X-Sling-Upstream: app-9c41d0e:3000 (3 of 3)The new version is the one answeringObserving for 30s before retiring the previous version...probes through the proxy, rolls back on 3 failures in a row+10s every probe passed+20s every probe passed+30s grace window passedRetiring previous version app-53a2b2f (SIGTERM, up to 10s to drain)...app-53a2b2f stopped, image paas-app:53a2b2f kept for rollbackLive: 9c41d0e
Traffic moves last.
Sling points Caddy at the new container only after it answers /health three times in a row. Then it checks again through the proxy, as your users reach it.
Safe deploys on one host. The parts you stop doing over SSH.
Rolls back a failed release
If 3 probes in a row fail in the 30-second grace window, Caddy goes back to the previous version.
Rolled back to 67dd9793 proxy probes failed in a row
Observing a07c3e530s grace window before retiring
Switched trafficapp-67dd979 to app-a07c3e5
Accepts only signed pushes
Every GitHub delivery needs a valid HMAC-SHA256 signature and must come from your repo and branch.
GOhandler.goGOqueue.go401 if wrong
funcvalidSignature(secret, body []byte, header string) bool { hexSig, ok := strings.CutPrefix( header, "sha256=") mac := hmac.New(sha256.New, secret) mac.Write(body)return hmac.Equal(got, mac.Sum(nil))}
Signs the raw body with your secret.
Gets HTTPS certificates itself
Set SLING_DOMAIN and Caddy obtains and renews a Let’s Encrypt certificate. Only /webhook is passed to Sling.
sling serve settings
4 of 5 on
HTTPS on your domainSLING_DOMAIN
Deploy on git pushSLING_WEBHOOK_SECRET
Deploy status APISLING_STATUS_TOKEN
Deploy one branchSLING_BRANCH=main
Keep deploy historySLING_DATA_DIR
Runs one deploy at a time
A file lock stops two deploys fighting over the proxy. A newer push replaces the one still waiting.
sling serve /queue
Db81e4a2push to main · waitingqueuednow
D7f30c19replaced by b81e4a2superseded8s
D9c41d0eholds the deploy locklive1m
D53a2b2fprevious, image keptstopped6m
How a commit goes live
Sling clones the commit, builds it with Docker and starts it beside the running version on a random loopback port. Three checks decide if users ever see it.
Three 200s in a row from /health, within 30 seconds
The X-Sling-Upstream header names the new container