Skip to content
carameldocs · 0.16.0
Browse documentation
Guides / Production

From local to live.

Build the release artifact, set the variables a compiled application needs, and run its process roles under the contract any deploy tool can rely on.

ContractValue
Release0.15.0
NeedsA committed application tree, a container engine such as Docker Desktop for the build, and a Linux host with PostgreSQL for the run
You will haveA linux/amd64 artifact that holds bin/SHARD and public/, migrated, serving on a Unix socket and answering GET /health
StatusThe build, the role script, migrate, serve --no-workers, GET /health and SIGTERM run in scripts/check frappe-project when a container engine runs; jobs unknown and the proxy are not run by a check
Proved byfrappe build · APP migrate · curl --unix-socket

A compiled application promises any deploy tool the behaviour below, as ADR 0032 ↗ records. SHARD is the application’s name in shard.yml, such as bookshelf; APP below stands for the unpacked bin/SHARD, such as /srv/bookshelf/bin/bookshelf.

Stage 1 · Build

Build the artifact

Terminal · inside the application
frappe build

Expect: Prints the path of .caramel/build/SHARD-YYYYmmddTHHMMSSZ-SHA7-linux-amd64.tar.gz as its only line of output. The archive holds bin/SHARD and public/.

frappe build publishes app/assets into public/, then compiles the committed tree with crystal build --release --static in the pinned crystallang/crystal:1.21.1-alpine image for linux/amd64, keeping debug info. Alpine’s gmp-static is installed first, so an application that uses BigDecimal links GMP into the binary. It needs a container engine, such as Docker Desktop, for this build only.

ConditionOutcome
frappe build with uncommitted changes in the working treerefuses to build
frappe build when publishing app/assets into public/ leaves changesrefuses to build

Serve public files

serve reads public/ from CARAMEL_ROOT when it is set, so the binary runs from wherever the artifact is unpacked.

ConditionOutcome
CARAMEL_ROOT that is not an absolute pathexits 1 with CARAMEL_ROOT must be an absolute path
CARAMEL_ROOT with no public/ inside itexits 1 with CARAMEL_ROOT has no public directory

Stage 2 · Run

Set the environment

With CARAMEL_ENV unset the binary runs as production. These variables are read when a process starts:

VariableMeaning
DATABASE_URLThe runtime role’s connection, for serve, work, seed and jobs; a missing one exits 1 with Missing database configuration: DATABASE_URL
MIGRATION_DATABASE_URLThe migration role’s connection, for migrate
APP_ORIGINThe public origin, such as https://bookshelf.example; serve exits 1 with APP_ORIGIN is required without it
APP_SECRETThe secret that signs sessions and tokens; serve exits 1 with APP_SECRET is required without it
CARAMEL_SOCKETThe Unix socket path serve listens on, in a directory only its owner can enter; serve exits 1 with CARAMEL_SOCKET is required; use frappe dev without it, and with CARAMEL_SOCKET must be in a private owned directory when the directory is open
CARAMEL_SOCKET_GROUPA group that may enter the socket’s directory instead, for a proxy that runs as another user; the socket is then mode 0660
CARAMEL_ROOTThe absolute path of the unpacked artifact, holding public/

The secret’s minimum length is on the security reference, and the database timeouts and Crema’s ops socket on the Crema reference.

Read the security limits →Read the Crema variables →

Run the process roles

A compiled application can serve HTTP or run Cold Brew with work, including queue selection, concurrency, and graceful completion of in-flight jobs. Run each web process as serve --no-workers, which keeps the PubSub broker and maintenance but runs no jobs or schedules, and run one work process.

Terminal · on the server
APP serve --no-workers

Expect: Prints TITLE is ready at APP_ORIGIN once the socket is bound, and serves until SIGTERM.

Read the serve and work flags →

Check readiness

Each process binds a socket path of its own, and the proxy moves to it once it is ready. A generated application routes GET /health to an action that touches no database; an application that removes the route has no readiness check.

Terminal · on the server
curl --unix-socket "$CARAMEL_SOCKET" -H "Host: bookshelf.example" http://localhost/health

Expect: Prints ok. The Host is the authority of APP_ORIGIN; another host answers 421.

Shut down gracefully

ConditionOutcome
SIGTERMstops serve accepting connections; it answers in-flight requests with Connection: close and exits when they finish or after 25 s
An open server-sent events streamcounts as in flight, so it holds serve for the whole 25 s
A supervisor that stops servemust allow at least 30 s before it kills the process

Stage 3 · Cut over

Check for unknown jobs

Before cutover, check the new binary against the queue.

Terminal · on the server
APP jobs unknown

Expect: Prints nothing and exits 0 when every queued class is known; otherwise it prints each class the binary lacks, one per line, and exits 1.

Set up the database

Terminal · on the server
APP db roles DATABASE MIGRATION_ROLE RUNTIME_ROLE

Expect: Prints, without passwords, the psql script that creates a migration role that owns the database and a runtime role with DML through default privileges.

Connect with sslmode=verify-full, plus sslrootcert for a private CA, or through a Unix socket, directly or through a session-mode pooler, never a transaction-mode one. The application needs no superuser and no extension. Each connection sets a statement and a lock timeout when it opens. Read the timeout variables →

Cut over in order

  1. Run the new binary’s migrate while the old one serves.
  2. Stop the old workers.
  3. Move traffic to the new web process.
Terminal · on the server
APP migrate

Expect: Prints Applied N migrations. with MIGRATION_DATABASE_URL set, then a warning for each queued job class this binary lacks.

ConditionOutcome
serve, work or seed with a migration not yet appliedexits 1 with Pending migrations. Run frappe migrate first.

Going back across a migration means restoring the database.

Stage 4 · Limits

What does not compile

Nothing on this page is checked at compile time: the artifact is the application as frappe check passed it. Each refusal above happens when the command runs, before it binds a socket or writes a row.

Not covered yet

  • Caramel ships no command that provisions servers, installs a proxy or deploys.
  • Artifacts are linux/amd64 only; arm64 artifacts are deferred.
  • Assets are published as files under public/; embedded and fingerprinted assets are deferred.

Prove it

Check the artifact before cutover

On the build host:

Terminal · inside the application
frappe build

On the server, with the artifact unpacked at /srv/bookshelf and the variables above exported:

Terminal · on the server
/srv/bookshelf/bin/bookshelf migrate
/srv/bookshelf/bin/bookshelf jobs unknown
/srv/bookshelf/bin/bookshelf serve --no-workers &
curl --unix-socket "$CARAMEL_SOCKET" -H "Host: bookshelf.example" http://localhost/health

Passing: frappe build prints one artifact path. migrate prints Applied N migrations., jobs unknown prints nothing and exits 0, serve prints Bookshelf is ready at https://bookshelf.example, and curl prints ok. scripts/check frappe-project runs the build, the role script, migrate, serve --no-workers, GET /health and SIGTERM in a container when an engine is running, and prints SKIP: frappe build needs a running container engine otherwise.