CARLOS rastrillo docs

🤖 Hosting elsewhere

A Rastrillo app is one static Go binary, a SQLite file and, if it stores files, an S3-compatible bucket. Anything that keeps one process running and gives it a persistent disk for the SQLite file can host it: a server of your own, a container platform, a Kubernetes cluster. The binary is the same one CARLOS runs, and it needs no code changes.

Deploying covers CARLOS. This page covers everywhere else.

What CARLOS did that you now do

On CARLOS the platform handles these. Elsewhere, each is yours to set up or go without:

Some CARLOS features do not come with the app: hibernation, carlos logs, edge analytics, the edge's protection and rate limits, canaries and channels. Your host may offer its own. Without hibernation the process stays up, so background jobs run until the next restart or deploy. Try a change on a second copy of the app, with its own port and database, before you replace the live binary.

Where the app listens

rastrillo.Resolve and rastrillo.Serve pick the first of these that applies:

  1. A socket systemd hands over (LISTEN_FDS).
  2. -socket path, a unix socket.
  3. -addr host:port, or Options.Addr set in main.go.
  4. :$PORT, when the PORT variable is set. Hosts such as Render and Railway set it.
  5. :8080.

PORT must be a number from 1 to 65535. Anything else stops the app with an error that names the value, rather than leaving it on 8080 where the host's health check would never find it. PORT is ignored when an earlier rule applies, so -addr always wins. A PORT in your own shell moves go run . too.

:$PORT and :8080 listen on every interface, which is right inside a container. On a server with a proxy in front, pass -addr 127.0.0.1:8080 so the app is not reachable around the proxy.

-db path names the database file. When systemd sets $STATE_DIRECTORY, Rastrillo resolves a relative path inside it.

Point the host's health check at GET /healthz. On SIGTERM the app stops accepting requests and waits up to 10 seconds for open ones to finish.

Environment variables

<APP> is the app's Go package name in capitals, as main.go reads it: an app called notes reads NOTES_ORIGIN.

Variable What it is When it is unset
<APP>_ORIGIN The public https:// address. The CSRF check and emailed sign-in links use it. http://localhost:8080, with a warning. Emailed links point there, and the CSRF check compares posts with the wrong address.
<APP>_INSTANCE_KEY A long random secret, for apps with sign-in. Keep it out of the repository. A new key at every start, so pending sign-in links stop working after a restart.
<APP>_SMTP_HOST, <APP>_SMTP_FROM Your SMTP relay and the sender address. Both are required to send. Mail is written to the log, not sent.
<APP>_SMTP_PORT, <APP>_SMTP_USER, <APP>_SMTP_PASS The relay's port, username and password.
CARLOS_STORE_BUCKET, _REGION, _ENDPOINT, _ACCESS_KEY, _SECRET_KEY Your S3-compatible bucket, for an app that stores files with blobs.S3FromEnv. No bucket: S3FromEnv returns no store.
CARLOS_ADMIN_TOKEN A long random secret that scheduled calls send as a bearer token. carlos.Tick refuses every call.

The CARLOS_ names stay the same off CARLOS.

If you run a proxy in front of an app with sign-in, set TrustedProxyHops in its auth.Config to the number of proxies, usually 1. Without it, the per-address sign-in limit counts every visitor as the proxy.

Work on a clock

On CARLOS the platform calls the app's scheduled paths. Elsewhere, cron or a systemd timer does, with the admin token as the bearer:

curl -fsS -X POST \
  -H "Authorization: Bearer $CARLOS_ADMIN_TOKEN" \
  https://myapp.example.com/jobs/sync

The handler is the same one: guarded by carlos.Tick and exempt from the CSRF check with csrf.Exempt, as Deploying shows. Give the cron job the same token the app has.

carlos.ScheduleAt returns carlos.ErrNotOnCarlos off CARLOS. For one-off work, store what is due as rows, and have a regular tick handle the ones whose time has come.

A plain server

Build for the server's processor (most are x86) with the scaffold's Makefile, which stamps the version from git, so commit first:

make release RELEASE_GOARCH=amd64

Copy releases/myapp-linux-amd64 to the server as /usr/local/bin/myapp, then run it as a systemd service:

# /etc/systemd/system/myapp.service
[Unit]
Description=myapp
After=network-online.target

[Service]
User=myapp
ExecStart=/usr/local/bin/myapp -addr 127.0.0.1:8080 -db myapp.db
EnvironmentFile=/etc/myapp/env
StateDirectory=myapp
Restart=on-failure

[Install]
WantedBy=multi-user.target

StateDirectory makes /var/lib/myapp, and the relative -db puts the database there. /etc/myapp/env holds the variables above, one NAME=value a line, readable only by root.

For socket activation, let systemd hold the port instead. Drop -addr from ExecStart and add a socket unit. A restart then queues new connections rather than refusing them:

# /etc/systemd/system/myapp.socket
[Socket]
ListenStream=127.0.0.1:8080

[Install]
WantedBy=sockets.target

Put a proxy in front for TLS. Caddy fetches and renews the certificate itself:

myapp.example.com {
	reverse_proxy 127.0.0.1:8080
}

For backups, run Litestream as its own service, replicating /var/lib/myapp/myapp.db to a bucket. Rastrillo opens SQLite in WAL mode, which Litestream needs. Its documentation covers the configuration and restoring.

A container image

A static Go binary runs on the distroless static image, which includes CA certificates:

FROM gcr.io/distroless/static
COPY releases/myapp-linux-amd64 /myapp
ENTRYPOINT ["/myapp", "-db", "/data/myapp.db"]

Build the binary with make release RELEASE_GOARCH=amd64 (or arm64 for an ARM host) before docker build. The image passes no -addr, so the app listens on $PORT where the host sets it and on 8080 where it does not.

Mount a persistent volume at /data. Without one, the database is lost on every deploy.

Container platforms

Fly, Render, Railway, Miren and similar hosts run the image as one service. Their limits and plans change, so check the provider's current documentation. What holds across them:

Kubernetes

Run exactly one replica, so two pods never open the same database: a StatefulSet, or a Deployment with the Recreate strategy.

A deploy restarts the one pod, so there is a short gap.

Moving an app off CARLOS

Bring the database file and point the app at the same bucket. If the app seals data with its instance key, keep the key: set <APP>_INSTANCE_KEY to the one it had on CARLOS. Before you move everything, run one copy on the new host to prove the binary and data work there.

Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.