# alf.toml

A project written down beside its code: its services and their settings, its databases, buckets and computers. A deploy from a directory with an `alf.toml` makes the project match it, then deploys the code, whether the deploy comes from `alf deploy`, [`deploy.sh`](/docs/deploying-code) in CI, or a [coding agent](/docs/agents).

Its first name, `smartalfred.toml`, still works wherever the file is read: a directory that has one keeps it, and `alf.toml` wins if both are there.

```toml
project = "shop"
organization = "acme"

[services.web]
dockerfile = "Dockerfile"
port = 3000
release_command = "npx prisma migrate deploy"
size = "m"
sleep = "10m"

[services.web.env]
NODE_ENV = "production"
DATABASE_URL = "${{postgres.DATABASE_URL}}"
S3_BUCKET = "${{uploads.S3_BUCKET}}"

[services.web.secrets]
AUTH_SECRET = "generate"
STRIPE_SECRET_KEY = "ask"

[[services.web.cron]]
name = "nightly report"
schedule = "0 3 * * *"
command = "node scripts/report.js"

[services.web.alerts]
error_rate = "5%"
down = true

[services.worker]
dockerfile = "worker/Dockerfile"
public = false

[databases.postgres]

[buckets.uploads]
```

[`alf launch`](/docs/launch) writes one for you. For a project you made in the dashboard, `alf config shop --save` writes it out as it is.

## What a deploy does with it

- **What's missing is made**: the services, databases, buckets and computers the file names that the project hasn't got, with the settings it gives them.
- **The file sets what it says.** A setting it names is set at every deploy. A setting it doesn't name is left as it is, so the dashboard is still the place for anything you'd rather not keep in a file.
- **Nothing changed in the dashboard is undone by accident.** If somebody changed a setting the file names in the dashboard since the file last set it (more memory in a hurry, a variable fixed by hand, a scheduled command moved or removed, an alert's threshold, the Dockerfile's path), the deploy doesn't quietly put the file's value back. `alf deploy` asks which to keep; keeping the dashboard's writes it into alf.toml, so the file says what runs. Where nobody can be asked (CI, an agent), the dashboard's value stays and the deploy says so. `--config-wins` puts the file's back instead. Changing the file alone is no conflict: that's what deploying it is for.
- **Nothing is deleted.** A service, variable, volume or scheduled command that isn't in the file (any more) stays until you remove it in the dashboard. The deploy says what the project has that the file hasn't.

Before anything is uploaded, the deploy prints its plan: `+` for what's made, `~` for what changes (with each setting's old and new value), `=` for what stays. `alf deploy --plan` (or `deploy.sh --plan`) prints it and stops.

## The project

| Setting | |
| --- | --- |
| `project` | The project's address part, like `shop`. What `alf deploy` deploys to; a deploy with a link or a token for one project goes to that one. |
| `organization` | Its organization, for people in more than one. |

## Services

Each `[services.<name>]` is one service; its name is its address part (lowercase letters, digits and hyphens), which the rest of the project reaches it by.

