API with a Database
Most backend services need a database. This guide walks through the full lifecycle: deploy the API, attach a DATABASE_URL secret, tune the connection pool with non-sensitive env vars, verify health, and rotate the password when the time comes.
The pattern applies equally to Python, Node.js, or any language — only the Go snippet and Dockerfile change.
Overview
Section titled “Overview”Satusky injects both secrets and environment variables into your containers as ordinary environment variables at pod start. The difference is in how they’re stored and who can read them:
- Secrets (
1ctl secret) — encrypted at rest with AES-256-GCM, values never returned after creation. Use these forDATABASE_URL, API keys, and tokens. - Env vars (
1ctl env) — stored in plaintext ConfigMaps. Use these for tuning knobs: pool sizes, timeouts, log levels.
Neither type hot-reloads into running pods. Changes take effect on the next pod start — either from a 1ctl deploy restart or a fresh 1ctl deploy.
The application
Section titled “The application”The API reads DATABASE_URL from its environment and refuses to start if it isn’t set:
package main
import ( "database/sql" "log" "net/http" "os"
_ "github.com/lib/pq")
func main() { dsn := os.Getenv("DATABASE_URL") if dsn == "" { log.Fatal("FATAL: DATABASE_URL is not set") }
db, err := sql.Open("postgres", dsn) if err != nil { log.Fatalf("FATAL: could not open database: %v", err) } defer db.Close()
if err := db.Ping(); err != nil { log.Fatalf("FATAL: could not connect to database: %v", err) } log.Println("connected to database")
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
log.Println("server listening on :8080") log.Fatal(http.ListenAndServe(":8080", nil))}The log.Fatal on a missing DATABASE_URL is intentional. A misconfigured pod that crashes immediately is much easier to diagnose than one that silently uses a nil connection and produces confusing errors later.
Dockerfile
Section titled “Dockerfile”FROM golang:1.23-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o server .
FROM alpine:3.20RUN apk add --no-cache ca-certificates tzdataWORKDIR /appCOPY --from=builder /app/server .EXPOSE 8080CMD ["./server"]The multi-stage build keeps the final image small. ca-certificates lets the binary verify TLS when connecting to managed databases that require SSL. tzdata covers any code that formats timestamps in a named timezone.
satusky.toml
Section titled “satusky.toml”[app] name = "go-api" port = 8080 dockerfile = "Dockerfile" cpu = "0.5" memory = "256Mi"Commit this file. It contains no secrets — only structural deployment spec.
Step 1: Deploy first (it will crash — that’s expected)
Section titled “Step 1: Deploy first (it will crash — that’s expected)”1ctl deploy --waitExpected output:
💡 Packaging build context...💡 Submitting build to cloud...💡 Build ID: bld_4m9rk2x7q1Step 1/5: Building image (cloud) go-api ✓Step 2/5: Creating/updating deployment go-api ✓Step 3/5: Configuring services go-api ✓Step 4/5: Setting up environment and storage go-api ✓Step 5/5: Configuring public routing and dependencies go-api ✓💡 Generated new domain: quietpanda-r7w4p1.satusky.com✅ 🚀 Deployment for go-api is successful! Your app is live at: https://quietpanda-r7w4p1.satusky.comDeployment ID: 4m9rk2x7-5f87-4612-b306-3da846b95d18💡 Waiting for deployment to become healthy...✅ Deployment is healthy — pods Running--wait blocks until the pod reaches Running status — which only means the container started, not that the application is healthy. The pod will be running and immediately crashing in a restart loop because DATABASE_URL is missing. That’s expected. The next steps fix it.
Step 2: See the crash in logs
Section titled “Step 2: See the crash in logs”1ctl logs streamExpected output:
2026-04-27T10:00:08Z [go-api] FATAL: DATABASE_URL is not set2026-04-27T10:00:08Z [go-api] exit status 12026-04-27T10:00:11Z [go-api] container restarting... (backoff: 10s)2026-04-27T10:00:21Z [go-api] FATAL: DATABASE_URL is not set2026-04-27T10:00:21Z [go-api] exit status 12026-04-27T10:00:26Z [go-api] container restarting... (backoff: 20s)Press Ctrl+C to stop the stream. Kubernetes is in CrashLoopBackOff, doubling the restart delay each time. Normal — move to the next step.
Step 3: Add the database secret
Section titled “Step 3: Add the database secret”1ctl secret create \Expected output:
✅ Secret go-api created successfullysecret create merges: it only updates the keys you pass. Any other secrets already attached to this deployment are left unchanged. The secret is now stored encrypted and associated with the deployment, but the running (crashing) pod doesn’t have it yet — pods pick up changes on the next start.
Step 4: Add connection pool env vars
Section titled “Step 4: Add connection pool env vars”1ctl env create \ --env DB_MAX_CONNECTIONS=25 \ --env DB_POOL_TIMEOUT=30sExpected output:
✅ Environment go-api created successfullyThese are tuning knobs, not credentials — env vars are the right tool here. Your application reads them with os.Getenv("DB_MAX_CONNECTIONS") and passes the value to db.SetMaxOpenConns. Like secrets, env vars also merge: only the keys you pass are touched.
Step 5: Restart
Section titled “Step 5: Restart”1ctl deploy restartExpected output:
💡 Initiating rolling restart for deployment <id>...✅ Rolling restart initiated. Pods are being replaced one by one.💡 Use '1ctl deploy status --deployment-id <id>' to monitor progress.deploy restart does a rolling replacement: it starts a new pod with the updated secrets and env vars, waits for it to pass its readiness check, then terminates the old pod. Traffic is routed to healthy instances throughout — zero downtime. No rebuild happens; the existing image is reused.
Step 6: Verify healthy
Section titled “Step 6: Verify healthy”1ctl logs streamExpected output:
2026-04-27T10:05:14Z [go-api] connected to database in 42ms2026-04-27T10:05:14Z [go-api] server listening on :80802026-04-27T10:05:31Z [go-api] GET /health 200 91µsPress Ctrl+C when you’ve seen enough. For a machine-readable health check:
1ctl -o json deploy getExpected output:
{ "deployment_id": "4m9rk2x7-5f87-4612-b306-3da846b95d18", "app_label": "go-api", "status": "completed", "replicas": 1, "cpu_request": "0.5", "memory_request": "256Mi", "memory_limit": "256Mi", "port": 8080, "image": "registry.satusky.com/...", "domain": "https://quietpanda-r7w4p1.satusky.com", "created_at": "2026-04-27T10:00:00+08:00", "updated_at": "2026-04-27T10:05:10+08:00"}"status": "completed" means the deployment is fully healthy. Use this in health-check scripts — the -o json output is stable and machine-parseable.
Step 7: Remove a tuning variable
Section titled “Step 7: Remove a tuning variable”Decided DB_POOL_TIMEOUT should use the driver’s built-in default instead:
1ctl env unset --key DB_POOL_TIMEOUTExpected output:
✅ Key "DB_POOL_TIMEOUT" removed from environmentenv unset removes exactly the one key you name. DB_MAX_CONNECTIONS and any other variables are untouched. Restart to apply:
1ctl deploy restartStep 8: Rotate the database password
Section titled “Step 8: Rotate the database password”Your database provider has issued new credentials. Update the secret by running secret create again with the new value:
1ctl secret create \Expected output:
✅ Secret go-api created successfullyBecause secret create merges, only DATABASE_URL is updated. No other keys in the secret group are touched. Then restart:
1ctl deploy restartThe new pod starts with the rotated credential. The old pod is terminated only after the new one passes its readiness check.
Never put DATABASE_URL in satusky.toml. Config files end up in version control. Secrets do not.
Use -o json in health-check scripts. 1ctl -o json deploy get gives you a stable structure you can pipe through jq. Example: 1ctl -o json deploy get | jq -e '.status == "completed"' exits 0 on healthy, 1 otherwise.
Rolling restart stalled? Run 1ctl logs stream immediately. If the new pod is crashing (wrong credentials, missing env var, unreachable database), the crash reason appears in the stream within seconds. The old pod stays up until the new one is healthy, so there’s no outage while you diagnose.
secret create is idempotent on the keys you pass. Calling it multiple times with the same value is safe — it upserts, it does not create duplicates.