๐ค Deploying on CARLOS
Rastrillo builds apps; the CARLOS platform runs them. The contract
between the two is what rastrillo.Resolve and rastrillo.Serve
implement, and none of it is yours to hand-roll.
What the framework answers
Activation argv in both shapes the platform uses: -socket/-addr/-db
flags for an agent exec child on a hibernating route, or a bare serve
subcommand for a carlos-app@.service unit tenant. LISTEN_FDS socket
activation, before falling back to Addr. $STATE_DIRECTORY, so a
relative -db resolves inside it when systemd provides one โ a unit
tenant's working directory is not its state directory. GET /healthz
and GET /api/version. The SIGTERM drain, which fits inside the
activator's SIGKILL budget. And baseline security headers, CSP included,
framework-owned and outermost, with your own Set or Del winning and
Options.CSP swapping the policy wholesale.
Between Serve and Run, every route kind the platform runs โ
always-on instance, hibernating exec child, unit tenant โ boots the same
scaffolded app.
Hibernation needs nothing from you
The activator owns the restore/replicate cycle. Your app does not participate in it, does not know it is happening, and needs no hook.
Two consequences worth designing around. A wake can be cut short at any moment, which is why migrations apply one transaction at a time and a killed wake rolls back cleanly and retries. And background work does not survive: a job is a goroutine, and hibernation ends it, so keep jobs idempotent.
Work on a clock
Since hibernation ends goroutines, a timer inside your process is not a
scheduler. The platform keeps the clock instead: you declare a schedule
from outside the app with carlos schedule set -name sync -every 6h -path /jobs/sync, and when it comes due the platform wakes the instance
and POSTs to that path.
Your side is an ordinary handler, guarded so that a stranger POSTing the same path gets nothing:
func handleSync(w http.ResponseWriter, r *http.Request) {
if !carlos.Tick(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if err := syncer.RunOnce(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
Do the work inside the request. The instance stays awake while it is open, and the idle clock starts when you return.
One-off work โ a reminder, an expiry โ you ask for yourself with
carlos.ScheduleAt, and it arrives as the same kind of tick.
carlos is the package, and the reason
TickOccurrence is there: delivery is at-least-once, so a retry brings
the same occurrence back.
The middleware seam
Options.Wrap is where your app middleware goes โ sessions, CSRF, panic
pages, authorization:
opts.Wrap = func(next http.Handler) http.Handler {
return sess.Middleware(next)
}
It runs inside the framework's chrome. GET /healthz and
GET /api/version are answered outside it, so platform probes never
traverse your middleware and a broken authorization layer cannot make
your app look dead. Locale-prefix stripping happens before it, so your
middleware sees the same paths your routes match on.
Returning nil from Wrap is a boot error, not a silent pass-through.
rastrillo.Handler is Serve minus the listener, for test harnesses โ
see Testing.
Your first deploy with migrations
It has to be schema-neutral, and this is the rule most likely to bite you.
If your app is already deployed and you are introducing migrations for
the first time, generate 0001_init from the models as already deployed
and ship it alone. Change a model only in a later release.
Otherwise boot refuses on the new column, and baseline โ the tool you
would reach for under pressure โ would strand that migration for good.
Migrations
has the detail.
Shipping
examples/helloworld is a real scaffolded app, checked in, proven to
ship, promote and serve through the actual carlos binary.
hack/local-deploy-demo.sh in the framework repo runs the sequence.
The platform's own documentation owns the ship/promote commands and
their flags. What matters from this side is that a Rastrillo binary is
an ordinary CARLOS app: no wrapper, no init container, no
platform-specific build.
Read this page as markdown โ exact, unstyled, and cheap for an agent to fetch.