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.

| Contract | Value |
| --- | --- |
| Release | 0.15.0 |
| Needs | A committed application tree, a container engine such as Docker Desktop for the build, and a Linux host with PostgreSQL for the run |
| You will have | A `linux/amd64` artifact that holds `bin/SHARD` and `public/`, migrated, serving on a Unix socket and answering `GET /health` |
| Status | The 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 by | `frappe build` · `APP migrate` · `curl --unix-socket` |

A compiled application promises any deploy tool the behaviour below, as [ADR 0032 ↗](https://github.com/caramelizedev/caramel/blob/v0.16.0/docs/decisions/0032-application-production-contract.md) 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`.

## The guide in four stages {#c-deploy-map}

11 sections · read in order the first time

- **1 · Build**

  - [Build the artifact](#c-deploy-contract)
  - [Serve public files](#c-deploy-public)
- **2 · Run**

  - [Set the environment](#c-deploy-env)
  - [Run the process roles](#c-deploy-now)
  - [Check readiness](#c-deploy-ready)
  - [Shut down gracefully](#c-deploy-shutdown)
- **3 · Cut over**

  - [Check for unknown jobs](#c-deploy-unknown)
  - [Set up the database](#c-deploy-database)
  - [Cut over in order](#c-deploy-order)
- **4 · Limits**

  - [What does not compile](#c-deploy-refusals)
  - [Not covered yet](#c-deploy-gaps)

Stage 1 · Build

## Build the artifact {#c-deploy-contract}

**Terminal · inside the application**

```bash
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.

| Condition | Outcome |
| --- | --- |
| `frappe build` with uncommitted changes in the working tree | refuses to build |
| `frappe build` when publishing `app/assets` into `public/` leaves changes | refuses to build |

## Serve public files {#c-deploy-public}

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

| Condition | Outcome |
| --- | --- |
| `CARAMEL_ROOT` that is not an absolute path | exits 1 with `CARAMEL_ROOT must be an absolute path` |
| `CARAMEL_ROOT` with no `public/` inside it | exits 1 with `CARAMEL_ROOT has no public directory` |

Stage 2 · Run

## Set the environment {#c-deploy-env}

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

| Variable | Meaning |
| --- | --- |
| `DATABASE_URL` | The runtime role’s connection, for `serve`, `work`, `seed` and `jobs`; a missing one exits 1 with `Missing database configuration: DATABASE_URL` |
| `MIGRATION_DATABASE_URL` | The migration role’s connection, for `migrate` |
| `APP_ORIGIN` | The public origin, such as `https://bookshelf.example`; `serve` exits 1 with `APP_ORIGIN is required` without it |
| `APP_SECRET` | The secret that signs sessions and tokens; `serve` exits 1 with `APP_SECRET is required` without it |
| `CARAMEL_SOCKET` | The 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_GROUP` | A group that may enter the socket’s directory instead, for a proxy that runs as another user; the socket is then mode 0660 |
| `CARAMEL_ROOT` | The 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 →](https://caramelize.dev/docs/0.16.0/security/index.md)[Read the Crema variables →](https://caramelize.dev/docs/0.16.0/crema/index.md)

## Run the process roles {#c-deploy-now}

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**

```bash
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 →](https://caramelize.dev/docs/0.16.0/commands/index.md)

## Check readiness {#c-deploy-ready}

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**

```bash
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 {#c-deploy-shutdown}

| Condition | Outcome |
| --- | --- |
| SIGTERM | stops `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 stream | counts as in flight, so it holds `serve` for the whole 25 s |
| A supervisor that stops `serve` | must allow at least 30 s before it kills the process |

Stage 3 · Cut over

## Check for unknown jobs {#c-deploy-unknown}

Before cutover, check the new binary against the queue.

**Terminal · on the server**

```bash
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 {#c-deploy-database}

**Terminal · on the server**

```bash
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 →](https://caramelize.dev/docs/0.16.0/crema/index.md)

## Cut over in order {#c-deploy-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**

```bash
APP migrate
```

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

| Condition | Outcome |
| --- | --- |
| `serve`, `work` or `seed` with a migration not yet applied | exits 1 with `Pending migrations. Run frappe migrate first.` |

Going back across a migration means restoring the database.

Stage 4 · Limits

## What does not compile {#c-deploy-refusals}

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 {#c-deploy-gaps}

- 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 {#c-deploy-prove}

On the build host:

**Terminal · inside the application**

```bash
frappe build
```

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

**Terminal · on the server**

```bash
/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.

[← Work with an agent](https://caramelize.dev/docs/0.16.0/agents/index.md) · [Review worker process options →](https://caramelize.dev/cookbook/0.16.0/background-jobs/index.md)

Caramel 0.16.0 · https://caramelize.dev/docs/0.16.0/deployment/
