You are here:Run time dist/
What runs in production
WhyKnowing what the binary does, and what it leaves to others, decides the rest of the setup.
cybertrain build compiles the framework, your controllers and models and the template interpreter into dist/blog, with every view embedded, and copies public/ beside it. Nothing else is installed where it runs: no Ruby, no gems.
browser ──HTTPS──▶ reverse proxy (kamal-proxy or Caddy)
└─ HTTP/1.1 ──▶ dist/blog (port 3000)
├─ public/ static files, 404.html, 500.html
└─ storage/production.sqlite3
- It links the system’s SQLite and libc, so build it on the same OS and CPU as the machine that runs it. Building inside a Docker image, as the Kamal setup does, takes care of that.
- It speaks plain HTTP/1.1 only. TLS and HTTP/2 are the reverse proxy’s job. In production the session cookie is
Secure, so the site must be served over HTTPS or forms fail withInvalid authenticity token. - The database is one SQLite file. Run one process on one server, and keep the file on storage that outlives a deploy.
- It is production by default and refuses to start without
CYBERTRAIN_SECRET_KEY_BASE../blog migrateapplies the migrations compiled into it.
Two ways to deploy
WhyBoth end with HTTPS in front of the same binary; they differ in where it is built and how a deploy swaps it.
| Kamal | systemd and Caddy | |
|---|---|---|
| Built | In a Docker image, on your machine or a remote builder | On the server, from a git checkout |
| In front | kamal-proxy, with a Let’s Encrypt certificate | Caddy, with a Let’s Encrypt certificate |
| Static files | The app serves public/ | Caddy serves dist/public/ itself |
| A deploy | The new container takes traffic once its health check passes: nothing is refused | A restart: a few seconds of Caddy’s 500.html |
| Rolling back | kamal rollback VERSION | Point current at an earlier release and restart |
| The server needs | SSH and Docker (kamal setup installs it), plus an image registry | Ruby, a C toolchain, the SQLite headers, the cybertrain CLI and Spinel |
Either way, the app is one process with its SQLite file on one server.
Deploy with Kamal
Whycybertrain new already wrote the files; you fill in a server, a domain and a registry.
Dockerfilean Ubuntu build stage runscybertrain setupandcybertrain build; a slim stage of the same release getsdist/and the libraries it linksconfig/deploy.ymlthe server, the domain, kamal-proxy with TLS on port 3000, the health check and the database volume.kamal/secretsreadsKAMAL_REGISTRY_PASSWORDandCYBERTRAIN_SECRET_KEY_BASEfrom your environment, never the valuespublic/up.htmlthe page kamal-proxy polls before it sends traffic to a new container
Fill in the values marked CHANGE ME in config/deploy.yml, commit, then:
export KAMAL_REGISTRY_PASSWORD=...
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)" # once; keep it
kamal setup # first time: Docker and kamal-proxy on the server, build, push, start
kamal deploy # every deploy after that
- Each deploy builds the image (Spinel stays in Docker’s layer cache after the first), pushes it, and starts a container that runs
./blog migrateand thenexec ./blogon0.0.0.0:3000. - kamal-proxy waits until
/up.htmlanswers 200, switches traffic to the new container, then stops the old one, which finishes its requests in flight onSIGTERM. - The database lives on the volume
blog_storage, mounted at/app/storage, so it survives every deploy. - Set
builder.archto the server’s CPU. Spinel compiles inside the image, so building another architecture under emulation is slow; build on the server withbuilder.remoteinstead.
The step-by-step guide, with backups of the volume, is docs/deploy-kamal.md. examples/blog carries the same setup, and CI builds and smoke-tests its image.
Behind Caddy, under systemd
WhyNo Docker: the server builds the app from git, systemd keeps it running, Caddy faces the internet.
- The app listens on
127.0.0.1:3000only, so nothing reaches it except through Caddy. systemd starts it withdist/as its working directory, sincepublic/andstorage/are resolved from there, and reads its secret from an environment file outside the repository. - A deploy script clones each release into its own directory, checks that
gen/is fresh, runscybertrain buildanddist/blog migrate, then points thecurrentlink at it, restarts the service and waits forup.htmlto answer; if it does not, the script switches back to the previous release. A failure before the switch leaves the running release untouched. Going back does not undo migrations, so they must stay compatible with the previous release; a failed first deploy has nothing to go back to and only exits with an error. - Caddy gets the certificate, redirects HTTP to HTTPS, serves the files in
dist/public/itself and proxies everything else to the app. While the app restarts, it answers with500.html.
blog.example.com {
encode zstd gzip
root * /srv/blog/current/dist/public
@static file
handle @static {
file_server
}
handle {
reverse_proxy 127.0.0.1:3000
}
handle_errors {
rewrite * /500.html
file_server
}
}
The app adds X-Content-Type-Options and X-Frame-Options to what it serves; files Caddy serves from dist/public/ do not pass through the app, so add them in the site block if you want them there too. nginx works the same way: terminate TLS, serve dist/public/, proxy the rest.
The full walkthrough, from a fresh Ubuntu server to backups and rollbacks, is docs/deploy.en.md (the Japanese original is docs/deploy.md).
Configure it
WhyProduction settings come from the environment, so the same binary runs anywhere.
| Variable | In production |
|---|---|
CYBERTRAIN_SECRET_KEY_BASE | Required. Signs the session cookie; changing it signs everyone out. Generate it once with openssl rand -hex 32 |
CYBERTRAIN_DATABASE | The SQLite file; storage/production.sqlite3 under the working directory by default. Keep it outside the release |
CYBERTRAIN_HOST | 127.0.0.1 by default, right behind a proxy on the same machine; 0.0.0.0 in a container |
PORT | 3000 by default; what the proxy forwards to |
CYBERTRAIN_ENV | production already in a built binary; development would start file watching and rebuilds |
The tutorial’s Build for deployment step lists every variable and its config/app.rb attribute.
Run it day to day
- Every change is a deploy. Views are embedded at build time, so even a view-only change needs a rebuild and a restart. Commit
gen/with the change: the deploy script stops on a stalegen/. - Migrations run before the new code serves: in the deploy script, or when the container starts. Remove a column in a later deploy, once no running code reads it.
- Back up with SQLite’s
.backup, not by copying the file: it takes a consistent copy while the app writes. - Rolling back past a migration means rolling the database back too, with
db rollbackfrom the newer release, before you switch. - Stopping is graceful: on
SIGTERMthe server stops accepting, finishes the requests in flight and exits, after 10 seconds at most.