| Setting | | Example |
| --- | --- | --- |
| `dockerfile` | Built from your code, with this Dockerfile. The default for a service without `image`. | `"api/Dockerfile"` |
| `context` | The directory it's built in. Default: the Dockerfile's. | `"."` |
| `image` | Runs a published image instead. | `"redis:7"`, `"ghcr.io/you/app:1.4"` |
| `static` | A [static site](/docs/static-sites): the folder of the image that holds it (or `true` for its `LABEL`). | `"/site"` |
| `spa` | For a static site: paths that aren't files get `index.html`. | `true` |
| `name` | Its name on the canvas. Default: the table's. | `"Shop API"` |
| `port` | The port it listens on (`$PORT` is set to it). | `3000` |
| `public` | An address on the internet (`true`, the default) or only [inside the project](/docs/networking). | `false` |
| `command`, `entrypoint` | Instead of the image's. A line, or a list. | `"node dist/server.js"` |
| `release_command` | Runs once per deploy, before the new version takes over: see [release commands](/docs/deployments#release-commands). | `"bin/rails db:migrate"` |
| `health_path` | A path that has to answer before a new version takes over. | `"/health"` |
| `zero_downtime` | Start the new version beside the old one (`true`), or stop the old first. | `false` |
| `size` | Its [size](/docs/usage#sizes): memory and the cores that come with it, billed by the hour awake. Changed live, with no restart. | `"s"`, `"xl"` |
| `memory`, `cpus` | Its size said as memory or cores instead: the smallest size with at least so much. | `"2GB"`, `4` |
| `sleep` | How long without visitors before it [sleeps](/docs/sleeping); `"never"` keeps it up once started. A private service always runs unless it has one; then calls from the project's own services wake it. | `"10m"`, `"1h"` |
| `always_on` | Started straight away and never asleep. | `true` |
| `instances` | How many servers it runs on. | `2` |
| `regions` | Where it may run. | `["Europe"]` |
| `max_replicas`, `target_rps` | [Scaling](/docs/scaling) on a server under load. | `4`, `50` |
| `domains` | [Your own domains](/docs/domains), added if missing; the deploy says where to point them. | `["shop.example.com"]` |

Sizes are `xs`, `s`, `m`, `l`, `xl`, `2xl` (Hobby's largest), `3xl`, `4xl` and `5xl`; amounts are `"512MB"` or `"1GB"` (a plain number is megabytes for memory, gigabytes for disks); durations are `"30s"`, `"10m"`, `"2h"`, `"1d"`.

### Variables

`[services.<name>.env]` sets [variables](/docs/variables-and-secrets). A value may refer to another part of the project, and Alf fills it in and keeps it up to date: `${{postgres.DATABASE_URL}}`, `${{uploads.S3_SECRET_ACCESS_KEY}}`, `${{api.URL}}`. A reference to something that's neither in the project nor in the file is refused before anything is uploaded.

### Secrets

The file is committed with your code, so a secret's value is never in it. `[services.<name>.secrets]` says which secrets a service has, and how each comes about:

```toml
[services.web.secrets]
SESSION_SECRET = "generate"        # 64 random letters and digits, made once and kept
APP_KEY = "generate:32"            # …of this length
ENCRYPTION_KEY = { generate = 64, hex = true }      # hex digits, for keys read as bytes
PUBLIC_KEY = { generate = 32, prefix = "pk-" }      # for keys that have to start a certain way
STRIPE_SECRET_KEY = "ask"          # you give it
```

A generated secret is made when it's missing and never changed after. For `"ask"`, `alf deploy` asks for the value the first time (it isn't shown, and goes straight to Alf), or set it in the dashboard under **Variables**; a deploy says which ones still have no value. A value written in the file instead is refused, with the reason.

### Volumes

```toml
[[services.web.volumes]]
name = "uploads"
path = "/app/uploads"
size = "20GB"         # its size, billed full or not (default 1GB)
grow_up_to = "100GB"  # grow by itself as it fills, up to this (default: it doesn't)
```

Added where missing, and set to `size` at every deploy: bigger, or smaller as long as it holds less than that (otherwise it stays as it is, and the deploy says so). One that [grew by itself](/docs/volumes#growing-by-itself) past `size` keeps what it grew to. A deploy never moves or removes a volume. Its size changed in the dashboard since the file set it is asked about. A service with a volume runs on one server.

### Scheduled commands

```toml
[[services.web.cron]]
name = "cleanup"
schedule = "0 3 * * *"     # UTC, five fields, or @daily and friends
command = "node scripts/cleanup.js"
timeout = "30m"             # default 15 minutes
enabled = true
```

Made or changed by name; one that isn't in the file is left as it is. One changed or removed in the dashboard since the file set it is [asked about](#what-a-deploy-does-with-it), not put back. As in the dashboard, [scheduled commands](/docs/deployments#running-something-on-a-schedule) are an admin's or an owner's to change: a deploy by somebody else leaves them as they are, and says so.

### Alerts

```toml
[services.web.alerts]
error_rate = "5%"            # of requests answered with a 5xx
response_time = "3s for 10m" # the slowest 5% of requests take longer
memory = "90%"               # of its memory
down = true                  # crashed, failing its health check, or its volume is full
```

Each says when to [tell you](/docs/alerts): past that for the whole window, five minutes unless it says `for 10m` (1 to 60 minutes; `down = "for 2m"` for being down). `true` takes the usual threshold (5%, 3s, 90%), `false` turns that alert off. Made where the service hasn't got one, and set as the file says; alerts it doesn't name are left as they are, and so are the organization's alerts on every service. One changed or turned off in the dashboard since the file set it is [asked about](#what-a-deploy-does-with-it). Like scheduled commands, alerts are an admin's or an owner's to change.

## Previews

```toml
[previews]
seed = "npm run db:seed"   # run once in a new preview, after its app is first live
service = "web"            # where (default: the service with a release command)
```

A [preview](/docs/previews)'s databases start empty. `seed` gives it data to click through: it runs once in each new preview, in a fresh container of `service` with its variables, after that service is first live (so after its release command's migrations). Left out, `service` is the one with a release command, else the only one built from code. Previews made later from the dashboard, the API or CI get it too; one made by a push of a branch whose file adds it gets the branch's. `alf previews` shows how each seed went.

## Databases

```toml
[databases.postgres]          # named after its engine: that's all it needs

[databases.cache]
engine = "redis"
size = "s"
```

| Setting | |
| --- | --- |
| `engine` | `postgres`, `mysql`, `redis` or `mongodb`. Not needed when the name is one of those. |
| `version` | Its major version, when it's made (`"16"`). A deploy doesn't change the version of a database that's there. |
| `size` | Its size (or `memory`): xs for Postgres and Redis, m for MySQL and MongoDB unless it says. Changed live, with no restart. |
| `disk` | Its disk's size: 1 GB for Postgres and Redis, 2 GB for MySQL and MongoDB unless it says. Billed by its size. |
| `regions`, `name` | Where it runs, when it's made; its name on the canvas. |

A database is [made the way the dashboard makes one](/docs/databases): private, always on, with a daily snapshot. Apps reach it with references, like `${{postgres.DATABASE_URL}}` (`${{cache.REDIS_URL}}` for Redis).

## Buckets

```toml
[buckets.uploads]
```

A [bucket](/docs/buckets), made if missing. Apps reach it with `${{uploads.S3_ENDPOINT}}`, `${{uploads.S3_BUCKET}}`, `${{uploads.S3_REGION}}`, `${{uploads.S3_ACCESS_KEY_ID}}` and `${{uploads.S3_SECRET_ACCESS_KEY}}`.

## Computers

```toml
[computers.dev]
system = "claude"     # claude, debian, ubuntu or fedora
size = "xl"           # up to 3xl; m unless it says
disk = "20GB"         # /workspace's size
idle = "1h"           # switched off after this long with no shell open ("never": kept on)
```

A [computer](/docs/computers) in the project, made if missing. Somebody may be working in it, so a deploy leaves a computer that's there as it is: its system, size and disk are changed in the dashboard. `idle` is set at every deploy.

## Compose files

With an `alf.toml` that lists services, those are the services: a docker-compose file beside it isn't read. Without services, the file adds its databases, buckets and computers to the [compose file's](/docs/compose) services.

## One-click deploys

The [one-click deploys](/docs/one-click) are alf.toml files too, with a `[template]` table for the catalog: what it's called, and what its form asks for. Such a file is deployed from the one-click page; a deploy of your own refuses it until `[template]` is taken out.

## Paths and monorepos

The file's paths start where the file is. In a monorepo, put it in the app's directory: `dockerfile = "Dockerfile"` means that directory's, and `context = "../.."` builds from the repository's root. Everything in the repository is sent either way, as with any deploy.
