# Alf docs, in full > Every page of https://runalf.com/docs in reading order. The index for agents (how to connect, the model, the tools) is https://runalf.com/llms.txt. # Introduction Alf runs your apps, your databases and Linux computers for you or your coding agent: containers, whole docker-compose stacks, Postgres, MySQL, Redis and MongoDB. Every public service gets an HTTPS address the moment it's deployed, sleeps when nobody uses it and wakes on the next request: asleep it's paused, so it answers almost at once. Busy services scale out on their own. ## What you get - **Deploys from anywhere**: a container image, or your code, built from its Dockerfile: one command deploys a directory from your computer or from CI (GitHub Actions and the like) on every push. - **An address for every service**, like `web-shop-acme.smartalfred.com`, with HTTPS, and your own domains on top. - **Private networking**: a project's services reach each other by name (`db:5432`); nothing outside the project gets in. - **Scale to zero and back**: idle services sleep and cost nothing but disk; asleep they're paused, and a visitor wakes them. - **Autoscaling** across containers and servers, with the closest copy answering visitors. - **Databases in one click**: Postgres, MySQL, Redis or MongoDB, private, with a snapshot every night and a copy off the server. - **Computers**: a Linux machine in your project, with Claude Code ready or an empty Debian, Ubuntu or Fedora; sudo, Docker and SSH. - **Metrics, logs, errors and usage** from the first request, and alerts when something goes wrong. - **Preview environments**: throwaway copies of a project, one per pull request if you like. - **Coding agents** (Claude Code, Codex, opencode and others) that deploy and look after your apps, with approvals and limits you set. ## Where to start - New here? Follow the [Quickstart](/docs/quickstart): it takes a few minutes. - Want the big picture first? Read [How it fits together](/docs/concepts). - Moving an existing app? See [Deploying your code](/docs/deploying-code) or [docker-compose stacks](/docs/compose). - Working with an AI coding agent? Go to [Coding agents](/docs/agents). --- # Quickstart From nothing to a running app at its own address. ## The short way: launch it With the [command line](/docs/cli), in your code's directory: ```bash curl -fsSL https://runalf.com/cli.sh | sh # macOS, Linux alf launch ``` On Windows, install it from PowerShell or the Command Prompt with `powershell -c "irm https://runalf.com/cli.ps1 | iex"`; the rest is the same. `launch` works out what the code is, writes a Dockerfile if it has none, asks once about each database it uses, makes the project and deploys it: see [launching an app](/docs/launch). New here? It signs you in first, in your browser, where you make your account (with GitHub, Google or your email), and when your organization needs a payment method before its first deploy, it opens Stripe's page and waits for it. The rest of this page does the same in the dashboard. ## 1. Create an account [Sign up](/signup) with your email, and open the link we send to confirm it. Next, start an organization (your workspace; you can invite people later under **Members**), or skip that and join your team's when they invite you. ## 2. Create a project A project holds the services of one app, say a website, an API and a database. On the **Overview**, choose **Create → Empty project** and name it. ## 3. Add a service In the project, choose **+ Create**: - **Your code**: run `alf deploy` in your code's directory (the page has the line with the project filled in, or a one-time line with nothing to install). It finds the docker-compose file or the Dockerfile, sends the code, and Alf builds and runs it. `alf ci github` sets up the same on every push. See [Deploying your code](/docs/deploying-code). - **Service → Container image**: any public image, or one the server can pull, e.g. `nginx:1.27` or `ghcr.io/you/app:1.2`. Set the port your app listens on (or leave it empty to use the one the image declares) and choose **Create service**: it's deployed at once. ## 4. Open it The service card on the canvas shows its address. Open it: the first request wakes the app, and everything after that is as fast as usual. ## Next steps - Add [variables and secrets](/docs/variables-and-secrets). - Point [your own domain](/docs/domains) at it. - Add a database as a [private service](/docs/networking). - Turn on [notifications](/docs/notifications) for failed deploys. --- # Launching an app `alf launch` puts the code in a directory on Alf in one go: it works out what the code is, makes the project, adds the databases it uses, writes the two files that describe it, and deploys it. ```bash cd ~/code/shop alf launch ``` ``` Looking at /Users/ada/code/shop A Next.js app. launch writes a Dockerfile for it. It uses Postgres (Prisma's schema (postgresql)). Before each new version takes over: npx prisma migrate deploy (Prisma's migrations). The project's name, in Acme? [shop] This app uses Postgres. Add a Postgres database to the project? (private, backed up every day) [Y/n] ✓ Made the project shop in Acme. ✓ Wrote Dockerfile: how the app is built (an ordinary one; change what you like). ✓ Wrote .dockerignore: what the build leaves out. ✓ Wrote alf.toml: the project as code. Keep it with your code. Project: shop + postgres: new database (postgres:17), private, size xs (256 MB, 0.25 cores), a 1 GB disk, a snapshot every day + web: new service, built from Dockerfile, with an address release command: npx prisma migrate deploy (before each new version takes over) variables: DATABASE_URL (new) secrets: AUTH_SECRET (generated) Sent the code (214 files, 1.2 MB). From now on, alf deploy deploys it again (commit alf.toml and the Dockerfile with your code). ✓ shop is being deployed: https://runalf.com/app/projects/prj_8f2c41 ``` Once the code is sent, the rest (building it and starting it) happens on Alf: launch says the project is being deployed, ends with its link, and is done. The project's page shows the build and when it's live. `--wait` stays until every service is live, and says what went wrong if one isn't; in CI (where `CI` is set) it waits anyway. From then on, `alf deploy` deploys it again. Commit `alf.toml` (and the Dockerfile, if launch wrote one) with your code. You need the [command line](/docs/cli), signed in. Run it without a terminal (an agent, a script) and every question takes its default; `--yes` does the same at a terminal. `--name` says the project's name, `--project` launches into a project you have already instead of making one, `--no-deploy` stops after the files, `--wait` waits for the deploy. ## What it recognises | It finds | It deploys | | --- | --- | | A docker-compose file | Its services, as the file says (see [docker-compose stacks](/docs/compose)). launch writes an `alf.toml` with just the project, for the databases, buckets and settings you add later. | | A `Dockerfile` | That, as one service. launch still looks at the code for its databases and migrations. | | **Node.js**: Next.js, Nuxt, Remix and React Router, SvelteKit, Astro, NestJS, Express, Fastify, Hono and other servers with a `start` script; npm, pnpm, Yarn or Bun, in a monorepo too (see below). The Node.js version is what `.nvmrc` or `engines.node` asks for, else the current LTS. | A service, from a Dockerfile it writes. | | **Static sites**: Vite, Create React App, Vue, Angular, Gatsby, Docusaurus, Eleventy, VitePress, Astro without a server, a Next.js export, Hugo, Jekyll, MkDocs, Blazor WebAssembly, plain HTML | A [static site](/docs/static-sites): built, and its files served from the edge. | | **Python**: Django, FastAPI, Flask, Streamlit, Gradio, or a `Procfile` | A service (gunicorn, uvicorn, or the framework's own server). | | **Ruby**: Rails, Sinatra and other Rack apps | A service. | | **PHP**: Laravel, Symfony, WordPress, plain PHP | A service, served by [FrankenPHP](https://frankenphp.dev). | | **Elixir**: Phoenix, and other Mix projects | A service, built as a release. | | **.NET**: ASP.NET Core, and .NET workers | A service. In a solution, its web project. | | **Java and Kotlin**: Spring Boot, Quarkus, Micronaut, Ktor (Maven or Gradle) | A service. | | **Go**, **Rust** (a Cargo workspace too), **Deno** | A service. | | Bots (discord.js, Telegraf, discord.py, aiogram…) and workers with no web server | A service with no address, which always runs (see below). | The Dockerfile it writes is an ordinary one, commented, with nothing Alf-specific in it: change what you like, and Alf builds what it says. It never writes over a file that's there. ## Monorepos In a JavaScript monorepo (pnpm, npm, Yarn or Bun workspaces, with or without Turborepo), launch deploys one app of it: the one whose directory you run it in, or, from the workspace's root, the app it finds (asking which when there are several; `--app apps/web` says it). Libraries, VS Code and browser extensions, command-line tools and shared settings are left out, and it says so. The app is built from the workspace's root, since it uses the workspace's other packages and the lockfile is there: the Dockerfile goes in the app's directory (`apps/web/Dockerfile`), the `.dockerignore` at the root, and alf.toml says `context` is the root. The whole workspace is installed (with pnpm, its packages are fetched from the lockfile first, so that step is redone only when the lockfile changes), then the app and the packages it uses are built: through Turborepo when the workspace has it (`turbo run build --filter=web...`), otherwise in order. The app runs from its own directory, or, for a static export, its files are served. An app with a Dockerfile of its own is built with that one: from the workspace's root when it copies the workspace in (it names the lockfile, `apps/` or `packages/`, or runs `turbo prune`), otherwise in its directory. To launch another app of the same monorepo as its own project, run launch in its directory. ## Apps side by side A repository that keeps its apps in directories of their own, with nothing to run at its root (`frontend/` and `backend/`, or `apps/web` and `services/api`), is launched as one project with a service for each, called after its directory and built from it. Directories like `docs/`, `scripts/` and `tests/` are left out. Where one app's `.env.example` points at another on a laptop (`VITE_API_URL=http://localhost:8000/api` in the frontend, with the backend answering on 8000), it gets the other's address instead: `VITE_API_URL = "${{backend.URL}}/api"`. ## Workers, schedulers and bots An app's background jobs run beside it, as services of their own: the same code and variables (its secrets by reference, so there's one of each), each with its own command, private and always running. launch finds them in: - a `Procfile`: each line besides `web` is a process of its own (`worker: …`, `clock: …`), and `release` is the release command; - Sidekiq, GoodJob, Delayed Job or Resque in a Ruby app (with Solid Queue, Rails 8's own way: in Puma, with `SOLID_QUEUE_IN_PUMA`); - Celery (its worker, and `beat` where it has a schedule) or RQ in a Python app; - Laravel's queue (`app/Jobs`, or Horizon) and its scheduler; - a `worker` script in `package.json`. An app that uses BullMQ or the like without one is told to add it. A program that serves nothing over HTTP (a Discord or Telegram bot, a queue's consumer, a `Procfile` with no `web` line) has no address and no port, and always runs: it's called `bot` or `worker` instead of `web`. ## Launching into a project you have `--project` launches into a project you have already. Its services keep their names: if it has a `web` already, the app is called after its directory instead (launch asks, at a terminal), so it's a service of its own beside the other one, not a new version of it. ## Databases launch looks at what the code depends on (`pg`, Prisma's schema, `psycopg`, the `pg` gem, `DB_CONNECTION` in Laravel's `.env.example`…) and asks once about each database it finds: Postgres, MySQL, Redis or MongoDB. A yes adds it to the project ([private, with a disk and daily snapshots](/docs/databases)) and gives the app its address as a variable that's kept up to date: `DATABASE_URL = "${{postgres.DATABASE_URL}}"` (Laravel, WordPress, .NET and the Java frameworks get the variables they read instead, like `ConnectionStrings__DefaultConnection`). An app on **SQLite** (Django's default, `better-sqlite3`, Prisma's `sqlite`, a Go or Rust driver) keeps its database file on a [volume](/docs/volumes) at `/data`, so a deploy doesn't start it afresh: launch links the file the code names (`db.sqlite3`, `./data/app.db`) there, and migrates it as the app starts, since a release command can't see the volume. A volume ties the app to one server; a Postgres database doesn't. ## Migrations When the code has migrations launch knows how to run, it sets them as the service's [release command](/docs/deployments#release-commands): run once per deploy, before the new version takes over, so the new code never meets an old database. It recognises a `db:migrate` or `migrate` script, Prisma, Drizzle, Knex, Sequelize, Django, Alembic, Flask-Migrate, Rails, Laravel and Doctrine. ## Secrets and variables Secrets the framework needs are generated (`AUTH_SECRET`, `SECRET_KEY_BASE`, Django's `SECRET_KEY`, Laravel's `APP_KEY`, WordPress's keys and salts…). If the code has a `.env.example`, launch reads it: its own secrets are generated, other people's (API keys, passwords) are asked for at the first deploy, and settings keep their example values unless those only work on your machine. Those, and settings the example leaves empty, are asked for at the terminal (Enter skips one); with nobody there, launch names them so you can `alf env set` them. Variables Alf sets itself, like `PORT`, and switches meant for a laptop, like `APP_DEBUG`, are never taken from it. A Rails app's `config/master.key` can go in as the secret `RAILS_MASTER_KEY`. None of the values is ever written to a file. Variables a frontend's build bakes into the browser's code (`VITE_…`, `NEXT_PUBLIC_…`, `NUXT_PUBLIC_…`, `PUBLIC_…`, `REACT_APP_…`) are given to the build, for a static site and an app that runs alike: the Dockerfile launch writes takes each one the `.env.example` names with an `ARG` line. Changing one builds the app again; a variable you add later needs an `ARG` line of its own. ## A directory with an alf.toml already Run in a directory whose `alf.toml` names a project you don't have (someone else's code, another organization), launch makes that project and deploys it, databases and all. If the project is there, it says so and leaves it to `alf deploy`. --- # How it fits together ```diagram organization An organization, acme, holds projects. Project shop has its own private network with web (public, with an HTTPS address, sleeping when idle), db (private, reached as db:5432, always on) and a computer (a Linux machine with no address of its own). Project blog has another network: its services can't reach shop's. Alf places every service on servers it runs in regions, like Europe (Falkenstein, Nuremberg) and the US, or on private servers only the organization uses; a project's network reaches across the servers it runs on. ``` ## Organizations An organization is your workspace: its projects, servers, members, agents and settings. You can belong to several and switch between them at the top of the sidebar. ## Projects A project groups the services of one app. Its services share a **private network**: they reach each other by name, like `db:5432` or `cache:6379`, even when they run on different servers. Services in other projects can't reach them. Every public service's address contains the project's short name: `{service}-{project}-{organization}.smartalfred.com`. You can change these short names in the project's and organization's settings; the addresses move with them. ## Services A service is one container image (or the image built from your code), the settings it runs with and its addresses. A service can be: - **Public**: it gets an HTTPS address and sleeps when idle (unless you say otherwise). - **Private**: no address on the internet, only `name:port` inside the project. Use this for databases, caches and workers. Private services always run. - **A static site**: built like any other, but its files are served by Alf's edge, and nothing of it runs. See [Static sites](/docs/static-sites). Click a service on the canvas to open its page. Its parts are listed in the sidebar: **Overview** (how it's doing, with its charts), **Deployments**, **Logs** and **Terminal**, then its settings: **General**, **Variables**, **Networking** (addresses, domains, public ports), **Scaling** (where it runs, its size, sleep), **Volumes** and **Schedules**. The name of the project at the top of the sidebar takes you back to the canvas. Above the cards, **From the internet** shows how each service is reached from outside the project, with a line down into it: its own address, the [domains](/docs/domains) you gave it (and whether each is live yet), its [public TCP and UDP ports](/docs/tcp-ports) and who may connect to them, and the ports a computer shares. A service that isn't there can only be reached from inside the project. Lines move while there's something awake to answer, and between services when one has been seen calling the other lately. ## Deployments Every change to what runs (a new image, pushed code, changed variables) is a deployment. You can follow its build log, and [roll back](/docs/deployments) to an earlier one. ## Servers Servers are the machines your services run on. Alf runs them, in several regions: it places each service on one or more online servers, spreads them over regions when it can, and moves them if a server goes away (services with a volume stay where their data is). An organization can also have private servers that only it runs on. See [Servers and regions](/docs/servers). ## Visitors A visitor's request reaches Alf's edge first, then the nearest server that runs the service, over a connection the server keeps open. If the service is asleep, the request waits while it starts, then gets its answer. --- # The command line `alf` (short for `smartalfred`; either name works) is the dashboard in your terminal: putting your code on Alf and deploying it, your services and computers, how they are, their logs, restarting them, and a shell inside them. For macOS, Linux and Windows. ```bash curl -fsSL https://runalf.com/cli.sh | sh alf login alf launch # in your code's directory: a project, its databases, and the first deploy alf services ``` ## Installing it On macOS and Linux: ```bash curl -fsSL https://runalf.com/cli.sh | sh ``` It goes in `/usr/local/bin` where you can write there, and in `~/.local/bin` otherwise — no `sudo` either way (if `~/.local/bin` isn't on your `PATH` yet, it says which line to add). On Windows, in PowerShell or the Command Prompt: ```powershell powershell -c "irm https://runalf.com/cli.ps1 | iex" ``` It goes in `%LOCALAPPDATA%\Programs\smartalfred`, no administrator needed, and onto your `PATH`: open a new terminal afterwards. Everything works there but [`connect --all`](#your-machine-in-a-project-s-network), which needs a network interface of its own: in [WSL](https://learn.microsoft.com/windows/wsl/install) it works (installed with the macOS and Linux line), or give the WireGuard app for Windows the file `connect --config` writes. Either way the download's checksum is checked before anything is installed, and nothing else is changed. Beside it goes its short name, `alf`: `alf deploy` is `smartalfred deploy`, `alf logs web` is `smartalfred logs web`. It's left out where something else is called `alf` already: then type `smartalfred` wherever these docs say `alf`. An install from before it had one gets it with `smartalfred update`. `alf update` installs the newest version later; so does running the line again. ### Tab completion ```bash alf completion --install # asks, then adds it to ~/.zshrc, ~/.bashrc or fish's completions ``` Commands, their second words (`env set`, `domains add`) and their flags complete with Tab in zsh, bash, fish and PowerShell. `alf completion zsh` (or `bash`, `fish`, `powershell`) prints the script itself. ## Signing in ```bash alf login ``` It shows a code and opens a page in your browser. Check the page shows the same code and press **Sign it in**. The page works on any device where you're signed in to Alf, so signing in a server over SSH is the same: open the link on your laptop. No password is typed in the terminal. `--no-browser` only shows the link. `alf launch` and `alf deploy` do this themselves when the computer isn't signed in yet, and then carry on. They also see to a payment method: if your organization needs one before it deploys, they open Stripe's page and wait until it has arrived (owners and admins add it; in CI, where nobody can, they say so instead). Every computer you sign in is listed under **Your account** in the dashboard (the menu at the top right), with where it was last used and when. **Sign out** there ends it; so does `alf logout` on the computer itself. A sign-in that isn't used for 30 days ends on its own. The sign-in is kept in `~/.config/smartalfred/config.json`, readable only by you. ## Which organization Commands work in one organization: the one you chose with `alf use `, or your first. `alf orgs` lists yours and marks the one in use; `--org ` picks another for one command. ## Launching and deploying ```bash alf launch # this directory as a new project: files, databases, first deploy alf deploy # again: the code and alf.toml's settings alf deploy web # only one service of it alf deploy --plan # what it would do, and stop alf deploy --wait # and stay until it's live (what it does in CI) alf deploy --preview pr-12 alf deploy --profile production # a compose file's services in that profile too alf config shop --save # a project you made in the dashboard, as alf.toml here ``` [`launch`](/docs/launch) looks at the code, writes a Dockerfile where there's none, asks about the databases it uses, makes the project, writes [alf.toml](/docs/alf-toml) and deploys. `deploy` sends the directory as [`deploy.sh`](/docs/deploying-code) does — but after the first push only what changed since the last one goes up, and nothing when nothing did — (in a git repository: its files, uncommitted changes included, ignored ones left out), to the project alf.toml names (or `--project`), shows the plan, sends the code and ends with the project's link: the deployments carry on without the terminal. With `--wait`, and in CI (where `CI` is set), it follows them until they're live and exits with an error if one fails (`--no-wait` doesn't, even there). Secrets the file asks for are asked for, not shown, the first time. A setting changed in the dashboard since alf.toml set it is asked about, not undone: Enter keeps the dashboard's and writes it into the file (`--config-wins` takes the file's without asking; see [what a deploy does](/docs/alf-toml#what-a-deploy-does-with-it)). Without an alf.toml it deploys the docker-compose file or the Dockerfile, like `deploy.sh`; a compose file's services in [profiles](/docs/compose#profiles) come with `--profile`. - **What goes up:** what a build can see, as Docker would: a `.dockerignore` leaves files out of the push too, not only out of the image. A big file that goes up anyway is named, with its size, so you can leave it out (`.gitignore` or `.dockerignore`) if the build doesn't need it. See [what's sent](/docs/deploying-code#what-it-deploys). - **While it deploys:** a followed deploy prints the build's lines as they come, then the first lines the app printed once it's live. `--quiet` leaves them out. - **When it doesn't start:** if the app listens on another port than the service's on its first deploy, the port is set to that one and it deploys again by itself; it says so. If the app stopped because a variable is missing, it says the `alf env set` that sets it. See [troubleshooting](/docs/troubleshooting). ## Your project at a glance In a directory with an alf.toml, `alf` on its own (or `alf status`) shows that project: each service, how it's doing, its address, and what was deployed last and when. Elsewhere it's the help. There, every command about a service works on that project's app without naming it: `logs`, `open`, `restart`, `stop`, `start`, `shell`, `exec`, `redeploy`, `rollback`, `env`, `run` and `domains`. When the project has several apps it asks which at a terminal, and otherwise stops and lists them; it never guesses. A name is looked up in that project first, so `alf logs web` there is its web even if another project has one too. `stop`, `start` and `restart` take several names, or `--all` for every service of the project: ```bash alf restart # this directory's app alf stop web worker # two of them (asks first) alf stop --all --yes # the whole project alf exec -- npm run seed # a command in this directory's app ``` ## Deploying from GitHub ```bash alf ci github # in the directory with alf.toml ``` It writes `.github/workflows/smartalfred-deploy.yml` (every push to the default branch deploys the project) and `smartalfred-preview.yml` (a [preview](/docs/previews) for each pull request, ended when it closes; `--no-previews` leaves it out). Then, once you say yes, it makes a deploy token for the project, which can deploy to it and nothing else, for the repository's secret `SMARTALFRED_TOKEN`. With the [GitHub CLI](https://cli.github.com) (`gh`) installed and signed in, it saves the secret itself and the token is never shown. Without it, the token is shown once at the terminal, with the repository's page to paste it in (opened in your browser); the repository is the one the git remote points at. Commit and push the workflows, and the next push deploys. The workflows are the ones in [deploying code](/docs/deploying-code#from-ci). ## Services and computers ```bash alf services # every service: its state, address and server alf service shop/web # everything about one, and its last deployments alf projects alf computers # every computer, in whichever project it is alf computer shop/computer ``` A service is named by its name, or as `project/service` when that name is in more than one project or environment (a preview's copy doesn't count: the plain name means the project's own). A computer is named the same way, `project/computer`, by its own name when that's in one project only, or by its project's name when it's the only computer there. Ids (`svc_…`, `prj_…`) work everywhere too. Previews are left out of the lists unless you add `--all`. ## Variables, and running the code here ```bash alf env # the variables, and the secrets by name alf env set LOG_LEVEL=debug # set, and deploy (--stage: wait for the next deploy) alf env set --secret STRIPE_KEY # a secret: asked for, not shown alf env unset LOG_LEVEL alf env push # a .env file's lines onto the service (asks first) alf env pull # the variables into .env, for this machine alf run -- npm run dev # a command here, with them, the project connected ``` `env push` reads a `.env` file (comments, `export` and quotes as `.env` files have them) and sets its lines on the service: a key it has already keeps its kind, and a new one that looks like a password, key or token is offered as a secret. It shows what it will set and asks first (`--yes` doesn't). `env set` deploys the change straight away (the code stays as it is; only the environment changes), with anything else that was [staged](/docs/variables-and-secrets) in the dashboard. A key that's a variable becomes a secret with `--secret`, and back. `run` gives the command the service's variables the way its containers have them, and connects the private services they point at for as long as it runs: `${{db.HOST}}:${{db.PORT}}` becomes `127.0.0.1` and a port of this machine, over the project's [network](/docs/network-access) (the first time, this machine becomes one of the project's devices, as with `connect`). Anything set in your shell wins, which is how to change one for a run. A secret's value isn't shown again once it's set — not in the dashboard, not to agents, not here — so `run` and `pull` leave secrets out and say which: set those in your shell or `.env`. Keep `.env` out of git; `pull` says so when it isn't. ## A copy of the database ```bash alf db dump # its data into postgres-2026-10-20-1430.sql here alf db restore seed.sql # a .sql file into it (a snapshot is taken first) ``` Both run in the database's own container with its own tools (`pg_dump`, `psql`, `mysqldump`, `mysql`), so nothing needs installing here, and they move the data with flow control, so a big one doesn't pile up anywhere on its way. `dump` writes a file that loads into a database of your own (`psql -d myapp_dev -f postgres-….sql`). `restore` asks first, makes room on the database's disk for the file (a volume grows by itself, but an import can outrun it), takes a snapshot of the database so it can be undone, then loads the file. Postgres and MySQL (and MariaDB); `--db` names the database when the project has several. See [working with a database](/docs/database-access). ## Domains ```bash alf domains add shop.example.com # the DNS record to make, then waits until it's live alf domains # each domain, and how it's doing alf domains remove shop.example.com ``` `add` says the one record to create at the domain's DNS provider, and waits (at a terminal, or with `--wait`) while it shows up and the HTTPS certificate is issued — usually a few minutes after the record exists. See [domains](/docs/domains). ## Registries ```bash alf registries add ghcr.io --username you # the token is asked for, not shown alf registries # the organization's logins alf registries remove ghcr.io ``` The organization's logins for [private images](/docs/images#private-registries): services that run one, and builds whose Dockerfile starts `FROM` one. Without a terminal, the token is read from what's piped in. ## Coding agents ```bash alf agents ``` Finds Claude Code, Codex and Cursor on this machine and, for each that hasn't got it, asks whether to add Alf's [MCP server](/docs/agents). Nothing changes without a yes (or `--yes`); with nobody at the terminal it prints the commands instead. `launch` suggests it at the end when it sees an agent that hasn't got it yet. ## Logs ```bash alf logs web # the last 100 lines its container printed alf logs web -f # …and keep following alf logs web --since 2h --grep timeout alf logs web --since "2026-10-14 09:00" --until "2026-10-14 10:00" --stderr alf logs -f # in a launched directory: every service, interleaved ``` In a directory with an alf.toml, `logs` with no name shows all of the project's services together, in the order their lines were printed, each with its service's name, like `docker compose logs`. Name one for its lines alone. Plain `logs` reads the running container, now. With `--since`, `--until`, `--grep` or `--stderr` it searches what Alf keeps of a service's output — the last few days, from every server it ran on; see [metrics and logs](/docs/metrics-and-logs). `-n` says how many lines. ## When something is wrong ```bash alf doctor # every service of this directory's project: what's wrong, and what to do alf errors web # what went wrong lately: each error once, with its stack trace alf requests web -f --status 5xx # the requests it answers, as they come alf top # each service now: CPU, memory, requests, errors ``` `doctor` puts side by side what Alf knows about a service, so the cause is in one place: - its state, and a failed newest deploy with the failure's hint; - running out of memory, or getting close to it; - a volume that's full or can't grow; - its error rate and slowness over the last hour, and slow wakes; - domains that don't work, and variable changes waiting for a deploy; - and, in the app's directory, variables its `.env.example` lists that the service hasn't got. Each finding says what to run or do next. It ends with 1 when something is a problem, so a script can tell. `errors` reads what Alf kept of its output for errors: each one once, with its stack trace, how many times and when it was first and last seen. `--since 2h` looks back less (or up to `3d`), `--trace 30` shows more of each trace. `requests` shows each request it answered: when, the status, the method, how long it took, the path (without the query string) and the size. Its servers keep the newest few hundred, so it's what just happened; `-f` follows, `--status 5xx` (or `4xx`, `404`, `error`) and `--path /api` narrow it down, and `--with-logs` shows under each 5xx what the app printed while answering it. Calls from the project's own services are there too (`← worker`); `--from inside`, `internet` or a service's name narrows to those, and `--connections` shows the connections from inside that weren't HTTP (a database's protocol, TLS): who, the port, how long, the bytes each way and how each ended. `top` is the project's services as they are now, refreshing at a terminal: running or asleep, CPU and memory against their limits, requests a minute, 5xx answers and the slowest responses over the last five minutes. When a build fails, the deploy says why and what fixes it for the usual causes: a file the Dockerfile copies that wasn't sent (and which line of `.gitignore` or `.dockerignore` left it out), a lockfile out of step with `package.json`, a missing `build` or `start` script, a Node.js, Python or Go version older than a dependency needs, a base image that doesn't exist. ## A shell, or one command ```bash alf shell shop/computer # a shell in the computer alf shell web # a shell beside the service, in a container of its own alf shell web --live # in the container that serves its visitors alf shell web -- bin/rails console # something else, with a terminal alf exec web -- bin/rails db:migrate # one command, then its exit code alf exec web --live 'ls -la /app/uploads | head' ``` - A computer's shell is the computer itself. A service's is a fresh container of its image, with its variables, volumes and network — the same as the dashboard's terminal — so whatever you try there leaves the containers serving its visitors alone. `--live` opens it in the running container instead, to look at what it's doing. - The window follows your terminal's size, Ctrl-C goes to what's running there, and leaving (`exit`, Ctrl-D) ends that shell. - `exec` runs in the same place, with one argument through `sh -c` (so pipes work) and several as they are. It has two minutes unless `--timeout` gives it longer. - Flags go before the command: `alf exec web --timeout 10m -- ./backfill`. - A shell or a command is an owner's or an admin's to open, as in the dashboard. ## Your machine in a project's network ```bash alf connect shop # its services on localhost, until Ctrl-C alf connect shop db=15432 # only db, on localhost:15432 alf connect shop --all # every port, at postgres.shop.internal and the like (sudo) alf connect shop --config # a WireGuard file for another machine alf disconnect shop # revoke this machine's device alf devices # every device in your projects' networks ``` The first `connect` registers this machine as a device of the project, with a key made here that never leaves it. It works for 30 days (`--days`, `--forever`), and it's listed, with whether it's connected now, in the project's settings, where it can be revoked. See [network access](/docs/network-access). ## Restarting, stopping, starting ```bash alf restart web # fresh containers of the version that runs now alf restart shop/computer # a computer: stopped, then started again alf stop web # asks first alf start web alf redeploy web # its image again (or its code), with changed variables that wait for a deploy alf rollback web # back to the version that was live before (asks first) ``` `rollback` deploys the version that was live before the current one again — the same image, or the same code built again — with the variables and secrets the service has now; `--to ` goes back further (`alf service web` lists the recent ones with their ids). `restart`, `redeploy` and `rollback` wait until the new containers are live and show what went wrong if they don't start, with the last lines the app printed. `--no-wait` returns straight away. A public service without a volume normally keeps answering while it restarts. Restarting a computer ends whatever runs in it, so it asks first; its [workspace](/docs/computers) stays. It's also how a change that waits for a restart takes effect. ## Scheduled commands, previews and backups ```bash alf cron # the project's scheduled commands: when next, how the last run went alf cron run nightly # run one now and see what it prints (ends with its exit code) alf cron last nightly # its last run, with what it printed alf previews # the project's previews: when each goes, and its data alf previews open pr-12 # its address in your browser alf previews extend pr-12 --hours 48 alf previews end pr-12 # delete it now (asks first) alf backups # the project's snapshots alf backups take db --note "before the migration" alf backups restore snp_1a2b3c # put one back (asks first) ``` [Scheduled commands](/docs/deployments#running-something-on-a-schedule) are made in alf.toml or the dashboard; a name may say its service (`web/nightly`). Previews are made by `alf deploy --preview `; see [previews](/docs/previews). [Snapshots](/docs/backups) are taken with the container frozen for a moment, so they're safe for a database; restoring one replaces what's in the volume now, and its service is stopped meanwhile. ## Computers from the terminal ```bash alf computer create scratch --shell # a new project of its own, and a shell once it runs alf computer create build --project shop --system ubuntu --size xl --disk 50 alf computer extend shop/build --hours 48 # it goes in two days alf computer extend shop/build --keep # …or not at all alf computer create devbox --image ghcr.io/acme/devbox:latest alf ssh shop/build # SSH in, as dev (sudo works) alf ssh shop/build -L 3000:localhost:3000 # …with a port forwarded alf ssh-config shop/build --add # an SSH host for VS Code, Cursor, scp, rsync alf cp ./site shop/build: # files in… alf cp shop/build:report.pdf . # …and out alf computer resize shop/build --size 2xl # a bigger size (8 cores, 8 GB), as it runs alf computer docker shop/build on # Docker in it (after a restart) alf computer startup shop/build web -- 'npm run dev' # started when it starts, kept running alf computer copy shop/build # another like it, with what's in it alf computer delete shop/build ``` `--system` is `claude` (Debian 12 with Node 22, which gets Claude Code when you first open it in the dashboard), `debian`, `ubuntu` or `fedora`; `--image` makes one of your own image instead. A new computer is kept until you delete it unless you say `--hours`. `--size` is xs to 3xl (m unless you say; `--memory` or `--cpus` pick the smallest size with at least so much). `--disk` is its `/workspace`'s size in GB, billed by its size. `ssh` needs OpenSSH, which macOS, Linux and Windows have; its key is the tool's own, made the first time and given to the computer each time you connect. The `sandbox` commands still work, as other names for these. ## Inside a computer Every computer has an `alf` (and `smartalfred`) of its own for its ports, no install needed: `alf share 3000` gives a port an address, `alf ports` lists what listens; see [sharing a port](/docs/computer-access#sharing-a-port). If you install the full tool in a computer, it hands those commands to that one, so both keep working. ## What it costs ```bash alf usage # this month so far, and where it's heading ``` The organization's month so far and what it comes to at this rate, its free credit or when it's charged, its spending limit, and the services the money goes to (most first, so far → the whole month). It's the same as Billing in the dashboard, where the rest is. ## Keeping it up to date When a newer version is out, a command says so once a day, after what it printed: `alf update` gets it. It looks this up at most once a day, without holding the command up. It says nothing in CI, with `--json`, or when stderr isn't a terminal; `SMARTALFRED_NO_UPDATE_NOTICE=1` turns it off. ## In scripts - `--json` prints what the API answered, for `jq` and friends. - `--yes` answers the questions `stop`, `restart` and `computer delete` ask. With several services and nobody to ask which, a command stops and lists them instead of picking one. - It ends with 0 when it worked, 1 when it didn't (and says why), 2 when the command line was wrong — and `exec` and `shell` with the command's own exit code. - `NO_COLOR=1` turns colors off. It signs in as you. To deploy from CI, use [`deploy.sh` with a deploy token](/docs/deploying-code) instead: it reads the same alf.toml, and `alf ci github` sets it up for GitHub Actions. ## Every command | Command | What it does | | --- | --- | | `login`, `logout`, `whoami` | Sign this computer in or out; who you are | | `launch [directory]` | This code as a new project: its files, databases and first deploy | | `deploy [service]` | This directory's code and alf.toml, deployed | | `config [project]` | A project as alf.toml (`--save` writes it here) | | `ci github` | Deploy from GitHub Actions: the workflows, and the token as a repository secret | | `agents` | Add Alf's MCP server to the coding agents here (asks first) | | `env`, `env set`, `env unset`, `env push`, `env pull` | A service's variables: list, set and deploy, take away, from a .env file, to a .env file | | `db dump`, `db restore ` | A copy of a database's data here; a .sql file into it (a snapshot first) | | `rollback [service]` | Back to the version that was live before | | `run [service] -- ` | A command here, with the service's variables and its project connected | | `domains`, `domains add`, `domains remove` | A service's own domains; add one and wait until it's live | | `registries`, `registries add `, `registries remove ` | The organization's logins for private images | | `orgs`, `use ` | Your organizations; work in another | | `projects`, `services [project]`, `service [service]` | What you have, and everything about one | | `projects delete ` | Delete a project and everything in it (you type its name first; `--yes` for scripts) | | `computers`, `computer ` | Your computers, and everything about one | | `computer create`, `computer extend`, `computer delete` | Make, keep longer, delete | | `ssh `, `ssh-config `, `cp ` | SSH into a computer; it as an SSH host (`--add`); files in and out | | `computer resize`, `computer docker`, `computer startup`, `computer copy` | Its memory and CPU as it runs; Docker in it; what it starts; another like it | | `logs [name]` | What it printed; `-f` follows (no name: every service of the directory's project) | | `doctor [service]` | What's wrong: why it's down, failing or slow, and what to do | | `errors [service]` | What went wrong lately: each error once, how often, its stack trace | | `requests [service]` | The requests it answered; `-f` follows, `--status 5xx` and `--from` narrow, `--with-logs` shows what a 5xx printed, `--connections` the connections from inside that weren't HTTP | | `top [project]` | Each service now: CPU, memory, requests, errors | | `cron`, `cron run `, `cron last ` | Scheduled commands; run one now; its last run | | `previews`, `previews open`, `previews extend`, `previews end` | A project's previews | | `backups`, `backups take`, `backups restore ` | Snapshots of a service's volumes; take one; put one back | | `connect `, `disconnect ` | This machine in a project's private network; out again | | `devices`, `devices revoke ` | The devices in your projects' networks; take one off | | `shell [name]`, `exec [name] -- ` | A shell in it; one command in it | | `restart`, `stop`, `start [name ...]` | Fresh containers; switch off; switch on (several, or `--all`) | | `move ` | To another server, with its data (a service with volumes is stopped, copied and started there) | | `redeploy [service]` | Deploy it again, as it is | | `open [name]` | Its address in your browser (`--dashboard`: its page here) | | `usage` | This month's cost so far, and where it's heading | | `update`, `version`, `help [command]` | The newest version; which this is; more on a command | | `completion [shell]` | Tab completion (`--install` adds it to your shell) | `alf help ` has every flag. --- # Services A service is one container image, or the image built from your code, with the settings it runs with and its addresses. Most of what you run here is a service: a web app, an API, a worker, a cache. (Databases, computers, static sites and buckets are services or sit beside them, and have sections of their own.) ## Where it comes from - **Your code**, built from its Dockerfile with one command from your directory, your CI or your agent: [deploying your code](/docs/deploying-code). - **A docker-compose file**, a whole stack at once: [docker-compose stacks](/docs/compose). - **A published image**, from Docker Hub or any registry: [container images](/docs/images). - **A ready-made stack**, an app with its database: [one-click deploys](/docs/one-click). ## Public or private - **Public**: it gets an HTTPS address and [sleeps when idle](/docs/sleeping), unless you say otherwise. The next request wakes it. - **Private**: no address on the internet, only `name:port` inside its project. Use this for workers and anything only your other services call. Private services always run. ## While it runs Every change to what runs is a [deployment](/docs/deployments), with a log and a way back. It runs in a container with the memory and cores [it's given](/docs/containers), with its [variables and secrets](/docs/variables-and-secrets) in its environment and, if it keeps data on disk, [volumes](/docs/volumes). It can [scale](/docs/scaling) to more containers under load, and a [preview](/docs/previews) of its project is a copy to try a change in. --- # Deploying your code One command deploys the directory it's run in. It works the same from your computer, from a coding agent and from CI: there is no connection to GitHub or any other host to set up, and nothing about your repository is stored here but the code you push. ```bash alf launch # the first time: sets the directory up and deploys it alf deploy # every time after ``` That's the [command line](/docs/cli). It doesn't wait for the deployments: once the code is sent it says the project is being deployed and prints its link, where the deploy is followed live (`--wait`, or CI, waits). Where it isn't installed (CI, an agent's sandbox, a server), [`deploy.sh`](#without-the-command-line) does the same with nothing to install. ## What it deploys The command looks at the directory: | It finds | It deploys | | --- | --- | | An [`alf.toml`](/docs/alf-toml) that lists services | Those, with the settings it gives them, and the databases, buckets and computers it names. | | A docker-compose file (`docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, `compose.yaml`) | Every service in it: `build:` ones are built from your code, `image:` ones run that image. See [docker-compose stacks](/docs/compose). | | Otherwise a `Dockerfile` | One service, built from it, named after the directory (or `--service `). | Services the project already has get the new code. Missing ones are created: public on the port the image declares, unless the compose file says otherwise. Without an alf.toml, a push brings code, not settings: variables, domains, scaling and everything else you set in the dashboard stay as they are. With one, the settings it names are set too, and those it doesn't name stay. Nothing is ever deleted by a push. Before anything is sent, the command prints the plan: ``` Project: shop ~ web: deploy the pushed code (built from web/Dockerfile) + worker: new service, built from worker/Dockerfile, private = db: unchanged (postgres:17) ``` For what's new or resized, the plan also says what it costs: its size, so much an hour awake (and, for one that never sleeps, a month), and nothing while asleep. Then it packs the code, sends it, and waits. From the [command line](/docs/cli), only the files that changed since the services' last push are sent (nothing at all when none did), and their server puts the code together again: a deploy of a large repository after a small change sends a few kilobytes. Your server builds the images; the build log is live in each service's **Deployments** tab, and a preview's services are built on the servers its project's are, where their build cache is. When a deployment fails, the command prints why, with the container's last output, and exits with an error. **What's sent.** In a git repository: the repository's files, including uncommitted changes, without ignored files (so `node_modules`, build output and `.env` files in your `.gitignore` stay home). Elsewhere: the directory, without `.git` and `node_modules`. `alf deploy` also sends the files of submodules that are checked out (a site's theme, say), leaves out what the builds' `.dockerignore` does (Docker wouldn't see it anyway), and names any file of 50 MB or more that goes up, so data dumps and the like can be left out. Large code is fine: it goes up in parts, up to 8 GB compressed. (Gigabytes of "code" are usually data, models or build output: what the image doesn't need is better left out, and what it does need makes every deploy as slow as that upload. For the heaviest cases, build the image in CI, push it to a registry and deploy the image.) Paths are from the repository's root, so a monorepo works from any subdirectory. ## From your computer ```bash cd ~/code/shop alf deploy # what alf.toml (or the compose file, or the Dockerfile) has alf deploy web # only the service web alf deploy --project shop # without an alf.toml: which project ``` In a project, **+ Create → Your code** (or, for one service, **Deployments → Deploy new code**) shows the command for it. ### Without the command line The same page has **a one-time line, nothing to install**: `deploy.sh` with a link good for 15 minutes, so no token is stored on the machine. It needs `sh`, `curl` and `tar`: ```bash curl -fsSL https://runalf.com/deploy.sh | sh -s -- "https://api.runalf.com/api/v1/push/…" ``` A service's own link always deploys to that service, whatever else the directory holds. [Coding agents](/docs/agents) get this line from their `push_code` and `upload_code` tools. ## From CI On GitHub, one command in the code's directory sets it up: ```bash alf ci github ``` It writes the two workflows below and, once you say yes, makes a deploy token for the project for the repository's secret `SMARTALFRED_TOKEN`: saved there with the GitHub CLI when it's installed and signed in (the token is never shown), else shown once with the page to paste it in. See [the command line](/docs/cli#deploying-from-github). By hand, or in another CI: CI uses a **deploy token** instead of a link. In **+ Create → Your code → From CI → Another CI, or by hand**, create one for the project (or make one under **Agents**: deploy access, limited to the project) and save it in your CI as the secret `SMARTALFRED_TOKEN`. In CI the command is `deploy.sh`, which needs nothing installed. ### GitHub Actions ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to Alf run: curl -fsSL https://runalf.com/deploy.sh | sh env: SMARTALFRED_TOKEN: ${{ secrets.SMARTALFRED_TOKEN }} SMARTALFRED_PROJECT: shop ``` Every push to `main` is deployed; the job fails when the deployment does, and its summary shows what happened. The deployment carries the commit and its message. ### A preview for every pull request ```yaml # .github/workflows/preview.yml name: Preview on Alf on: pull_request: types: [opened, synchronize, reopened, closed] concurrency: group: smartalfred-preview-${{ github.event.number }} jobs: preview: # Pull requests from forks get no secrets, so they're skipped. if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest # The comment is written with the job's own token: nothing to install on GitHub. permissions: contents: read pull-requests: write env: SMARTALFRED_TOKEN: ${{ secrets.SMARTALFRED_TOKEN }} SMARTALFRED_PROJECT: shop steps: - uses: actions/checkout@v4 if: github.event.action != 'closed' - name: Deploy the preview id: deploy if: github.event.action != 'closed' run: curl -fsSL https://runalf.com/deploy.sh | sh -s -- --preview pr-${{ github.event.number }} - name: Say where it is on the pull request if: github.event.action != 'closed' && steps.deploy.outputs.urls != '' env: GH_TOKEN: ${{ github.token }} URLS: ${{ steps.deploy.outputs.urls }} SHA: ${{ github.event.pull_request.head.sha }} run: | marker='' body="$(printf '%s\n' "$marker"; printf '**Preview** of %s, live on Alf:\n\n%s\n' "${SHA:0:7}" "$(printf '%s\n' "$URLS" | sed '/^$/d; s/^\([^ ]*\) \(.*\)$/- **\1**: \2/')")" comments="repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments" id="$(gh api --paginate "$comments" --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)" if [ -n "$id" ]; then gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null elif [ "${{ github.event.action }}" != closed ]; then gh api "$comments" -f body="$body" >/dev/null; fi - name: End the preview if: github.event.action == 'closed' run: curl -fsSL https://runalf.com/deploy.sh | sh -s -- --end-preview pr-${{ github.event.number }} - name: Say it ended on the pull request if: github.event.action == 'closed' env: GH_TOKEN: ${{ github.token }} run: | marker='' body="$(printf '%s\n' "$marker"; printf 'The preview of this pull request ended when it closed.\n')" comments="repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments" id="$(gh api --paginate "$comments" --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)" if [ -n "$id" ]; then gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null elif [ "${{ github.event.action }}" != closed ]; then gh api "$comments" -f body="$body" >/dev/null; fi ``` `--preview` deploys to a [preview](/docs/previews) of the project with that name, and makes it the first time: a copy of the project at its own addresses. The steps after it put the addresses (`deploy.sh` hands them on as the output `urls`) in a comment on the pull request, one that's updated with each push and says when the preview ended, with the job's own token: nothing to install on GitHub. Pull requests from forks get no secrets, so they're skipped. ### Any other CI The command is the same everywhere: check the code out, set `SMARTALFRED_TOKEN`, run it. ```yaml # .gitlab-ci.yml deploy: image: alpine/git rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - apk add --no-cache curl - curl -fsSL https://runalf.com/deploy.sh | sh -s -- --project shop ``` It needs `sh`, `curl`, `tar` and (in a repository) `git`. ## Options Each option can also be set in the environment, as `SMARTALFRED_` and its name: `--keep-staged` is `SMARTALFRED_KEEP_STAGED=1`. | Option | | | --- | --- | | `--project ` | The project: its name, address part or id. Not needed with a link, a token made for one project, or an `alf.toml` that names it. | | `--service ` | With a Dockerfile: the service to deploy to (a new name creates it). With a compose file: only that service of it. | | `--config ` | Use this project file instead of `alf.toml`. | | `--no-config` | Leave `alf.toml` out. | | `--compose ` | Use this compose file instead of the one found. | | `--profile ` | The compose file's [profiles](/docs/compose#profiles) to deploy, comma-separated. Its services in other profiles are left out, as compose does. | | `--dockerfile ` | Use this Dockerfile; a compose file and `alf.toml` are ignored. | | `--context ` | The directory to build in. Default: the Dockerfile's. | | `--preview ` | Deploy to the project's preview of that name, made if it isn't there. | | `--end-preview ` | End that preview, and do nothing else. | | `--candidate` | Start new versions [beside the live ones](/docs/deployments#candidates-try-a-version-before-it-s-live) and wait to be promoted. | | `--keep-staged` | Leave [staged variable and secret changes](/docs/variables-and-secrets) staged. By default they go live with the deployment. | | `--config-wins` | Settings changed in the dashboard since `alf.toml` set them go back to the file's. By default [they stay](/docs/alf-toml#what-a-deploy-does-with-it), and the deploy says which. | | `--group ` | Show newly created services as one [group](/docs/one-click#groups). | | `--message ` | Shown with the deployment. Default: the last commit's subject. | | `--plan` | Print the plan and stop: nothing is sent or changed. | | `--no-wait` | Don't wait for the deployments. | A directory can be given last: `… | sh -s -- --project shop ./apps/web`. ## Where the Dockerfile is For a new service it's the `Dockerfile` in the directory the command runs in, built in that directory. After the first push the service remembers where its Dockerfile is, so pushing from another directory of the same repository still builds the right one. `--dockerfile` (and `--context`) change it; so do the **Dockerfile** and **Build directory** fields in the service's settings. ## A service that waits for its code **+ Create → Service → Your code** makes a service without deploying anything: useful when you want to set its variables, secrets or volumes first. It's deployed with the first push to it. ## Tokens and safety - A deploy token made for one project can't touch another. Tokens can carry [limits](/docs/agent-safety), and a project that requires approval for agents' changes refuses pushes with tokens; a person's link still works. - A push shows up in the token's activity under **Agents**. - The link or token authorizes the push; after that, the push has its own one-hour address for sending the code and asking how it went. --- # 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.]` 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..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..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. --- # docker-compose stacks Bring a whole stack in one go: [deploy a directory](/docs/deploying-code) that has a docker-compose file, and every service in the file becomes a service in the project. ## Deploy one In the directory with the compose file, run the command from **+ Create → Your code** (or the same command in CI). It prints what it will create, then sends the code: ``` Project: shop + web: new service, built from web/Dockerfile, with an address + db: new service, image postgres:17, private + cache: new service, image redis:7, private ``` Push again later and the services that are there get the new code; a service you added to the file is created; an `image:` you changed is switched. Services you removed from the file are left alone: delete them in the dashboard. Settings you changed in the dashboard (variables, scaling, domains) stay: after the first push the compose file decides what exists and what it's built from, not how it's configured. `--service ` deploys just one service of the file; `--group ` shows the new services as one [group](/docs/one-click#groups) on the canvas. ### Profiles Services in `profiles:` come as `docker compose up` brings them: only when one of their profiles is deployed. `--profile production` deploys the file's services without a profile and those in `production`; several go comma-separated (`--profile production,init`). Without `--profile`, the plan says which services were left out, and in which profiles. A service named with `--service` comes whatever its profiles. For well-known apps there may already be a [one-click deploy](/docs/one-click): the same idea, ready-made. ## How the file is read | In the compose file | On Alf | | --- | --- | | `build:` | A service built from your code (Dockerfile and context as given, relative to the compose file). | | `image:` | A service running that image. | | `ports:` | A public service with an address; the container port is used. HTTPS ends at the edge, so when the file publishes 80 and 443, it's 80. | | `ports:` only on `127.0.0.1` | Private: the file keeps it to its own machine. [`alf connect`](/docs/cli#your-machine-in-a-project-s-network) reaches it from yours. | | no `ports:` | A private service, reachable as `name:port` inside the project. | | `environment:` | Variables, when the service is created (move passwords to secrets afterwards). | | named `volumes:` | Volumes that keep their data across deploys. | | a database's image (`postgres`, `mysql`, `mariadb`, `mongo`, `redis` and the like) or data directory, on a volume | Backed up from the start, as a [database](/docs/databases) made here is: a [snapshot](/docs/backups) a day, the last week of them, and a copy off the server. | | `profiles:` | Left out unless one of its profiles is deployed (see above). | | anchors and `<<: *defaults` | Merged, as compose does. | | `command:`, `entrypoint:` | Kept. | | `deploy.replicas` | Instances. | | `mem_limit`, `deploy.resources.limits.memory` | Its [size](/docs/usage#sizes): the smallest with at least that much memory. Without one it's s (512 MB). | | label `smartalfred.public: "false"` | Private even with `ports:`. | | labels `smartalfred.always_on`, `smartalfred.health_path` | Never sleeps; the path that says it's ready. | | label `smartalfred.static: ` (or `"true"`) | A [static site](/docs/static-sites): that folder of the built image is served from the edge, and nothing runs. `"true"`: the image's own `LABEL smartalfred.static` names the folder. | | label `smartalfred.spa: "true"` | A static site in SPA mode: paths that aren't files get `index.html`. | Private services (databases and the like) are created first, so they're up when the apps start. Services keep their compose names, so `DATABASE_URL=postgres://…@db:5432/app` keeps working. There's no host environment here, and no `.env` file is read: `${VAR:-default}` takes its default (which may hold variables of its own), and `${VAR}`, `$VAR`, `${VAR:?…}` and `${VAR:+…}` are empty. An image left without its tag or its name that way (`app:${VERSION}`) is refused before anything is made, naming the variable: write the tag into the file, or give it a default. Anything else the file uses that Alf can't do (bind mounts, a build context outside the code) is listed as a warning in the plan. A bind mount of a folder of your project that the app writes to (`./data:/app/data`) becomes a volume of the same name, so what's written there is kept across deploys. It starts empty: what's in the folder on your machine isn't copied. The project itself (`./`), single files and read-only mounts are code and configuration: put them in the image instead. Paths outside the project (`/var/run/docker.sock`) are skipped. ## What isn't done here The plan names each of these once, with the services that use it: - **`depends_on`** isn't waited for. Private services (databases and the like) start first, and an app that starts before what it needs is started again: 10 seconds later, then less often. - **`healthcheck`** isn't run. For an app, the label `smartalfred.health_path` holds requests until that path answers. - **`networks:`** are one network: every service reaches every other by its name, including those the file keeps apart. Other names on a network (`aliases:`) aren't kept, and nor are `network_mode`, `extra_hosts` and `links`. - **`privileged`, `cap_add`, `devices`** and the like aren't used: every container runs [in a virtual machine of its own](/docs/security), with the same settings. `user` isn't either (it runs as its image's user), nor `shm_size`. - **`secrets:` and `configs:`** aren't read: put their values under [Variables](/docs/variables-and-secrets), as secrets, or the files in the image. `env_file` isn't read either. --- # Container images The quickest way to run something: any image from Docker Hub, GitHub Container Registry or another registry the servers can pull from. ## Create one **+ Create → Service → Container image**, then enter the image, e.g. `nginx:1.27`, `postgres:16` or `ghcr.io/acme/api:2.3`. - **Port**: the port the app listens on inside the container. Leave it empty and Alf uses the port the image declares (`EXPOSE`), or 8080. The app also gets a `PORT` variable. - **Command**: optional; replaces the image's command, e.g. `redis-server --appendonly yes`. ## Updating Change the image (say from `:1.2` to `:1.3`) under the service's **Settings**: that starts a deployment. To fetch a moving tag like `:latest` again, press **Deploy** on the service's page. ## Private registries Give the organization a login for the registry, and its private images are pulled with it: services that run one, and builds whose Dockerfile starts `FROM` one. ```bash alf registries add ghcr.io --username you ``` The token is asked for and not shown (or piped in: `echo "$GHCR_TOKEN" | alf registries add ghcr.io --username you`, for CI). In the dashboard: **Settings → Registries**. The registry is its host, as images from it start with: `ghcr.io`, `registry.gitlab.com`, `registry.example.com:5000`, or `docker.io` for Docker Hub. One login per registry; adding one again replaces it. Admins add and remove them. | Registry | Username | Token | | --- | --- | --- | | GitHub (`ghcr.io`) | your GitHub username | a personal access token (classic) with `read:packages` | | Docker Hub (`docker.io`) | your Docker ID | an access token, read-only | | GitLab (`registry.gitlab.com`) | the token's username | a deploy token with `read_registry` | - **It's checked first.** Alf asks the registry, as Docker does, and a login it turns away isn't kept. A registry that can't be reached from Alf is saved anyway, and the first pull says whether it works. - **It stays the organization's.** The token is encrypted and never shown again. Servers get it only with the organization's own services, for their pulls and builds. An image pulled with it is kept under a name of the organization's own on that server, so another organization's service naming the same image doesn't get it: its server has to pull it, and the registry asks for a login. - **New pulls use it.** After adding one, deploy the service again (or **Redeploy**). Removing one leaves images already pulled where they run. A private image pulled without a login fails with `pull access denied` or `unauthorized`. The deployment says which registry needs a login. `alf registries` lists the logins, and `alf registries remove ghcr.io` takes one away. --- # One-click deploys A one-click deploy is a ready-made stack: an app together with its database and settings, put into a project in one go. Browse them at [runalf.com/one-click](/one-click), or on a project's canvas under **+ Create → One-click deploy**. ## Deploying one 1. Pick one, for example **Umami** (web analytics). 2. Choose the project it goes into, or let it make a new one. 3. Fill in what it asks for. Most ask for nothing: passwords and keys are generated. 4. **Deploy**. You get: - its services in the project, wired to each other by [references](/docs/variables-and-secrets) (the app's `DATABASE_URL` is `${{umami-db.DATABASE_URL}}`, kept up to date); - its databases made the way the dashboard makes [one](/docs/databases): private, always on, a snapshot every day; - an HTTPS address for the public service; - a note on what to do first (the default login, what to change); - the generated passwords and keys, **shown once**. They're stored as [secrets](/docs/variables-and-secrets): copy them then if you'll need them elsewhere. From there they're ordinary services and databases: change images, variables, scaling or domains like any other, or delete them. They appear as one [group](#groups) on the project's canvas, named after the deploy. A project can't hold two things with the same name, so the same deploy twice goes into two projects. ## Where they come from The catalog is a public repository: [SmartAlfred/one-click](https://github.com/SmartAlfred/one-click). Each folder is one deploy and holds an [alf.toml](/docs/alf-toml), the same file a project keeps beside its code; the name, description and what the form asks for are in it under `[template]`. Alf reads the repository as it is, about once a minute, so a deploy merged there is on the page right after. To add one, open a pull request there: its README explains the format. To deploy a stack of your own without publishing it, write it in your project's [alf.toml](/docs/alf-toml) and deploy that. ## Groups A group is a frame on the project's canvas around what belongs together: services, computers and buckets. A one-click deploy, the services of one repository with their bucket, or whatever you decide. - **Made for you**: a one-click deploy is one group; importing a compose file offers one, named after the repository. - **Make your own**: **+ Create → Group**, give it a name and a colour, and tick what goes in it. Services, computers and buckets are listed apart. - **Change one**: click its name on the canvas to rename it, recolour it or change what's in it. The **Settings** of a service, a computer or a bucket have its group too. - **Move one**: drag it by its name; everything in it moves together. - **Remove one**: what was in it stays where it is, ungrouped. A group with nothing left in it disappears by itself. Groups are only for looking at. Nothing about how anything runs, reaches anything else or is billed depends on them, and each thing is in one group at most. ## The canvas is shared Where the cards are on a project's canvas is the project's, not your browser's: when someone in your organization drags a card or a group, everyone sees it there within a few seconds. Cards nobody moved are laid out by themselves, each group as a block of its own. --- # Deployments and rollbacks ## Statuses | Status | Meaning | | --- | --- | | Queued | Waiting for a server to pick it up. | | Building | A server is building the image from your code. | | Built | The image is ready; the new version is starting (shown as **Starting** or **Switching**). | | Live | It runs and takes the requests. | | Failed | The build failed, or the new version never became ready: the log and the error say why. | | Superseded | A newer deployment replaced it before it went live, or after. | Click a deployment in the **Deployments** tab for its log: the build, then each step of the switch. ## Deploys without downtime A new version of a public service doesn't replace the running one at once: 1. The new version starts **next to** the running one. Visitors still reach the running one. 2. Alf waits until the new version is ready: its port answers, and its **health check path** if you set one. 3. New requests go to the new version. Requests still open on the old one finish (up to 60 seconds, WebSockets included). 4. The old version stops and is removed. If the new version isn't ready within 3 minutes (or exits), it's removed, the deployment **fails**, and the old version keeps running as if nothing happened. Nothing is retried by itself: fix the cause and deploy again. ```diagram deploy 1. The new version is built or pulled while the old one keeps serving; if the build fails, the deployment fails and nothing else changes. 2. Its release command runs, if it has one: once, in a container of the new version, in the project's network; if it fails or runs over 30 minutes, the deployment fails and the new version never starts. 3. The new version starts next to the old one, and visitors still reach the old one. 4. It's ready when its port answers, and its health check path if it has one; if not within 3 minutes, or it exits, it's removed, the deployment fails and the old one carries on. 5. New requests go to the new version, which is live; the old one finishes its open requests (up to 60 seconds, WebSockets too) and is removed. ``` A few details: - **Health check path** (service settings, e.g. `/health`): a path that answers with a 2xx or 3xx status once the app can take requests. Set one if your app opens its port before it's ready (migrations, warm-up); without one, the open port counts. It's waited for every time the app starts, not only on a deploy: when a request wakes it from sleep, and when another replica starts under load, nothing is sent to it until the path answers. - **Replicas**: when the running version has several containers under load, the new version starts as many before the switch, so capacity doesn't drop. - **Several servers**: one server goes first. The others keep the old version until the new one is live there, so a version that doesn't start never reaches them. - **Sleeping services**: the new version is started once to check it, then takes over. - **Settings**: changing the port, command, memory or CPU limit, or public/private is a deployment too ("settings changed") and rolls out the same way. ## Release commands A service's **release command** runs once per deploy, before the new version takes over: what database migrations are for (`npx prisma migrate deploy`, `python manage.py migrate`, `bin/rails db:migrate`, `php artisan migrate --force`). Set it in the service's settings (**Command, variables and deploys → Release command**) or as `release_command` in [alf.toml](/docs/alf-toml); [`alf launch`](/docs/launch) sets it when it recognises the code's migrations. 1. The new version's image is built (or pulled), while the old version keeps serving. 2. The command runs in a container of the **new** version, with its variables and secrets and in the project's network, so it reaches the database. It runs like `alf exec`: through the image's entrypoint. 3. When it ends without an error, the new version starts and takes over as usual. Its output is in the deployment's log. 4. When it fails (or runs longer than 30 minutes), the deployment **fails** with what it printed, the new version never starts, and the old one keeps serving. Nothing is tried again until the next deploy. Good to know: - It runs once per deploy, on the server the deploy goes to first, not once per server or per container. A rollback runs it again (migrations that are applied already are skipped by every migration tool). - It runs without the service's volumes: the running version still has them. An app keeping SQLite on a volume migrates in its start command instead. - Migrations should keep working with the version that's still serving while they run, as with any deploy without downtime: add before you remove. - A service with a release command can't have [candidates](#candidates-try-a-version-before-it-s-live): a candidate would migrate the live database before anyone decided to promote it. Try such a change in a [preview environment](/docs/previews), which has its own database. - Unlike a start command that migrates, it doesn't run when a sleeping app wakes, so waking stays fast. ## Candidates: try a version before it's live A candidate is the new version started **beside** the live one and held there: - in the same project network, so it reaches the same database and services; - with the same variables and secrets, or with the staged ones if changes are waiting (they go live when it's promoted); - at its own address, the service's address with `--next` (`web-shop-acme--next.smartalfred.com`). Visitors and the project's other services keep reaching the live version. When the candidate is healthy it shows as **Ready to promote**: - **Open** it and check. - **Promote** makes it the live version: the usual switch, without downtime. - **Discard** removes it. The live version was never touched. Start one from the arrow next to **Deploy**, from **Deploy new code** (tick *Hold it as a candidate*), with `--candidate` on the [deploy command](/docs/deploying-code), or have an agent do it (`deploy`, `upload_code` or `push_code` with `candidate`). Good to know: - A candidate talks to the **real** database. If the new version changes the schema on start, that happens for real. For that kind of change use a [preview environment](/docs/previews), which is a separate copy of the project. - One candidate per service; a new one replaces the one waiting. Left alone, it's removed after a day. - Only services that can run twice at once can have one: public services without a volume. - Previews and candidates are different things: a preview copies the whole project, with its own network, addresses, empty volumes, databases and buckets; a candidate is one service's next version inside the real project. ## When a deploy fails A failed deployment says what kind of failure it was and shows what the app last printed: | It says | Meaning | Usual fix | | --- | --- | --- | | **The app stopped** | The process exited before it was ready (with its exit code; "out of memory" if it used more than its limit). | Read its last output: a missing variable or secret, a database it can't reach, a wrong start command. When the output says a variable is missing, the failure names it and the `alf env set` that sets it. | | **Nothing on its port** | It runs, but nothing accepts connections on the service's port. If it listens somewhere else, that port is named. | Set the service's **Port** to the one named, or make the app listen on `$PORT` and on `0.0.0.0`. | | **Health check** | The port is open, but the health check path answered with an error, or not at all. | Fix the path, or change or clear **Health check path** in Settings. | | **Release command** | The [release command](#release-commands) failed, so the new version never started; the old one still serves. | Read its output: often a database it can't reach yet, or a migration that doesn't apply. | | **Couldn't start** / **Build** | The container or image couldn't be made. | The message and the build log say why; for the usual build failures (a file not sent, a lockfile out of step, a missing script, a runtime too old, a missing base image) the failure says what fixes it. | A service that has never been live and fails its first deploy because its app listens on another port gets that port and deploys again by itself, once: the failed deployment says "Fixed by itself" with the port, and the new one follows it. It's only done when the app listens on exactly one other port. A port changed that way is the service's from then on, as if you had set it: if alf.toml says another, the next `alf deploy` asks which to keep and can write the new one into the file (without a terminal, as in CI, the new one stays). The same goes for a service's very first start: a new service is started once, right after it's created, to see that it comes up. A service that sleeps when idle goes to sleep again afterwards. ### When the old version stops first Some services' old container stops before the new one starts (a gap of a few seconds): - services with a [volume](/docs/volumes): two containers on one data directory would corrupt it; - [databases](/docs/databases): they have one place in the project's network; - private services, unless you choose otherwise: two workers at once might do the same job twice. A private app is called through its server's front, so it can switch without downtime like a public one: the new version starts beside the old one and takes the calls once it's ready. Choose **Without downtime** under **Settings → When a new version is deployed** (or `zero_downtime = true` in alf.toml) for an API the project calls; leave it for a worker. You can choose to stop first for any service: **Settings → When a new version is deployed → Stop the old version first**. ## Deploying again **Deploy** at the top of the service's page deploys now: image services pull their image again, services built from code rebuild the code pushed last. New code arrives with the [deploy command](/docs/deploying-code), from your computer or CI. [Variable and secret changes](/docs/variables-and-secrets) waiting to be deployed go along. ## Rolling back Earlier deployments that were live have **Roll back**: it deploys the same image or commit again, with today's variables and secrets. Pushed code can be rolled back while it's kept (the newest three pushes of a service). ## Working inside a container **Service → Terminal** opens a shell in a container, for a migration, a database console or a look around. Two kinds: - **A container of the service's image**, with its variables, secrets, volumes and project network. Anything you break here is thrown away when you close it. - **Its own running container** (tick *Its own running container*): what's actually serving traffic. It's held awake while the terminal is open, and what you change is live. Either way the container is metered like any other container of the service while it's open, and it closes itself after fifteen minutes without a command. Your coding agent has the same thing without the terminal: `exec` runs a command and gives back the exit code and output, and `open_session` keeps a container between commands. Both need a token that may run commands. Everything run in a session is written down. ## Running something on a schedule **Service → Scheduled** runs a command over and over: a nightly clean-up, an hourly digest, a report on the first of the month. Each run gets the service's image, variables, secrets, volumes and project network, in a container thrown away afterwards — the same as a one-off command, with a time attached. The schedule is written the way cron has always been written, in **UTC**: ``` 0 3 * * * every day at 03:00 */15 * * * * every fifteen minutes 0 9 * * 1 every Monday at 09:00 @daily the same as 0 0 * * * ``` One thing that catches people out, and that cron has always done: if you give **both** a day of the month and a day of the week, it runs on **either**. `0 0 1 * 1` is the first of the month *and* every Monday, not the first when it falls on a Monday. **Run now** runs it without waiting, which is how to see whether it works. A failed run is kept with its output, and you're told through your notification channels. A run still going when the next is due is skipped rather than started twice, and a service that can't be deployed to — an unpaid invoice, a spending limit — is passed over until that's sorted out. --- # What a container gets Every service runs as a container: your image, started on one of the servers, at the size you choose: its memory and the cores that come with it. Disk speed isn't limited or charged: a container reads and writes as fast as the server's disk does. ## What it gets Its size, from xs (a quarter of a core and 256 MB) to 5xl (32 cores and 64 GB); an app gets s (half a core and 512 MB) unless it says. Its server keeps that much for it, and it's billed for it each hour it's awake ([sizes and prices](/docs/usage#sizes)). Under **Scaling** you choose another, and it's given to the running containers at once: nothing restarts. The two limits behave differently when you reach them, and the difference matters when something is slow: - **Memory** is a wall. Over it, the container is stopped and started again. It can't be slowed down there instead (nothing could free the memory but swap, which would slow its neighbours), so you're told before it happens: when it stays above 90% of its memory, its **Overview** says so and your notification channels hear "nearly out of memory", and they hear it too if it was restarted for running out. The fix is a bigger size under **Scaling**. - **CPU** is a ceiling. At it, the container runs slower — it is never killed for being busy. The CPU chart draws the limit beside the reading, so you can see the difference between busy and throttled. If your work needs more CPU — a build, a transcode, anything that computes more than it remembers — give it a bigger size: each one has twice the cores of the one before (up to 32). ## Its own filesystem A container can write anywhere inside itself, but what it writes there doesn't survive a deploy and doesn't go on the server's disk: it comes out of the container's memory. So a service that writes a few hundred megabytes of scratch files needs the memory to hold them. Anything you want to keep goes in a [volume](/docs/volumes). ## Volumes have a size that is really enforced A volume is given a filesystem of its own, with the size you choose. Writing past the end fails the way a full disk always has — the program gets "no space left on device" — and nothing else on the server notices. Your database stays up and readable instead of being stopped, which is the whole reason it works this way. Its size is set aside on its server's disk, and it's billed by it, full or not. It can be made bigger or smaller (down to what it holds) as it runs, or told to grow by itself as it fills, up to a limit you choose. ## What a container can't do Every container runs in a virtual machine of its own, [Kata Containers](https://katacontainers.io), with its own Linux kernel and the processor's hardware virtualization between it and the machine: your apps', your databases', your computers', the terminals you open in them, and every step of a build. A bug in a kernel is then not a way out of the container, which is what makes it safe to run different organizations' code on one machine. There are no exceptions, and a server that can't do it runs nothing at all. If you'd rather have machines to yourself anyway, [private servers](/docs/servers#private-servers) are the answer. Because it's a real kernel, programs run as they would on the machine itself: a database's commits, a build's steps, anything that leans on the kernel. What the VM costs is a little memory for its own kernel, which comes on top of the container's size rather than out of it, and a moment more when it starts from cold. A container asleep is a paused VM, so waking it is quick. Beyond that, a container is created without the powers nothing ordinary needs: - **No raw sockets.** This is the one that matters most: raw sockets would let a container forge ARP and DNS replies to its neighbours on the same server, and that needs no clever exploit at all, just a socket. - **No device nodes, no chroot, no writing to the host's audit log**, and no way to hand itself further powers later. - **A cap on processes and threads**, by size, so a runaway can't exhaust the server's process table. It's generous — thousands — because a database with many connections and a runtime with a thread pool are both perfectly normal. What's kept is what ordinary images need: a container can still start as root and drop to its own user, own its files, and bind to low ports. Postgres, nginx, Redis and the rest run unchanged. ## What's shared and what isn't Each project has its own private network. Services in it reach each other by name; nothing else can, whether it belongs to you or to anyone else, on the same server or across servers. On a shared server, containers from different organizations run on one machine, each with its own memory, CPU, filesystem and network. What they share is the machine itself — and everything above is about making that sharing uneventful. --- # Variables and secrets Your app reads its settings from its environment. Alf has two kinds: - **Variables**: settings everyone in the organization may see, like `NODE_ENV=production` or `LOG_LEVEL=info`. - **Secrets**: passwords, API keys and tokens. The app gets them the same way, but once saved nobody can read them in the dashboard again, only replace or remove them. They're not given to builds. Both are stored encrypted. `PORT` is always set for you. ## Changing them Open the service's **Variables & secrets** tab, edit, and **Save changes**. Nothing restarts yet: a banner lists what changed ("`STRIPE_KEY` (secret) added · `LOG_LEVEL` changed") with **Deploy changes** and **Discard**. Deploying restarts the containers with the new values (the same image or commit, no rebuild). So you can change several things and send them out together. Project cards show "changes to deploy" while some are waiting. From the [command line](/docs/cli#variables-and-running-the-code-here): `alf env set KEY=value` sets and deploys in one go (`--stage` leaves it waiting), `alf env push` sets a `.env` file's lines on the service (passwords and keys as secrets, asking first), `alf env pull` writes the variables to a `.env` file, and `alf run -- ` runs your code on your machine with them, its databases connected. ### What other deploys do with waiting changes Every deployment says what it did with them, in the **Deployments** tab and in what the deploy command and agents get back: | Deploy | Waiting changes | | --- | --- | | **Deploy** button, [pushed code](/docs/deploying-code), an agent's `deploy` | go live with it ("Deployed with 2 staged changes…"), unless you ask to keep them staged (`--keep-staged`) | | A rollback, a settings change | stay staged ("…still staged and NOT in this deployment") | | A [candidate](/docs/deployments#candidates-try-a-version-before-it-s-live) | run in the candidate, and go live when it's promoted | ## Pasting a .env file **Paste a .env file** adds its `KEY=value` lines as variables. Move anything sensitive to secrets afterwards. ## Connecting services Use the project network's names in your values: ``` REDIS_URL=redis://cache:6379 ``` Values are taken as they are (one variable can't refer to another), so a connection string with a password goes in as a whole secret: ``` DATABASE_URL=postgres://app:the-password@db:5432/app (a secret) ``` ## Pointing at another service A variable can point at another service in the same project instead of repeating its value: ``` DATABASE_URL = postgres://app:${{db.PASSWORD}}@${{db.HOST}}:${{db.PORT}}/app ``` `db` is the other service's address part. The point is that the password lives in one place: change it on the database, deploy the app, and the app has the new one — nobody has to remember the second copy. Alongside whatever that service has set, four things about it are always readable: | | | |---|---| | `HOST` | its name inside the project | | `PORT` | the port it listens on | | `URL` | `https://…` if it's public, otherwise `http://host:port` inside the project | | `NAME` | what it's called | A service can point at **itself** for those four — that's how it learns its own address (`${{web.URL}}`) — but not at its own variables. Two things worth knowing. A reference resolves to what the other service has **stored**, and no further: if that value is itself a reference, it's left alone. And a reference to a service that isn't in the project is refused when you save it, rather than turning up in the container as the text you typed. --- # Volumes Files a container writes are gone after the next deploy, unless they're on a volume. A service with a volume stops its old container before the new one starts (two can't share the data), so its deploys have a gap of a few seconds; see [Deployments](/docs/deployments). A volume is for a database's files and other data a program keeps on disk. For files your app keeps for its users (uploads, exports, generated images), use a [bucket](/docs/buckets) instead: a service with a volume runs on one server, and one that keeps its files in a bucket can still scale out and move. ## Add one In the service's **Volumes** tab, give it a name, the path inside the container (e.g. `data` at `/var/lib/postgresql/data`) and its size. Volumes can also come from a [docker-compose file](/docs/compose) or [alf.toml](/docs/alf-toml#volumes). ## Its size A volume has the size you give it: 1 GB unless you say (a [database](/docs/databases)'s, its engine's). It's what the volume may hold, it's set aside for it on its server's disk, and it's what it's billed for, full or not: **$0.10 per GB a month**, so 10 GB is $1. Its size is a ceiling it can't be written past, so one service filling up can't fill its server's disk and take its neighbours down with it. If it fills up, its service is stopped and the volume shows **Full**: make it bigger, or delete data from inside the container, and the service starts again. **Size** under the volume changes it at once, without a deploy or a restart: bigger, as far as your plan allows (100 GB on Hobby) and its server has room, or smaller, down to what it holds. It's billed by its new size from then on. (A filesystem can't be made smaller while it's in use, so one made smaller is held to its new size by measuring it every half minute: the programs in it still see the bigger disk, and if it fills past the new size its service is stopped, as one that's full.) For a big import, make room for it first: [`alf db restore`](/docs/cli#a-copy-of-the-database) does it by itself from the file's size, and the API takes `room_for_gb` (room for that much, and a quarter again). ## Growing by itself A volume can also **grow by itself** before it's full: once it's 80% full it's given half as much room again (enough to be at most 60% full after), live, with nothing restarted, and is billed by its new size. It's off unless you switch it on under **Size** (**As it fills**) or give it a limit (`grow_up_to_gb` in the API, `grow_up_to` in [alf.toml](/docs/alf-toml#volumes)). It grows: - up to the limit you choose, which keeps what it may cost in hand; without one, as far as your plan lets a volume hold; - and only as far as its server's disk has room to promise. You hear about it on the notification channels that follow **A volume grew by itself**, and once when it can't grow any further. ## Good to know - A volume lives on the server that runs the service, so a service with a volume runs on **one server** and isn't moved automatically. Choosing another region for it [moves it there with its data](/docs/placement#services-with-a-volume). It also runs one container (no autoscaling). - Deleting a volume deletes its data on the server, for good, and its snapshots with it. - [Previews and environments](/docs/previews) get new, empty volumes, and databases and buckets of their own. - Back volumes up with [snapshots](/docs/backups): by hand before a risky change, or on a schedule. They're kept on the server; download the ones that must survive it. --- # Sleeping and waking Services can sleep when nobody uses them, and asleep their size costs nothing. A [volume](/docs/volumes) is billed by its size either way, awake, asleep or stopped, since its disk is kept. Asleep is **paused**: the app's virtual machine is frozen where it was, for as long as it sleeps. The next request lets it go on, and it answers almost at once. Nothing asleep is ever stopped; only you stop a service (see [Switching a service off](#switching-a-service-off)). For its first half hour asleep, an app keeps everything in memory. After that it sleeps **deeply**: its memory moves to the server's disk (swap) and comes back as the app touches it, so the first answers after waking take a little longer: a small app's first answer took about a tenth of a second instead of a few hundredths. That's what lets a server keep many apps asleep without holding their memory. A server short of memory (it keeps a quarter free) does the same sooner, for the apps paused longest. ```diagram sleep A service is awake while it answers requests, billed at its size. After 10 minutes without a request (you choose how long) it's asleep: paused, frozen where it was with its memory kept, its size not billed. After half an hour it's deeply asleep: its memory moved to the server's disk. The next request wakes it from either and is held until it answers, in about 40 ms when paused and about a tenth of a second for a small app deeply asleep; calls from the project's other services wake it too. Only Stop switches it off, and then no request wakes it until Start. ``` The first request after a deployment starts the new version instead. While it starts it gets two cores, whatever its own size, so it's up sooner; the extra cores aren't charged. Either way the request is held, not dropped, and answered once the app is up. If the app opens its port before it can serve (many frameworks do), give it a [health check path](/docs/deployments): the request is then held until that answers, instead of reaching an app that isn't ready. ## Choosing In the service's **Scaling** tab, **When nobody is using it**: - **Sleep**: after 1, 5, 10 (the default) or 30 minutes, 1 or 6 hours, or a day without requests. - **Keep running once started**: never sleeps after the first request. - **Always on**: starts right away and keeps running. ## Private services A private service always runs, unless you ask it to sleep: **Sleep — a call from inside the project wakes it** in its **Scaling** tab, or `sleep = "10m"` beside `public = false` in [alf.toml](/docs/alf-toml). It then sleeps like a public one, and a call from the project's other services wakes it (see below). A [database](/docs/databases) never sleeps. Only calls wake it, so keep these running: - A worker that takes jobs from a queue: nothing calls it. - Anything with a timer of its own (a scheduler inside the app, a polling loop): it's paused with the rest. [Scheduled commands](/docs/deployments#running-something-on-a-schedule) are fine: they run in a container of their own. - A service that pushes to its callers on its own (a WebSocket hub broadcasting, a gRPC stream fed by a timer). A connection nothing moves on doesn't keep it awake. `alf doctor` warns about a private service that sleeps and that nothing in its project has called for a week: most likely one of these. ## Calls from inside the project A sleeping service wakes for the project's other services too, not only for visitors: when another service calls it by name (`web:3000`), the call is held while the app wakes, then answered. Calls from inside also count as use, so a service that's only ever called by your own backend doesn't fall asleep in the middle of it. How: every app has a stable address inside the project, held by its server (its front), and its name resolves there. The server wakes the app if needed and passes the call on; HTTP and gRPC calls are kept with the service's requests, saying which service made them. Databases are reached at their containers directly, as they're always there. Good to know: - When an app wakes from sleep, the services on its server that it called lately wake beside it, so a chain of sleeping services (a web app calling an API calling another) wakes side by side rather than one after the other. One that isn't called after all goes back to sleep after half a minute. A service on another server wakes when the call gets there. - A connection that's been quiet for a while (a kept-alive connection in a pool) doesn't keep the service awake. It stays open while the service sleeps, and the next call on it wakes the service first. - This needs private networking on the server (WireGuard; the Servers page says if it's missing). The service's **Networking** tab says whether calls from inside wake it. - TCP wakes it. UDP is passed straight to the container: it reaches an app that's awake, and doesn't wake one that sleeps. - These calls pass through the server program, like visitors' requests do: when it updates itself (a few seconds), open connections from inside are cut and callers reconnect. HTTP callers keep their own connection to the server, so an app being deployed again or waking doesn't cut it. ## Switching a service off Sleeping is the service's own idea and the next request undoes it. Switching it off is yours, and nothing undoes it: **Stop** on the service's page keeps it down until you press **Start**. While it's off: - Its address answers with a page saying it's switched off, and no request wakes it. - An always-on service stays down too, and a crash isn't restarted. - It isn't alerted on — nobody should be paged for a service they turned off themselves. - Deploying to it is refused: start it first. - Nothing is deleted. Its container is kept, so it comes back with the filesystem it was stopped with, and so do its volumes, its variables and its address. While it's off its size isn't billed; its volumes still are, by their size. Agents can do the same with the `stop_service` and `start_service` tools, and the API with `POST /services/{id}/stop` and `/start`. ## Good to know - Apps that start slowly (large runtimes, migrations on start) make the first visitor after a deployment wait longer. Keep those running, or make startup lighter. - Background work inside a sleeping app stops too: it's paused. Run it in a private service that's always on instead. - A paused app wakes exactly where it was, its open connections included. If the other side closed one meanwhile (a database that drops idle connections), the app reconnects as it would after any network blip; most database libraries do that by themselves. - **Wake-ups** and **wake time** are charted on the service's **Overview**, under **All metrics**. --- # Scaling Three ways, which work together. All are under the service's **Scaling**, with the servers it runs on and what it does when nobody uses it: a bigger [size](/docs/usage#sizes) (more memory and cores for each container, given to it as it runs, with no restart), more instances, and more containers under load. Each container is billed at its size while it's awake. ```diagram scaling Three ways to scale, which work together. A bigger size gives each container more memory and cores, as it runs, with no restart (m to xl, say). More instances run one in each region you pick, each on a server of its own, like web in Europe and in the US; visitors reach the nearest awake one. More containers start under load, up to 20 per instance (#1, #2, #3 on one server), and when its servers are full another server in the region runs it too. Each container is billed at its size while it's awake. ``` ## Regions: an instance in each Under **Scaling → Regions**, pick the [regions](/docs/servers#choosing-regions) it runs in: it runs one instance in each, on a server of its own there (Hobby up to 3, Business up to 10). Visitors reach the nearest awake one; if a server goes down, the others answer. **Anywhere** is one instance, on the server Alf picks. [Where services run](/docs/placement) says how it chooses, and when instances move. The API and [alf.toml](/docs/alf-toml) can say more: `instances` (several, spread by themselves) or `region_instances` (`{"Europe": 2}`), for more than one in a region. ## Under load: more containers Each instance of a public service can start more containers when it's busy (**Scaling → Under load**): - **Up to so many containers per instance** (up to 20). - **Requests per second per container**: when the rate goes above this, another container starts; the proxy sends each request to the least busy one. When load stays lower for a minute, extra containers drain (finishing their open requests) and stop. **Scaling → Where it runs** lists each container under its server: its own (#1) and the ones added under load (#2 and up), with how long each has been up, its open requests, memory and CPU, and the requests per second they're for. The **Replicas** chart on its **Overview** shows how many over time. Each is billed at its size while it runs. ### Looking at one container The **Container** menu at the top of a service's **Overview**, **Logs** and **Terminal** picks one of them (or **Logs** and **Terminal** beside it under **Where it runs**): - **Overview** shows that container's numbers now, and its charts: the requests it answered, its response times, memory, CPU and traffic, and the time it ran. With all containers the charts are the service's: requests and traffic add up, memory and CPU are per container (the most any one held, what they used on average), as a size limits each. - **Logs → Live** follows that container's output; **History** shows only what it wrote. With all containers, each line in History says which one wrote it (`#2`). - **Terminal** opens in that container itself, which isn't stopped while the terminal is open. Coding agents pass `container` to `get_logs` and `search_logs` the same way. ## When its servers are full: another server in the region A server starts an extra container only while it has the memory for one. When every server a service is placed on in a region wants more containers than it has memory for, and has for half a minute, another server in that region with room runs it too: - It's **added under load**: it isn't one of the instances you asked for, and doesn't count against them. **Scaling → Where it runs** shows it with that label under its region. - Visitors near that region are spread over its servers there, the added one included, so it takes its share at once. Its first container may take a little longer if that server hasn't got the image yet. - It runs up to the containers left of the service's limit in that region: the service's **containers per instance** for each instance placed there, as it could run if its servers had the room. Another is added half a minute later if the region is still short. - A minute after the region can carry the load without it, it gets no new visitors, finishes the open requests, and goes. - It's billed like any container: its size, by the hour it's awake. - Only ever in a region the service already runs in. A new region is yours to choose: a server across the world would answer slower than one awake nearby. Services with a volume and private services run one container per instance and are never added to. Services with a volume and private services run one container per instance; a service with a volume runs one instance, where its data is. During a [deploy](/docs/deployments), the new version starts as many containers as are running now before it takes over, and with several instances one server switches first. ## Where to look - **Metrics → Requests per second** and **Replicas** for one service. - **Scaling → Where it runs** for each server it runs on, by region: where it is, whether it was added under load, how many containers it runs of how many it may, and each of them. - **Servers** for where each service runs and on how many servers, and whether a server is full right now. See [The servers page](/docs/capacity). --- # Preview environments A preview is a throwaway copy of a project at its own addresses: to try a pull request, or any change, without touching the real thing. ## What's copied - Every service, with its settings, variables and secrets. - Services built from code start with the code the original runs (or, if nothing of it is live, its newest push), built again with the copy's own variables. A static site's files are built the same way, so its pages point at the copy's services, not the original's. - Volumes: new and **empty** (see [data to try it with](#data-to-try-it-with)). - Release commands: the copy runs them too, so its database gets the original's migrations. - Custom domains: not copied. - Each service runs on one server, without autoscaling. ## Data to try it with A preview's databases start empty. Give the project a seed command in [alf.toml](/docs/alf-toml#previews): ```toml [previews] seed = "npm run db:seed" ``` Every new preview runs it once, after its app is first live (and its migrations have run): the app's own seed script, with made-up data rather than anybody's real data. `alf previews` shows each preview's seed: waiting for its app, running, done, or failed with its exit code (a failure is also sent where deploy failures go). ## Create one In a project, **+ Create → Preview environment**. Optionally give it a **name** (like `pr-12`), and choose how long to keep it: 6 hours to a week. Coding agents can create previews too. Then push code to it like to any project: its services have their own **Deploy new code**, and the deploy command takes `--preview `. ## A preview per pull request In CI, `--preview ` does both steps: it deploys to the project's preview of that name, and creates the preview the first time. `--end-preview ` ends it when the pull request closes. The [workflow for GitHub Actions](/docs/deploying-code#a-preview-for-every-pull-request) is ready to copy, and [`alf ci github`](/docs/cli#deploying-from-github) writes it for you, with a step that comments the preview's addresses on the pull request: one comment, updated with each push, and saying when the preview ended. It uses the job's own token, so there's nothing to install on GitHub. (`deploy.sh` hands the addresses to the steps after it as the output `urls`, one "service address" per line, for a workflow of your own.) ## Finding your previews - The sidebar shows each project's previews beneath it. - In a project, its name has a menu to go through it and its previews; **All previews** at its foot lists every one, under the project each copies, with when they go, **Keep 24 h longer** and **End**. - A preview's page says it's a preview, links to the original, and lets you keep it longer or end it. Anyone in the organization can end a preview. When its time is up, it's deleted with its services and their data. ## Environments A preview is a copy that expires, because CI makes one per pull request and nobody would clear them up. An **environment** is the same copy without the expiry: a staging you deploy to first, beside the production everyone uses. Nothing deletes it. Make one from a project's **Environments** page, in its sidebar. It gets the same services, settings, variables and secrets, its own addresses, and volumes that start empty. Custom domains stay with the original — point your own at the environment if you want one. ### Databases and buckets A copy — preview or environment — has everything the original has, its own of each: - **Databases** are copied as databases: the same engine, the same settings and credentials (so an app that wrote its database URL out in full still reaches it), and **no data**. An environment's database is backed up on the original's schedule; a preview's, which is thrown away, isn't. - **Buckets** are made again under the same name, each with a key of its own, and **empty**. - An app's references follow: `${{postgres.DATABASE_URL}}` and `${{uploads.S3_BUCKET}}` find the copy's database and bucket, never the original's. Production's data stays in production. To give a copy some data, restore a [snapshot](/docs/backups) you downloaded, or seed it the way you would a new database. ### Releases **Record what's running** writes down the version every service in an environment is on, under a name. Open a release to see exactly what it holds, and **Promote** puts those same versions into another environment. **Forget it** throws the record away; whatever was promoted from it carries on running. Only the versions move. The other environment's variables, secrets, domains and volumes are its own and aren't touched — that's the point, so production keeps pointing at the production database. A promotion is refused unless every service in the release exists there, so it can't half happen and leave a mixture running. A service built from pushed code is **built again** from the same commit rather than running the image that was tried, because an image belongs to the service that built it. That's the same source, not the same binary; the answer says which services were rebuilt. If that matters, build in CI and deploy a registry image instead. --- # Databases Add Postgres, MySQL, Redis or MongoDB to a project in one click: **+ Create → Database** on the project's canvas, then the one you want. It sits on the canvas beside the apps that use it, set up the way a database should be: - **The engine's official image** on its current major version: `postgres:17`, `mysql:8.4`, `redis:7` or `mongo:8`. A new deploy picks up its patch releases; nothing moves it to a new major version by surprise. - **A generated password**, kept as a secret. MySQL and MongoDB get a separate root password, so the app's never has root's powers (MongoDB's app user owns its own database, `app`, and nothing else). - **A disk of its own**: 1 GB for Postgres and Redis, 2 GB for MySQL and MongoDB, billed by its size ($0.10 a GB a month). Make it bigger as it runs, or let it grow by itself, under **Volumes**. - **Private and always running.** Only the project's own services reach it, and it never sleeps, so an app's first query doesn't wait for it to wake. - **In the region you choose**, where there's more than one: it runs on one server there, and stays there with its disk. See [choosing regions](/docs/servers#choosing-regions). - **Backed up from the start**: a [snapshot](/docs/backups) every day, the last week of them kept, and a copy off the server where the platform has somewhere to put one. ## Connecting an app Open the database and its **Connect** tab, choose your app and click **Connect**. The app gets one variable and is deployed with it: | Database | Variable | Looks like | | --- | --- | --- | | Postgres | `DATABASE_URL` | `postgres://app:…@postgres:5432/app` | | MySQL | `DATABASE_URL` | `mysql://app:…@mysql:3306/app` | | Redis | `REDIS_URL` | `redis://default:…@redis:6379` | | MongoDB | `MONGODB_URI` | `mongodb://app:…@mongodb:27017/app` | What's stored on the app is a [reference](/docs/variables-and-secrets), `${{postgres.DATABASE_URL}}`, not a copy of the URL. The app follows the database: change its password and the app has the new one with its next deploy, and the password is never pasted anywhere it could go stale. To use it in a variable of your own (a framework that wants the host and port apart, say), reference what you need: `${{postgres.HOST}}`, `${{postgres.PORT}}`, `${{postgres.POSTGRES_PASSWORD}}`. A second database of the same kind gets a name of its own: `postgres-2`, with its own password and its own URL. ## Working with it A prompt in it, a GUI client or a migration from your own machine, and backups: [working with a database](/docs/database-access) and [backups](/docs/backups). ## Good to know - **It's sandboxed like everything else.** It runs in a [virtual machine](/docs/security) of its own, with its own kernel, the same as your apps, and so does its terminal. That matters more for a database than it might seem: whoever has its password can run programs in it (Postgres's `COPY … FROM PROGRAM` is a shell). It costs a little speed on lots of small writes — a commit takes a couple of milliseconds — and a first start takes twice as long. - **Its size.** Postgres and Redis get xs (a quarter of a core, 256 MB: $1.50 a month), MySQL and MongoDB m (a core, 1 GB: $6; they hold a lot before they have any data). It never sleeps, so it's billed its size all month. Make it bigger under **Scaling** if it restarts under load: it's given the new size as it runs. - **MongoDB runs on its own, not as a replica set**, so it has no multi-document transactions. Mongoose and MongoDB's drivers work as they are; Prisma's MongoDB connector needs a replica set and doesn't, yet. - **One server.** A database lives on one server, with its disk. There's no replica or automatic failover yet: if that server is down, so is the database, and its snapshots are what bring it back somewhere else. - **Coding agents** can do the same with the `add_database` and `connect_database` tools. --- # Working with a database A database is private: only its project's services reach it, at `postgres:5432` and the like. For yourself there are two ways in, a prompt in its own container and your own machine, through the project's private network or a public port. ## A prompt in it From your terminal, with the [command line](/docs/cli): ```bash alf shell shop/postgres --live -- psql -U app app ``` For MySQL, `alf shell shop/mysql --live -- sh -c 'mysql -u app -p"$MYSQL_PASSWORD" app'`; for Redis, `alf shell shop/redis --live -- sh -c 'redis-cli -a "$REDIS_PASSWORD"'`; for MongoDB, `alf shell shop/mongodb --live -- sh -c 'mongosh -u app -p "$MONGO_PASSWORD" --authenticationDatabase app app'`. The **Connect** tab has the line with your names filled in. It runs in the database's own container (`--live`), where the client and the password are. In the dashboard instead: the database's **Terminal** tab, with *Its own running container* ticked, and `psql -U app app` there. ## From your own machine With [network access](/docs/network-access), the database opens on your machine without being put on the internet: a GUI client, `psql`, or a migration you run locally talks to it as if it were on `localhost`. ```bash alf connect shop postgres ``` opens it on `localhost:5432` (or a free port, if that one's taken) until you press Ctrl-C, so `psql postgres://app:…@localhost:5432/app` reaches the project's database. The password is a secret, so the dashboard doesn't show it, but the database's own container has it: `alf exec shop/postgres --live 'echo $POSTGRES_PASSWORD'` (`$MYSQL_PASSWORD` for MySQL, `$REDIS_PASSWORD` for Redis, `$MONGO_PASSWORD` for MongoDB), or `echo` it in its terminal. With `--all`, every service of the project is at its own name instead: `psql -h postgres.shop.internal -U app`. For something that can't run the command line (a hosted BI tool, a server elsewhere), give the database a [public TCP port](/docs/tcp-ports) under **Networking**, open only to the addresses you list. Anyone who can reach the port still needs the password. ## A copy of its data ```bash alf db dump # into postgres-2026-10-20-1430.sql, here alf db restore seed.sql # a .sql file into it ``` `dump` runs `pg_dump` (or `mysqldump`) in the database's own container and writes what it prints to a file on your machine, so nothing needs installing. The file loads into a database of your own: `psql -d myapp_dev -f postgres-….sql`, or `mysql myapp_dev < mysql-….sql`. It's made with `--clean`, so loading it replaces the tables it has. `restore` feeds a `.sql` file to the database's own client (`psql`, `mysql`) the same way, and stops at the first error. It says what it's about to do and asks first, then takes a [snapshot](/docs/backups) of the database, so what it changes can be undone (`--no-snapshot` doesn't). Both work for Postgres and MySQL (and MariaDB); Redis keeps no SQL, and its snapshots are its copies. For MongoDB they use `mongodump` and `mongorestore`, and the file is a MongoDB archive (`mongodb-….archive`) rather than SQL: `mongorestore --archive=mongodb-….archive` loads it into a MongoDB of your own, and `alf db restore` replaces each collection the archive has. --- # Backups (snapshots) A snapshot is a backup of a [volume](/docs/volumes): all its files as one archive. Take one before a risky change, put volumes on a schedule, and restore, download or delete snapshots in the service's **Volumes** tab. ## Take one Open the service's **Volumes** tab, choose **Snapshots** on the volume, then **Take a snapshot**. A note ("before the migration") helps you find it later. It shows as *Being taken…* and, a moment later, with its size. While its files are copied, the service is **frozen**: its processes stand still, requests wait, and everything carries on afterwards. That takes about as long as copying the data: under a second for a small app's files, a few seconds per gigabyte for a database. A service that's asleep is copied as it is, without waking it. ## Why freezing makes a database's snapshot safe Copying a database's files while it writes to them gives a copy that may not open. With the service frozen, the files are exactly what a power cut would leave behind, and that is something PostgreSQL, MySQL, SQLite, Redis and the like are built to start from: they replay their own log and come up with every committed change. It's the same guarantee a disk snapshot from a cloud provider gives. ## Restore **Restore** on a snapshot puts its files back into the volume: 1. The service is stopped. 2. The volume is emptied and filled with the snapshot's files, owners and permissions as they were. 3. The service starts again on them. Everything written since the snapshot is gone, so take a new snapshot first if you might want today's state back. The service is down for as long as the files take to copy. ## On a schedule Choose **Every hour**, **6 hours**, **12 hours**, **day** or **week** next to *On a schedule*, and how many to keep: older scheduled snapshots are deleted as new ones arrive. A smaller count, or switching the schedule off, applies at once: the older scheduled ones are deleted right away. Snapshots taken by hand are never deleted by the schedule. If a scheduled snapshot fails (the server is offline, its disk is nearly full), it shows in the list with the reason and is tried again an hour later. To hear about it, add **A scheduled snapshot of a volume failed** to a [notification channel](/docs/notifications). ## How many are kept Every snapshot is a whole copy of its volume, so they don't pile up. Per volume: | Taken | Kept | | --- | --- | | On the schedule | The newest *n*, as you set it (7 unless you choose otherwise). | | By hand | The newest 10. When an 11th is taken, the oldest of them is deleted. | | By a coding agent | 7 days, unless you **Keep** it, and at most the newest 10. | An agent's snapshot is a safety net for the change it's making, so it goes by itself after a week; it shows *deleted in …* until then. **Keep** makes it stay like one you took by hand (it then counts among those 10). Agents can't keep their own. Deleted means everywhere: the file on the server and its copy off the server. ## Where snapshots are kept On the same server as the volume, compressed. That makes them quick to take and to restore, and it protects against what usually goes wrong: a bad migration, a deleted table, an app that corrupted its files. It does **not** protect against losing the server or its disk. For that there are copies off the server. - Snapshots use the server's disk. One is never allowed to fill it: a snapshot that would leave less than 1 GB free is given up, and says so. - Deleting a volume (or its service) deletes its snapshots, and their copies. Download first if you want to keep them. ## Copies off the server Tick **Keep a copy of every new snapshot off the server** on a volume, and each snapshot (by hand, scheduled or an agent's) is sent to Alf's storage once it's taken. A single snapshot gets one with **Copy off the server**. It shows as *copy off the server* when it has arrived. ```diagram snapshots A volume, db's data, has its snapshots on the same server's disk: quick to take and to put back. With Keep a copy off the server, each snapshot is also copied to Alf's storage in the background. Restore or Download fetches the copy, onto whichever server the service runs on now. A snapshot covers what usually goes wrong, like a bad migration or a deleted table; a copy off the server covers losing the server. ``` A copy: - survives the server: if the server is lost, or you move the service to another one, **Restore** fetches the copy onto the service's current server and restores from it; - is what **Download** hands you, without loading the server; - is deleted with its snapshot (also when it's one more than a volume keeps), and with the volume; - counts as storage: **Billing → Usage** lists every copy, what it belongs to and its size, and what was held over the month. A copy goes up in the background after the snapshot; the service isn't paused for it. If it fails (the connection dropped, say), the snapshot itself is fine and says so: use **Copy off the server** to try again. ## Using a downloaded snapshot The archive holds the volume's files as the container saw them. Unpack it into an empty directory or volume and start the same image on it: ```bash mkdir pgdata && tar -xzf db-data-2026-10-11-09-00.tar.gz -C pgdata docker run -v "$PWD/pgdata:/var/lib/postgresql/data" postgres:17 ``` ## Good to know - One snapshot or restore of a volume at a time. - A snapshot without a copy belongs to its server: if the service later moves to another server, it can still be downloaded, but not restored there. One with a copy off the server can. - [Previews](/docs/previews) start with empty volumes and no snapshots. [Environments](/docs/previews#environments) start empty too, with the original's backup schedules. - [Coding agents](/docs/agent-tools) can take a snapshot before they change a database (`create_snapshot`), and restore one (`restore_snapshot`). - Servers need a current smartalfred-server; the dashboard says so when one is too old. --- # Computers A computer is a Linux machine of your own, in one of your projects: one container with a shell and a disk, isolated from everything else, with no address of its own. Use it for a coding agent to work in, or for anything you'd rather not run on your laptop — something you just wrote, something you downloaded, a build you don't quite trust, a job that takes all night. It lives in a project beside that project's services, databases and buckets, shows on its canvas with them, and reaches them by name from its shell (`psql postgres://…@postgres:5432`, `curl http://api:8080`). Make one from a project's **+ Create** menu, under **Computers** in the dashboard, or let an agent make its own with `create_computer`. (Computers were called sandboxes, and were each a project of their own, until October 2026. Each sandbox became a computer in a project of the same name.) ## Two kinds - **With Claude Code.** Debian 12 with `apt`, `git`, `curl`, build tools, Python and Node 22 — a machine you can actually work on. As it first starts, Claude Code is installed in it and given [Alf's tools](/docs/computer-agents#giving-an-agent-alf-s-tools) for the computer's project, and the computer restarts once so every shell has them. The computer's page shows each step; you can leave and come back, and it carries on where it left off. - **Empty.** The stock operating system and nothing else: **Debian 13**, **Ubuntu 26.04 LTS** or **Fedora 44**. Install what you need with its own package manager (`apt`, `dnf`). A coding agent can still be installed in it later, under **Add a coding agent**: whatever the system lacks for it (`curl`, `bash`) comes from its package manager first. Each kind is one fixed image, pinned, so every computer of a kind is the same machine. Or make one of **your own image** — anything Docker can pull, a devbox your team keeps on a registry, say: **Or an image of your own** in the dialog, or `alf computer create --image ghcr.io/you/devbox:latest`. It's set up as a machine like the rest as it starts (a user, sudo, SSH). Where there's more than one region, choose which it runs in when you make it; it stays there with its disk. See [choosing regions](/docs/servers#choosing-regions). ## What you get ```diagram computer A computer is a virtual machine of its own, always on: Linux (Debian, Ubuntu, Fedora or your own image), a user dev with sudo that shells, SSH and startup commands run as, a disk at /workspace of up to 100 GB that is dev's home and goes into snapshots, a system where what you install lasts (up to 30 GB), and an SSH server that's never open to the internet. It's in the project's private network and reaches the project's services by name, like postgres:5432, api:8080 and web:3000. It has no address of its own: nothing outside reaches it until you open a shell, SSH in, share a port, open a TCP port or join its network. ``` - **Everything in it lasts.** What you install with `apt` or `dnf`, what you change in `/etc`, what's in `/root` — all of it is still there after a restart, after a stop and a start, and after a change that needs a new container. Only choosing another system starts it afresh. - A disk at **`/workspace`** of the size you choose (up to 100 GB), billed by its size and made bigger as it runs. It's its user's home, so anything a tool writes to a dotfile — npm's global packages, an agent's config and sign-in, your `.bashrc` — is there, and its [snapshots](#snapshots) and copies take it along. - **A user of its own, `dev`, with `sudo`** and no password to type. Shells, SSH and startup commands run as `dev` (coding agents that refuse to run unattended as root are happy); `sudo -i` is root. On the node and Ubuntu images, the image's own first user becomes `dev`, so the files it owned stay its. - A shell in the computer itself: opening it gives it the whole window, with the shell filling it. Or [from your own terminal](/docs/computer-access#from-your-own-terminal), [over SSH](/docs/computer-access#ssh-vs-code-and-cursor) (VS Code, Cursor, scp, rsync), or with the `exec` tool. - No address, unless you ask for one. Nothing outside can reach it until you [share a port](/docs/computer-access#sharing-a-port) or [open a TCP port](/docs/computer-access#a-tcp-port-on-the-internet). - The same isolation every service here gets: a [virtual machine](/docs/security) of its own with its own kernel, hard memory, CPU and disk limits, and the project's private network. `sudo` makes you root inside that, not on the server. The first time a computer starts, it sets itself up — the user, `sudo`, an SSH server — which takes a minute; a shell opened before that is root's. A computer started before October 2026 gets all this when it's next restarted. ## It runs until you stop it A computer is always on. Nothing switches it off for being quiet, so you can leave an agent working in it, close the browser and come back tomorrow. (If you'd rather it were, `alf computer create --idle 60` makes one that's switched off after an hour with no shell open.) **Stop** in its **Settings** ends whatever is running in it and keeps its container and its disk; **Start** brings it back with everything in `/workspace` where you left it. Its size isn't billed while it's stopped (its disk is). While it runs it costs what a service of that size costs: m, a core and 1 GB, is $6 for a month running; see [sizes](/docs/usage#sizes). **It restarts when you say so.** Setting a key, putting it on the web or changing its port take effect without touching what's running: shells opened afterwards have them. **A new size, too:** **Settings → Size** has a slider over the sizes, xs to 3xl, applied with **Apply** as it runs (or `alf computer resize shop/computer --size xl`). A new computer is m (one core, 1 GB), or xl for a coding agent, until you choose otherwise. Giving an agent Alf's tools in the dashboard restarts it once, so every shell, and an agent that was already running, has the token. The few changes that need a new container (turning Docker on, say) wait: the page says so, and they apply when you restart it (**Settings → Restart**, or `alf restart`). The one restart outside your hands is the machine itself rebooting — the computer comes back on its own, with what it starts when it starts. To leave something running after you close the terminal, start it so it doesn't depend on the shell: `nohup claude -p "…" > run.log 2>&1 &`, or run it in `tmux`. Closing a terminal ends that shell and what it had in the foreground — not the computer, and not what you started that way. For something that should always run, make it a startup command. ## When it starts **Settings → When it starts** lists commands the computer starts as it starts, as `dev` in `/workspace`, and starts again whenever one ends — a dev server, a worker, a tunnel of your own. What they print is in its logs, with their name in front. From the terminal: ```bash alf computer startup shop/computer web -- 'cd app && npm run dev' alf computer startup shop/computer # what starts, and how each is doing alf computer startup shop/computer --remove web ``` Adding or changing one starts it at once; nothing restarts. For something on a timetable rather than always, a [scheduled command](/docs/services#scheduled-commands) on a computer runs in the computer itself, as `dev`, with what's installed there — like cron on a machine. ## Docker in it **Settings → Docker** (or `alf computer docker shop/computer on`) runs Docker in the computer: `docker build`, `docker run -p 8080:80`, `docker compose up` with its services finding each other by name. It takes a restart, and the first start installs Docker (a few minutes); after that it starts with the computer, and `dev` uses it without `sudo`. It's Docker inside a virtual machine, so a few things differ from your laptop: images are stored without overlayfs (they take more room on its system disk), and there's no `--privileged` and no kernel modules. Containers it runs reach the internet and each other, and `-p` publishes a port in the computer, which you can [share](/docs/computer-access#sharing-a-port) like any other. ## Its system disk What's installed outside `/workspace` — packages, toolchains, Docker's images — is on the computer's system disk, which can hold up to **30 GB**. Its **Settings** show how much it holds. A computer past that is stopped before it fills the server's disk, and says why; it starts again by itself ten minutes later, for you to make room (`sudo apt-get clean`, `docker system prune`, or move data into `/workspace`), and is stopped again if it's still past the limit. Data belongs in `/workspace`, which grows as it fills. ## Snapshots A computer's snapshots are of the whole machine: its `/workspace` and its system — everything installed in it — as they were. Putting one back restores both, and restarts it from what the snapshot kept. (A snapshot's copy off the server has its `/workspace` only.) See [snapshots](/docs/backups). ## Another like it **Settings → Make a copy** (or `alf computer copy shop/computer`) makes a new computer in the same project with everything installed in this one and everything in its `/workspace`, as they are now — set a machine up once, then make as many as you need. Its size, Docker and startup commands come along, and so do its variables; its secrets stay with the original. It's made on the same server, from the computer as it runs. ## How long it's kept A computer you make is kept until you delete it. If it's an experiment that should clear itself up, give it a time to go when you make it — a day, a week or a month — and it's deleted, with its disk, when that comes. **Keep it…** in its **Settings** changes that at any time. A computer a coding agent makes goes after a day unless the agent asks for longer, and an agent can keep one a week at most. Keeping one longer than that, or for good, is yours to decide. Deleting a computer deletes its disk. Its project, and everything else in it, stays. ## Usage **Usage**, at the top of the computer's page, has the same charts as a service's Overview: its memory, CPU and disk over the last hour, day or week. Tight on memory shows up there before it shows up as an agent being stopped. ## Reaching it, and agents in it - [Reaching a computer](/docs/computer-access): a shell from your own terminal, a port on the web, or SSH from your laptop. - [Coding agents in a computer](/docs/computer-agents): Claude Code, Codex or opencode in it, and Alf's tools for them. ## Good to know - A computer is a service underneath, of its own kind. Previews and environments made from its project leave it out, and so do releases: it's somebody's machine, not a version of the app. - After a new size, `free` and `/proc/meminfo` still show the memory it started with until it next restarts; what it may use is the new size at once. - A key you set, or the token for Alf's tools, is in every terminal you open afterwards. Terminals already open, and what was started from them, keep what they had. - Each time you open a computer, or click **Open a new shell**, you get a fresh shell with the computer's variables as they are now, so a key you just set is in it. - Deleting a computer deletes its disk and its system. There's no undo and no snapshot unless you took one. --- # Reaching a computer A computer has no address of its own, and nothing outside can reach it until you ask. The ways in: a shell from your own terminal, SSH (for VS Code, Cursor, scp and rsync too), files copied in and out, a port you share on the web, a TCP port on the internet, and the project's private network from your laptop. ```diagram computer-access Three ways in go through Alf, with nothing open on the internet. A shell: alf shell (or the dashboard) goes through Alf to the computer's server and opens a shell as dev. SSH: alf ssh goes the same way to the computer's SSH server on port 22. A shared port: anyone with the address, like …--5173.smartalfred.com, goes through Alf to the server and to localhost:5173 inside the computer. Two go straight to the computer's server. A public port: any client connects to the server's IP on a port between 20000 and 29999, and only the addresses on the allowlist get through to the port. Network access: alf connect joins your laptop to the project's network over WireGuard (UDP 51820), and every port of the computer is reachable. ``` ## From your own terminal With the [command-line tool](/docs/cli), it's one command, which the computer's page shows along the bottom (**Connect from your own terminal**) with its name filled in: ```bash alf shell shop/computer ``` Without installing anything, **Or a one-time line, nothing to install** there gives you a line to paste instead: ```bash curl -fsSL https://runalf.com/connect.sh | sh -s -- 'wss://api.runalf.com/api/v1/sessions/attach?token=…' ``` It opens a shell in the same container as the one on the computer's page, so what you start in one is there in the other. The window follows your terminal's size, and the shell's exit code is the command's. - The link is good for a few minutes and for one connection. Once you're connected it stays open for as long as you are; for another shell, get another command. - It needs `python3` on your machine, which does the talking. Nothing is installed and nothing is left behind. Closing the terminal — here or on the computer's page — ends that shell and what it was running in the foreground. The computer carries on, and so does anything you [started to outlive the shell](/docs/computers#it-runs-until-you-stop-it). ## Sharing a port A shell is where you build the thing you then want somebody to look at. Start it on a port — `npm run dev`, say — and share that port: it gets an https address of its own. ```bash alf share 5173 ``` prints the address, `https://----5173.smartalfred.com`. It answers within about 15 seconds, for as long as something listens on the port. - **Localhost works.** Most dev servers (Vite, for one) only listen on `localhost`. A shared port is reached from inside the computer, so that's fine as it is: no `--host 0.0.0.0` needed. - **Dev servers' host checks pass.** Requests arrive as `Host: localhost:`, which Vite, webpack's dev server and Angular's accept, with the address the visitor used in `X-Forwarded-Host`. Hot reload over WebSockets works. - **In its settings too.** **Ports**, in the computer's **Settings**, lists what's listening, with **Share** and **Stop sharing** for each port. `alf ports` shows the same in the terminal; `alf unshare 5173` takes a port off again. - **Agents know.** Installing Claude Code, Codex or opencode tells it about `alf share`, so asking an agent to "show me" ends in an address you can open. - **Nothing restarts.** Sharing a port, or stopping, doesn't touch what's running. **Anyone with the address can reach the port.** Nothing is shared until you or something in the computer asks. **Share every port automatically**, under **Ports**, shares each port as something starts listening on it, except debuggers' (9229, 9222), databases' (5432, 3306, 6379, 27017) and SSH's — a debugger's port lets whoever finds it run code in the computer. Those can still be shared by name. What you build in a computer goes to production as a service in the project, with its own domain there. ## SSH, VS Code and Cursor Every computer has an SSH server, reached through Alf — nothing is open to the internet, and whoever may open a shell in it may SSH into it. With the [command-line tool](/docs/cli): ```bash alf ssh shop/computer ``` logs you in as `dev`, the computer's user, with `sudo`. Anything after the name goes to ssh: `alf ssh shop/computer -L 3000:localhost:3000` forwards your port 3000 to the computer's, `-A` forwards your SSH agent (for `git push` from the computer with your own key), and `alf ssh shop/computer uptime` runs one command. For everything else that speaks SSH, make the computer an SSH host once: ```bash alf ssh-config shop/computer --add ``` It goes into `~/.ssh/config` as `computer.shop.smartalfred`, and then `ssh computer.shop.smartalfred`, `scp notes.txt computer.shop.smartalfred:`, `rsync -a site/ computer.shop.smartalfred:site/` work, and so does **Remote-SSH: Connect to Host…** in VS Code or Cursor, with the editor's server running in the computer. Without `--add` it prints the entry for you to put there yourself. - The key is the tool's own, made the first time (in its config directory) and given to the computer each time you connect. Your own keys work too: put them in `~/.ssh/authorized_keys` in the computer. - An SSH session gets the computer's variables, as a shell from the dashboard does. - The computer's host key is kept with it, so ssh knows it's the same machine next time. ```diagram ssh ssh (or VS Code, scp, rsync) runs alf as its ProxyCommand. The tool puts its own key in the computer's authorized_keys, then opens a stream signed in as you: a WebSocket over HTTPS to Alf, and on from there over the connection the computer's server keeps open. In the computer, alf pipe 22 connects that stream to the computer's SSH server on port 22, which is never on the internet, and it logs you in as dev. SSH runs end to end inside the stream, from your ssh to the computer's SSH server: Alf only carries the bytes. Whoever may open a shell in the computer may SSH into it. ``` ## Files in and out ```bash alf cp ./site shop/computer: # into /workspace alf cp ./notes.txt shop/computer:/tmp/ alf cp shop/computer:report.pdf . # out ``` One side is the computer and a path in it, as with scp; a path that isn't absolute is in `/workspace`, and a folder goes with everything in it. In the dashboard, drop files on the computer's shell to copy them into `/workspace`, or use **Files** at the top of its page for another folder, and to download a file (a folder comes as a `.tar`). ## A TCP port on the internet For something that isn't HTTP — a database you're trying out, a game server, an SSH server of a different kind — **Settings → Public ports** opens a TCP or UDP port on the computer's server to the addresses you allow, as for [any service](/docs/tcp-ports). It reaches the computer's port even if what listens there only listens on `localhost`. ## Every port, from your laptop With [network access](/docs/network-access), your laptop joins the computer's project network, and every port of the computer is reachable at its own name while it runs, without sharing anything on the web: ```bash alf connect shop --all ``` Then any program at `computer.shop.internal` (`computer` being the computer's name, `shop` its project's) — `ssh dev@computer.shop.internal` included, with a key in its `~/.ssh/authorized_keys`. Making the network interface needs `sudo`; the names go into `/etc/hosts` and come out again when you leave. --- # Coding agents in a computer A computer is a good place for a coding agent to work: a machine of its own, with a disk that lasts, that keeps running when you close the browser, and that can't touch anything outside its project. A computer made **with Claude Code** comes with it, and with Alf's tools for its project; any computer can have Claude Code, Codex or opencode added. ## A coding agent in it A computer made **with Claude Code** has it set up by itself. For any computer, **Claude Code**, **Codex** and **opencode** install with one button under **Agent & tools** at the top of its page. They go into `/workspace`, so they're still there after the container is replaced, and you run `claude`, `codex` or `opencode` in the shell. Each one needs to be signed in. Either put its provider key in the field under it — kept as a secret of the computer, so the agent finds it in its environment — or leave it empty and sign in from the shell the way you would anywhere else. The computer shows what each command printed, whether it worked or not. If an install or a setup command fails, the output is there and you can run it yourself in the shell. **Installs and updates can't leave a broken agent.** Each one is downloaded whole, checked, and only then put in place, in one step, into `~/.local/bin`. A restart or a closed terminal in the middle leaves the version you had, still working. Claude Code updates itself the same way; Codex and opencode get their newest version with **Install again**, which also clears away anything a half-finished install left behind. An agent only shows as installed once it actually runs. ## Giving an agent Alf's tools **Alf's tools**, in **Agent & tools**, points an agent in the computer at Alf's own MCP endpoint, so it can deploy, watch and undo for you. A computer made with Claude Code has this done for its own project as it starts. Choose **which project** it may touch — the computer's own to begin with, another one, or the whole organization — and whether it may change things or only look. It gets an [agent token](/docs/agents) of its own, scoped to exactly that, and the usual [approvals and limits](/docs/agent-safety) apply. The token is kept as a secret of the computer and reaches the agent as an environment variable. **It is never written into a config file**: what's written names the variable. Giving the tools from the dashboard restarts the computer once, so every shell has the token and an agent that was already running starts again with it. ## For coding agents | Tool | What it does | | --- | --- | | `create_computer` | Makes one, in the token's project (or a new one), and returns its `computer_id` | | `exec` with `service_id` (the `computer_id`) and `live: true` | Runs a command in it, where each command sees what the last one left | | `list_computers` | What's there, with state and expiry | | `extend_computer` | Keeps one longer, a week at most | | `delete_computer` | Deletes it and its disk (needs a token that may delete) | | `exec` running `alf share 3000` | Shares a port it started, and prints the address | A token limited to one project makes computers in that project and only sees those. See [agent safety](/docs/agent-safety). --- # Static sites A static site is a service whose files are served by Alf itself, not by a container: the HTML, CSS, JavaScript and images that `npm run build` puts in `dist`, or a whole site generator's output. It's built from a Dockerfile like any other service, on one of your servers. Its site folder is then copied out of the image, and after that nothing of it runs anywhere. It doesn't sleep, so there's never a cold start, and you pay only for the storage its files take. On the canvas it's a service like the others, with its own address, [custom domains](/docs/domains), deployments and rollbacks. ## Make one - **+ Create → Static site**: name it, pick where it comes from (code you push, or a published image), and say which folder of the image holds the site. Then push its code with the command shown, as for [any service](/docs/deploying-code). - **From a Dockerfile**: add `LABEL smartalfred.static=`. A service built from it is published as a static site from its next deploy on, nothing else to set. - **From a compose file**: its [labels](/docs/site-builds#in-a-compose-file). - **An existing service**: **Settings → Serve as a static site**. It's deployed again as a site, and its container goes. ## Deploys and rollbacks Each deploy builds the image, copies the site folder out, and sends Alf only the files it doesn't have already, so a deploy that changed one page uploads one page. It goes live all at once when every file is there: a visitor gets the old version or the new one, never half of each. If a deploy fails, visitors keep the version before it. Rolling back is instant. The files of the newest 10 deploys are kept, so going back to one of those switches to its files with nothing built. An older deploy is built again from its code. ## Limits and cost A site can hold up to 20,000 files and 2 GB, and each file can be up to 25 MB. A static site has no volumes, can't be private, and takes no memory on any server: it's built on one, and that's all. Its files count as storage, like a volume's contents: each distinct file once, however many of the kept deploys use it ([GB-months](/docs/usage#storage)). Requests to it cost nothing. ## More about sites - [Building a site](/docs/site-builds): the Dockerfile, its variables, and sites in a compose file. - [Paths and caching](/docs/site-paths): which file a path gets, single-page apps, and what browsers keep. --- # Building a site A static site is built like any other service: from a Dockerfile, on one of your servers, in [a virtual machine](/docs/security). Then its site folder is copied out of the image and published, and nothing of it runs. What matters is which folder holds the site, and that its variables reach the build. ## The Dockerfile Build the site in one stage, and copy what it made into the last one: ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . ARG VITE_API_URL RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html LABEL smartalfred.static=/usr/share/nginx/html ``` nginx never runs here: the image only carries the files. Ending on it means the same Dockerfile still makes a working container anywhere else, like on your laptop. Just the files works too: `FROM scratch`, `COPY --from=build /app/dist /` and `LABEL smartalfred.static=/`. What Docker adds to every container (`/.dockerenv`, `/etc/hosts` and the like) is left out. Each of its steps runs in [a virtual machine](/docs/security), like every build here. ## Variables A static site has nothing running to read an environment, so its [variables](/docs/variables-and-secrets) go to its build instead, as build args. The Dockerfile picks up the ones it wants with `ARG` (above, `VITE_API_URL`). Vite takes variables starting with `VITE_`, Next.js those starting with `NEXT_PUBLIC_`. An app that runs (a Next.js or Nuxt server) gets its variables when it starts, and those named like these (`VITE_`, `NEXT_PUBLIC_`, `NUXT_PUBLIC_`, `PUBLIC_`, `REACT_APP_`, `VUE_APP_`, `GATSBY_`, `EXPO_PUBLIC_`) at its build as well, the same way: changing one of them builds it again. Everything in a static site is public, so a site's **secrets** aren't given to its build. A variable can point at another service in the project, as in `VITE_API_URL=${{api.URL}}`. That reference can read the other service's `HOST`, `PORT`, `URL`, `NAME` and variables, but never its secrets or a bucket's keys. A reference to one of those stays as it was written. Changed variables are staged like any service's. Deploying them builds the site again. ## In a compose file | Label | Meaning | | --- | --- | | `smartalfred.static: /usr/share/nginx/html` | A static site, served from that folder of the built image. | | `smartalfred.static: "true"` | A static site; the image's own `LABEL smartalfred.static` says which folder. | | `smartalfred.spa: "true"` | SPA mode. | A static site is public whatever its `ports:` say. Its `environment:` becomes its variables, and so its build args. --- # Paths and caching A static site is answered by Alf's edge, from its live deploy's files, like a static host would: which file a path gets, what a single-page app needs, and how long browsers keep what they got. It answers at its own address and at its [custom domains](/docs/domains). ## How paths are answered | A request for | Gets | | --- | --- | | `/` | `index.html` | | `/docs/` | `docs/index.html` | | `/docs` (a folder) | A redirect to `/docs/` | | `/about` | `about`, or else `about.html` | | A path that's no file | The site's own `404.html` with status 404, if it has one | **SPA mode** (in its settings) is for single-page apps that handle their own routes: a path that isn't a file gets `index.html` instead, with a 200. Files are sent with the right content type, an ETag, and support for range requests. A file whose name carries a content hash, like `assets/index-B1x9Qz7c.js` from Vite or webpack, is cached by browsers for a year, because its name changes when its content does. Everything else is checked again on each visit, so a deploy shows at once. --- # Buckets A bucket keeps an app's files outside its container: what its users upload, exports it writes, assets it generates. It speaks S3, so every S3 client and SDK works with it. Keep files in a bucket rather than a [volume](/docs/volumes) when you can. A service with a volume runs on one server: it can't scale out across servers, and it can't move when its server goes. A service that keeps its files in a bucket stays free to sleep, scale out and move. ## Make one On the project's canvas, **+ Create → Bucket**, and give it a name, like `uploads`. It sits on the canvas beside your services, with how much it holds, and a dashed line from each app that uses it. Click it to open its page. Making a bucket makes a key for it, and shows the key's secret **once**. You only need it for a tool on your own computer: apps get it by reference (below). ## Connecting an app In the bucket's window, on **Connect**, choose the app and click **Connect**. The app gets five variables and is deployed with them: | Variable | Holds | | --- | --- | | `S3_ENDPOINT` | `https://storage.smartalfred.com` | | `S3_BUCKET` | The bucket's name over S3, like `uploads-k3m9x2ab` | | `S3_REGION` | `auto` | | `S3_ACCESS_KEY_ID` | The key's id | | `S3_SECRET_ACCESS_KEY` | The key's secret | What's stored on the app are [references](/docs/variables-and-secrets) like `${{uploads.S3_BUCKET}}`, not copies. Give the bucket a new key and the app has it with its next deploy. Then use them in your code with any S3 SDK: [buckets in your code](/docs/bucket-code). ## Its files The bucket's **Files** tab shows what's in it a folder at a time, with each file's size and when it changed; folders are how keys read up to each `/`. **Open** gives you the file through a link signed with the bucket's key and good for ten minutes; **Delete** removes it for good. The page's header says how much the bucket holds and roughly what that costs a month. ## Good to know - One upload in a single request can be up to 100 MB. Larger files go up in parts, which every S3 SDK does by itself for large files. - Storage is billed like volumes, by the gigabyte-month of what's held. Requests and data transfer aren't billed. - **New key** makes a new key and shows its secret once. The old key keeps working for a day, so connected apps carry on until their next deploy gives them the new one. - Deleting a bucket deletes every file in it, for good. So does deleting its project. - A project can have up to 10 buckets. - A bucket's name can't be the name of a service in the same project: references name both the same way. - Agents have the same: `create_bucket`, `connect_bucket`, `list_buckets`, `rotate_bucket_key` and `delete_bucket` in the [agent tools](/docs/agent-tools). --- # Buckets in your code A bucket speaks S3, so the SDK you'd use for Amazon S3 works with it, given the five variables [connecting an app](/docs/buckets#connecting-an-app) puts in its environment. Two settings differ from Amazon's defaults: **path-style** addressing, and the region `auto`. ## In your code Buckets use **path-style** addressing and the region `auto`. With the AWS SDK for JavaScript: ```js import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.S3_REGION, forcePathStyle: true, credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, }, }); await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: "avatars/ada.png", Body: file })); ``` With boto3 (Python): ```python import os, boto3 from botocore.config import Config s3 = boto3.client( "s3", endpoint_url=os.environ["S3_ENDPOINT"], region_name=os.environ["S3_REGION"], aws_access_key_id=os.environ["S3_ACCESS_KEY_ID"], aws_secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"], config=Config(s3={"addressing_style": "path"}), ) ``` From your own computer, the AWS CLI works with the key you were shown: ```bash aws s3 ls s3://uploads-k3m9x2ab --endpoint-url https://storage.smartalfred.com --region auto ``` ## Files for browsers Buckets aren't public: every request is signed with the bucket's key. To let a browser download a file, or upload one straight to the bucket, give it a **presigned URL**, made by your app with its SDK (`getSignedUrl` in JavaScript, `generate_presigned_url` in boto3). Presigned URLs work from any website: there's no CORS to set up. ## What works Objects in and out, with metadata, ranges and conditional requests; copying within the bucket; listing (with prefixes and delimiters, so folders work); deleting many at once; multipart uploads for large files; presigned URLs. Not supported: public buckets, versioning, object tags and ACLs, lifecycle rules, and copying between buckets (a key opens one bucket). --- # Networking and ports ## Public services A public service answers at `https://{service}-{project}-{organization}.smartalfred.com`, with HTTPS. Add [your own domains](/docs/domains) too. Visitors reach the nearest server running it. Other organizations' apps are on smartalfred.com too, so cookies aren't shared across it: a cookie your app sets with `Domain=smartalfred.com` is kept to its own address. To share cookies between your services, give them [your own domain](/docs/domains). ```diagram request A visitor's request goes over HTTPS to Alf's edge, which holds your address, your domains and their certificates. From there it travels over the connection the nearest server running the service keeps open to Alf, so nothing on the internet connects in to the server. The server wakes the app if it's asleep, holding the request meanwhile, and passes it to the app's port in its own virtual machine. A public TCP or UDP port is the other way in: it skips the edge and goes straight to the server. ``` ## Private services Choose **Private** under the service's settings (or when creating it): it gets no address on the internet. The project's other services reach it as `name:port`, e.g. `db:5432`. Use this for databases, caches, queues and workers. A private service always runs, unless you let it [sleep](/docs/sleeping#private-services): then a call from the project's other services wakes it. To reach a database from outside the project (from your laptop, or another platform), open a [public port](/docs/tcp-ports) for it (TCP or UDP): an address like `db-shop-acme.tcp.smartalfred.com:20431`, open only to the IP addresses you list. ## WebSockets A public service's address takes WebSockets as it takes anything else: connect to `wss://{service}-{project}-{organization}.smartalfred.com` and your app sees an ordinary upgrade. Your own domains work the same way. Nothing has to be switched on. A service with an open socket counts as in use, so it won't fall asleep underneath one. It can sleep again once the last socket closes. Two things worth knowing: - The app has to accept the upgrade. If it answers with plain HTTP, the visitor gets a `502` instead of a socket that opens and shuts. - Messages are passed whole, and very large ones (over about a megabyte) are refused. Send big things over HTTP and keep the socket for messages. For a protocol that isn't HTTP or WebSocket — a database, a game server — open a [public port](/docs/tcp-ports) instead. ## The project network All services of a project share a private network: - They reach each other by name (the service's short name, shown in the service's **Networking** tab), on whichever servers they run. - Between servers, traffic goes through an encrypted WireGuard tunnel, set up by itself between any two servers that run the same project. The server program opens UDP port 51820 in the machine's own firewall for it. A firewall in front of the machine — a cloud provider's security group or security list — has to allow UDP 51820 too, on at least one of the two servers and ideally on both. If a server can't run WireGuard, or isn't connected to the others, the Servers page says so. - Services in other projects, and the rest of the internet, can't reach them. - A call to an app (any service that isn't a [database](/docs/databases)) goes through its server's front: a stable address the name resolves to. Its requests are kept with the ones from the internet (`alf requests api --from inside`), and an app that sleeps [wakes for it](/docs/sleeping#calls-from-inside-the-project). HTTP and gRPC are read request by request; anything else (TLS, a protocol of your own) passes through as it is. UDP goes straight to the container. - The app sees the call coming from its server's address. For HTTP, `X-Forwarded-For` says which container called. Databases are reached at their containers directly, so they see their clients' own addresses. ```diagram project-network Project shop runs on two servers, fsn-1 in Falkenstein and nbg-1 in Nuremberg. On fsn-1, web calls db:5432; on nbg-1, api calls db:5432 too, and a private worker runs. The two servers' parts of shop's network are joined by an encrypted WireGuard tunnel over UDP 51820, set up by itself, so the names work on whichever server a service runs. Another project's app on fsn-1 can't reach shop's services. A call to an app goes through its server's front, which keeps it with the app's requests and wakes the app if it sleeps; db, a database, is reached at its container directly. ``` ## Ports Your app listens on one port inside its container: - Set **Port** on the service, or leave it empty: then the image's `EXPOSE` is used, or 8080. - The app also gets the port as `PORT`, so apps that read it work anywhere. If a deploy ends with "didn't open its port in time", the app listens on a different port than the service says. See [Troubleshooting](/docs/troubleshooting). --- # Custom domains Serve a public service at your own domain or subdomain, like `shop.example.com`, with a certificate issued for you. ## Add one 1. Open the service's **Networking → Custom domains** and add the domain. 2. At your domain's DNS provider, create the record it shows: | Type | Name | Target | | --- | --- | --- | | CNAME | `shop` | `domains.smartalfred.com` | The name is the part before your domain (some providers want the whole hostname). 3. Wait for it: the status goes from **Waiting for DNS** to **Issuing certificate** to **Live**, usually within a few minutes. **Check** asks again right away and shows what your DNS says now. ```diagram domain You add shop.example.com under the service's Networking, Custom domains. It waits for DNS until your DNS provider has the CNAME record shop pointing at domains.smartalfred.com. Once that's seen, a certificate for HTTPS is issued for you. Within minutes it's live, and https://shop.example.com answers. ``` From the [command line](/docs/cli#domains), `alf domains add shop.example.com` does the same: it says the record to create and waits until the domain is live with HTTPS. ## Root domains A root domain like `example.com` can't have a CNAME at most providers. Use an ALIAS or ANAME record pointing at `domains.smartalfred.com`, or CNAME flattening (automatic on Cloudflare). Or use `www.example.com` instead. ## Domains on Cloudflare If your domain's DNS is on Cloudflare, set the record to **DNS only** (grey cloud). ## Good to know - A domain can belong to one service at a time. - Private services can't have domains; a service with domains can't be made private until they're removed. - Removing a domain, or deleting its service or project, frees it at once. --- # Public TCP and UDP ports HTTP services have an address and need nothing from this page. A **database**, a message broker, a game server or anything else that doesn't speak HTTP can't go through that address. To reach one from outside its project, open a **public port**, over TCP (databases, most things) or UDP (game servers, DNS, VPNs, media). Inside the project nothing changes: other services keep reaching it as `name:port` (e.g. `db:5432`), and that stays the right way for your own apps. ## Open a port **Service → Networking → Public ports → Open a port**: 1. **Port in the container**: the port the app listens on (5432 for Postgres), and **TCP** or **UDP**. Left empty, it's the service's port. 2. **Who may connect**: the IP addresses or ranges that get through, e.g. `203.0.113.7` or `198.51.100.0/24`. **Add my address** fills in yours. Everyone else is cut off before they reach the app. You get an address like: ``` db-shop-acme.tcp.smartalfred.com:20431 ``` Connect with that host and port: ``` psql "postgres://user:password@db-shop-acme.tcp.smartalfred.com:20431/mydb" ``` The port number is picked for you (between 20000 and 29999) and stays the same if the service moves to another server. Private services can have public ports too: they stay without a web address. ## How it reaches the server HTTP goes through Alf's edge; a public port doesn't. Connections go **straight to the server** the service runs on, so: - The name points at the IP address of the server the service runs on (visible to anyone who looks it up). - The server's own firewall is opened for the port automatically. - The firewall in front of Alf's servers lets the range for public ports through (TCP and UDP `20000-29999`). **Test** next to a TCP port connects to it from outside and tells you which of the three it is: reachable, refused by the server, or blocked on the way. ```diagram tcp-port psql on your laptop connects to db-shop-acme.tcp.smartalfred.com:20431. The name is the IP address of the server db runs on, and port 20431 is open in that server's own firewall. Only addresses on the port's allowlist get through; everyone else is turned away, and listed under Turned away lately. Then the connection, TCP or UDP, reaches port 5432 in db's container. HTTP goes through Alf's edge; a public port doesn't, and Alf adds no TLS: switch it on in the database. ``` ## Who may connect Every port has an allowlist, and it can't be empty. - Change it any time under **Who may connect**. Connections from addresses you remove are cut at once. - **Anyone on the internet** is a choice you make explicitly. Scanners find open databases within hours: only with a strong password, and switch on TLS in the database. - The allowlist looks at the address a connection comes from. At home or in an office that's your router's public address, which can change. ### "Why can't I connect?" Almost always: the address you connect from isn't the one on the list. Your computer can have two public addresses, an IPv4 and an IPv6 one, and the server sees the one of the kind it's reached over (usually IPv4), which may not be the one your browser uses. So each port shows **Turned away lately**: the addresses that tried it without being on the list. Try to connect once, find your address there, and press **Allow**. Addresses you don't recognize are usually scanners: leave them out. Traffic is as encrypted as the app makes it. Alf forwards the bytes and doesn't add TLS: enable it in your database (most clients then use it by default) if you connect over the internet. ## UDP UDP ports work the same way: a port, an allowlist, the same name. What's different: - UDP has no connections. A client is remembered while it sends or receives, and forgotten after two minutes of silence. - **Test** can't check a UDP port (silence doesn't mean closed): try your app's own client, and look at **Turned away lately**. - The address a UDP packet claims to come from can be faked, so the allowlist is weaker than with TCP. Don't rely on it alone for anything sensitive. - The name contains "tcp" (`...tcp.smartalfred.com`); it's the same name for both. ## Your own domain Create one DNS record at your domain's provider: | Type | Name | Value | | --- | --- | --- | | CNAME | `db.example.com` | `db-shop-acme.tcp.smartalfred.com` | DNS only (no proxy), and no certificate is involved. Then connect to `db.example.com:20431`: the port stays part of the address. If the service moves to another server, the name follows by itself. ## Good to know - **Sleeping services** wake on the first connection and stay awake while one is open. Databases are usually private and always on anyway. - **Several servers**: the name points at all of them, and each forwards to its own copy. For a database with a volume there's exactly one. - **Deploys and updates**: connections are cut when the container is replaced, and for a moment when smartalfred-server updates itself. Clients with a connection pool reconnect by themselves. - **Limits**: 5 public ports per service. - Servers need an address that can be reached from the internet. One behind NAT without port forwarding can't offer public ports. --- # Network access Your laptop, or any other machine, can join one project's private network: its services — the private ones included, like a database with no address of its own — are then reachable from it as if it were one of them. It's how you point a database tool at production's Postgres, run a script against a private API, or debug something that only answers inside the project. ```bash alf connect shop ``` ```text In shop's network as 10.88.128.1. Its services, here: SERVICE ON THIS MACHINE IN SHOP db localhost:5432 10.128.4.2:5432 web localhost:80 10.128.4.3:80 ✓ Connected to fsn-1. Ctrl-C to leave. The device stays registered for next time. ``` Each service opens on `localhost`, on the port it listens on (or a free one, if that's taken), until you press Ctrl-C. `psql postgres://app:…@localhost:5432/app` then talks to the project's database. The list has the rest of the project too: a computer (which has no one port to open: name one, or use `--all`), and static sites and buckets at their public addresses, since they aren't anything to tunnel to. ## Every port, at each service's own address ```bash alf connect shop --all ``` ```text SERVICE ADDRESS INSIDE postgres postgres.shop.internal 10.128.4.4 (listens on 5432) web web.shop.internal 10.96.8.2 (listens on 8080) computer computer.shop.internal 10.128.4.5 ``` With `--all` the project's network becomes a network interface of your machine while it runs: every port of every service, at its own address, for any program — `psql -h postgres.shop.internal`, a browser at `http://web.shop.internal:8080`, `ssh root@computer.shop.internal`. The names go into `/etc/hosts` and come out again when you leave. Making an interface needs root, so it asks for `sudo`; only that part runs as root, and your sign-in and the device's key stay yours. A [computer](/docs/computer-access#every-port-from-your-laptop) has an SSH server already: `ssh dev@computer.shop.internal`, with your public key in its `~/.ssh/authorized_keys` (or use `alf ssh`, which needs no network access at all). ## What happens The first time, the [command-line tool](/docs/cli) makes a [WireGuard](https://www.wireguard.com) key on your machine and registers the **device** with the project. Only the public half of the key is sent; the private half never leaves the machine. The device gets an address of its own in the project's network, and every server the project runs on adds it as a peer. WireGuard runs inside the tool, with a network stack of its own: no `sudo`, no app, no kernel module, and nothing else on your machine sees the project's network. Only what `connect` opens on `localhost` reaches it. The connection goes out over UDP to the servers' port 51820, which most networks let through; it's kept alive every 25 seconds, so it works from behind NAT and over Wi-Fi that changes. ```diagram network-access On your laptop, programs like psql or a browser connect to localhost:5432, where alf connect listens. The tool runs WireGuard inside itself, with its own network stack: no app and no kernel module. It talks over UDP 51820, kept alive every 25 seconds, to each server project shop runs on. There the device is in shop's network: db at 10.128.4.2:5432, and web, woken by the first connection if it's asleep. What it sends to other projects or to the server itself is dropped. The device's key is made on the laptop and only its public half is sent; every server the project runs on adds the device as a peer until its time is up or it's revoked. ``` ## What a device can reach - **This project's services, and nothing else.** Each server lets a device into the project's network on that server, and drops everything else it sends: other projects (yours or anyone's on a shared server), and the server itself — its SSH, its own ports, everything. - **By their addresses inside the project**, the way the project's services reach each other. Their public addresses work as always. - **A service that sleeps** is woken by the first connection, the way the project's own services wake it: a device reaches it through its front address, which the server holds while it's asleep. The first connection takes as long as the service takes to start. ## Pick services and ports ```bash alf connect shop db # only db, on the port it listens on alf connect shop db=15432 # db on localhost:15432 alf connect shop worker:9000 # a port the service didn't say it listens on ``` ## How long a device works **30 days** from when it's made, unless you say otherwise: ```bash alf connect shop --days 7 alf connect shop --forever # until it's revoked ``` In the project's **Settings → Network access**, **Keep it…** changes that at any time: 7, 30 or 90 days from now, or for good. When a device's time is up it stops working, and `connect` makes a new one the next time. ## Seeing and revoking devices The project's **Settings → Network access** lists every device: whose it is, its address, whether it's connected now — to which servers, and from which address — or when it last was, and until when it works. Devices also show on the project's canvas, under the servers they're connected to. **Revoke** takes a device off the network at once, wherever it is: every server drops it within seconds. Revoked and expired devices stay in the list (**Show revoked and expired ones too**), so it still says who had access, and until when. - `alf disconnect shop` revokes this machine's device and forgets its key. - `alf devices` lists the devices in all your projects; `alf devices revoke ` revokes one. - **Someone who leaves the organization** takes no access with them: their devices are revoked when they're removed. Joining a device is an owner's or an admin's to do, as opening a shell is. Its owner, or an admin, can revoke it. ## On another machine ```bash alf connect shop --config > shop.conf ``` prints a WireGuard configuration for the device instead of connecting: for the WireGuard app, or `wg-quick up ./shop.conf` on a server of your own. With it, the whole of the project's network is routed (not only `localhost` ports), and the services' addresses are listed at the top of the file. **It has the device's private key in it:** keep it to yourself, and revoke the device if it gets out. ## Good to know - A device is one machine in one project. Another project, another device: `alf connect blog`. - A server too old for devices doesn't get them (servers update themselves, usually within a day): until then, what runs on it can't be reached, and `connect` says so. - If `connect` says **No answer yet**, the network you're on doesn't let UDP out to port 51820. Another network (a phone's hotspot, say) usually does. --- # Your own error page When one of your apps can't answer, the visitor gets a page that says so. By default it's Alf's. Give your organization a page of its own and your visitors get that instead: at your apps' smartalfred.com addresses and at your [own domains](/docs/domains). ## Set it In **Settings → Error page**, write the HTML or choose a file, check the preview, and save. It shows within a few seconds. **Remove** goes back to Alf's pages. It's one page for the whole organization, and only admins can change it. ## When it's shown Whenever a request reaches Alf but not your app: | What happened | Status | `{{code}}` | | --- | --- | --- | | The app was asleep and didn't wake in time | 503 | `did_not_wake` | | Its first version isn't deployed yet | 503 | `not_deployed` | | It isn't on a server yet | 503 | `not_placed` | | Its server isn't connected | 503 | `server_offline` | | It's running but didn't answer | 502 | `no_answer` | It is **not** shown: - for errors your app answers with itself (its own 404s and 500s are its business); - at an address with no app behind it, since there's nobody's page to show; - while the organization is paused by Alf; - to programs: anything that doesn't ask for HTML keeps getting the facts as JSON or plain text. The status code and the headers (`Retry-After`, `X-Smartalfred-Page`) are the same as with Alf's page, so monitoring keeps working. ## Saying what happened It's one HTML document for every case. These are filled in each time, wherever they appear in it: | Field | What it holds | | --- | --- | | `{{title}}` | What happened, in a few words: "This app is taking too long to wake up" | | `{{message}}` | A sentence or two for the visitor | | `{{status}}` | The HTTP status: `503` | | `{{code}}` | Which case it is, in a word: `did_not_wake` | | `{{host}}` | The address the visitor asked for | | `{{retry_after}}` | Seconds until it's worth trying again; empty when waiting won't help | The values are escaped, so they're safe anywhere in the HTML. Anything else in double braces is left as it is. A page that uses none of them is fine too: the same words for everything. ## Trying again by itself Most of these pass within seconds (an app waking, a deploy finishing). Alf's page reloads by itself; yours does if you ask for it: ```html ``` When there's nothing to wait for the field is empty, and the browser ignores the tag. ## Good to know - Up to 256 KB. Put styles in the page; link large images rather than inlining them, from somewhere that isn't the app that's down. - Scripts run on the real page (not in the settings preview). - The page is served from your app's address, so relative links (`/logo.png`) go to the app that isn't answering. Use full URLs. - Over the [API](/docs/api): `PUT /api/v1/organizations/{organization_id}/error-page` with `{"html": "…"}`. - A coding agent can write and set it for you: `set_error_page`, `get_error_page` and `remove_error_page` in the [agent tools](/docs/agent-tools). Not with a token limited to one project, as the page is the whole organization's. --- # Servers and regions Your services run on servers that Alf runs for you. There's nothing to install and nothing to keep up to date: you choose where a service runs, or let Alf choose. ## Shared servers Alf has servers in several regions. **Servers** in the sidebar lists them: the region, whether it's online, which of your services run there, and on how many servers each of them runs. See [The servers page](/docs/capacity). Every organization's services run on them, each project in its own private network: your services can't be reached by anyone else's, on the same server or across servers. See [Private networking](/docs/networking) and [Security](/docs/security). ## Where a service runs By default Alf places each service on as many online servers as its **instances** say, spreading them over regions and towards the servers with the fewest services. With several instances, visitors reach the nearest one that's awake. [Where services run](/docs/placement) has every rule and every case: what makes a service move, and what doesn't. A busy server takes no new services until its load is down; new ones go to servers with room, and if none has any, creating the service is refused with a message saying so. See [When a server is full](/docs/capacity#when-a-server-is-full). ## Choosing regions Alf groups its servers into regions, like **Europe** (servers in Falkenstein and Nuremberg, say) or **US**; the picker shows where each region's servers are. [Regions](/regions) lists the ones open today. **Regions**, when you create a service and in its **Scaling** settings, is where it runs: one instance in each region you pick, on a server of its own there. **Anywhere**, the default, is one instance where Alf picks. Changing them moves the service: it starts on a server in its new regions and leaves the old one, and its instances are spread over them again. ```diagram regions web is kept to Europe and US, so it runs one instance in each, on a server of its own there; db is kept to Europe. A visitor in Berlin reaches web on a server in Falkenstein, which also runs db with its volume; a visitor in New York reaches web on a server in the US. If the Falkenstein server goes away, another server in Europe, like Nuremberg, takes web. Anywhere, the default, is one instance where Alf picks. Databases, computers and services with a volume run on one server, in one region, where their data is. ``` - **Databases and computers** run on one server: choose their region when you add them. - **A service with a [volume](/docs/volumes)** stays on the server its data is on, so its regions can't leave that server's out. To move one, restore a [snapshot](/docs/backups) into a new service where you want it. - **More than one in a region**: through the API or alf.toml, like two in Europe and one in the US. See [How many in each region](/docs/placement#how-many-in-each-region). - **Not a particular server**: a service is kept to regions, and Alf picks the server there (one with room, near the project's other services), so a full or failing server is never yours to work around. - **Coding agents** do the same with `regions` on `create_service`, `update_service`, `add_database` and `create_computer`. - A project's services share a [private network](/docs/networking), so a region where none of its servers can join the project's network can't be chosen for it; the picker says so. When a server goes offline, its services move to another server in the same region, and to another of their regions only when none there can take them. Those with a volume wait for it instead: their data is on that server. The room every server keeps free is there for exactly this. See [Where services run](/docs/placement#every-case). All services of a project run on servers that can share its [private network](/docs/networking). Now and then that means a server on the list can't be chosen for a project that already runs elsewhere; the message says so. ## Private servers If you want machines that run only your organization's services, Alf can set up **private servers** for you: write to [contact@runalf.com](mailto:contact@runalf.com). Once your organization has a private server: - your new services run there, and only your organization's services do; - services you already had move over by themselves, except those with a volume (their data stays where it is until you [move them](/docs/placement#services-with-a-volume), which takes it along); - the **Servers** page marks it **Only yours** and shows which of your services run on it. Alf installs, updates and watches private servers the same way as the shared ones, including how much room they have: if yours fills up, it says "Full right now" and we'll be in touch about a bigger or another one. ## Updates Servers update themselves: when a new version of the program that runs your containers is released, each server restarts into it at a quiet moment (no builds running, no requests open). Your containers keep running meanwhile. Now and then a feature needs a version a server doesn't have yet. It's then refused for services on that server with a message saying so, and works a few minutes later. --- # Where services run This page explains how Alf decides which servers a service runs on, what makes it move, and what doesn't. You choose a service's regions under **Scaling**: it runs one instance in each. See [Choosing regions](/docs/servers#choosing-regions) for how. ## The short version - **One instance, one server.** A service runs on as many servers as it has instances, never two instances on the same server. - **One instance in each region you pick.** Kept to Europe and the US, a service runs once in each; **Anywhere**, once, where Alf picks. Through the API or alf.toml it can also say a number of instances to spread, or how many each region runs (below). - **What runs stays where it is.** A service moves only when you change it, when its server goes offline, or when the operators move or remove a server. ## Which servers it can use A server is a candidate for a new instance when all of these are true: - **It's yours or shared.** Once your organization has [private servers](/docs/servers#private-servers), your services run only on those. - **It's in the service's regions**, if you chose any. - **It's in the project's network zone.** A project's services share a [private network](/docs/networking), so they all run on servers that can join it. - **It's online.** A server is online when it has reported in the last minute. - **It has room.** It's not [full right now](/docs/capacity#when-a-server-is-full), and the sizes it has given out leave room for this one's, and for its volumes' sizes on its disk. (A service that sleeps counts as a share of its size, since most are asleep most of the time; one that never sleeps counts in full.) A full server only takes an instance to stand in for one that was lost (see below). - **The service isn't on it already.** ## Which one it picks Among the candidates, Alf goes down this list until one server is left: 1. **The region of the instance it replaces.** When an instance is lost because its server went offline, the replacement goes to that server's region. 2. **A region the service isn't in yet**, so instances spread over regions. 3. **A server the project's other services already run on**, so they reach each other on the same machine. 4. **A country the service isn't in yet.** 5. **The server with the fewest services**, then the one with the least CPU in use. Steps 2 and 3 swap places depending on the regions: - **Anywhere** (no regions chosen): staying near the project's other services comes first. A two-instance service whose database is in Europe runs both instances there if there's room, rather than one across the ocean. - **Kept to several regions:** spreading over them comes first. You chose those regions to be run in. ## How many in each region A service can say how many instances each region runs: two in Europe, where most visitors are, and one in the US (`region_instances` in the API, as in alf.toml). Picking regions under **Scaling** is one in each; the service's instances are the sum. - Each region is placed on its own, with its own count, by the rules above. A lost instance is made up for in its region, and only there: a count is where you asked for it to run. - A region runs at most as many instances as it has servers (each runs on a server of its own), and a service at most 10 in all. Asking for more is refused, and nothing changes. - A region with fewer servers that can take it than its count runs fewer. **Scaling → Where it runs** shows each region with how many run there against how many it wants, and the card on the canvas has a dot for every instance (a hollow one for each it wants and hasn't got). - Choosing regions again without counts, or a plain number of instances, lets the counts go: its instances are spread over its regions by themselves again. ## Every case | What happens | What moves | | --- | --- | | **You create a service** | It's placed by the rules above. If no server it may use has room, creating it is refused and nothing is made. | | **You raise its instances** | New instances are added by the rules above; the ones running stay. If every server it could still go to is full, the change is refused. If there are fewer servers than instances, it runs on all of them and takes the next one that joins. | | **You pick fewer regions** | The instances in the regions you took away stop; the others stay. | | **You change its regions** | Instances outside the new regions move into them. If a region you added has none of its instances and another region has two, one moves over. Otherwise nothing moves. | | **You set it back to Anywhere** | Nothing moves. | | **Its server goes offline** | After about a minute without a report, each lost instance is replaced, in the same region if a server there can take it (a full one stands in if needed), else in another of its regions. If nothing can take it, it stays on its server and starts again when that's back. | | **That server comes back** | Nothing moves back. The server drops the copies it no longer runs. | | **A server is full** | It takes nothing new, and what runs on it keeps running. It may stand in for an instance lost elsewhere: that's what the room it keeps is for. | | **No server in its regions can take it** | It runs on fewer servers than it wants, or, with none, shows **No server yet**. It gets one by itself as soon as one can take it. | | **A server joins** | Nothing moves. Services that are short of servers, or have none, take it if it suits them. | | **The operators retire a server** | Everything on it moves to the closest server with room that it may use (in its regions, if it's kept to some), before the server goes. A service without a volume moves at once; one with a volume, a database or a computer moves [with its data](#services-with-a-volume): stopped for the copy, started there, and you're told. Nothing new goes to a server that's being retired. | | **The operators move a server to another region** | Services kept to its old region leave it. Others stay. | | **The operators rename or merge a region** | Services kept to it are kept to the new one, and stay where they are. | | **Traffic rises** | [Replicas](/docs/scaling#under-load-more-containers) start more containers on the servers it's already on. When those are full, [another server in the same region](/docs/scaling#when-its-servers-are-full-another-server-in-the-region) runs it too until the load is gone. Load never adds regions. | | **It sleeps, wakes or is deployed** | It stays on the same servers. | ## Services with a volume A service with a [volume](/docs/volumes) keeps its data on one server's disk, so it runs on one server and stays there. That includes databases and computers. - **When the server goes offline,** it waits for that server instead of moving by itself: elsewhere its data wouldn't be there. - **It runs in one region.** Choosing several is refused. - **Choosing another region moves it there with its data**, to a server picked there with the room for it. Under **Scaling → Regions**, or `alf move shop/db Europe`: 1. It's stopped, so nothing is written while its data is copied. 2. Each volume is snapshotted, with a copy off the server; for a computer, everything installed in it too. 3. It's put on the new server, and its data is put back there. 4. It starts, and the old server deletes the volume it left. It's down for as long as that takes: a few minutes for a small volume, longer for a big one. The service's page shows each step. If something goes wrong before it runs on the new server, it stays where it was and runs again there. - **If its server is away** (offline for good, say), choosing another region moves it with its data as it was in each volume's newest copy off the server, and says how old that is. Without such a copy, it moves only if you ask it to move without its data, and starts empty. Copies off the server, on a [schedule](/docs/backups#on-a-schedule), are what make a lost server a short outage rather than lost data. ## Which instance visitors reach With several instances, a visitor's request goes to the nearest instance that's awake. A sleeping instance that's much closer (more than about 1,500 km nearer) is woken instead. If the first one doesn't answer, the next nearest is tried. ## Examples **Europe and the US, one instance.** It runs on one server, in Europe or the US: near the project's other services, else on the least busy server. If that server goes offline, it moves to another server in the same region. It moves to the other region only when no server in its own can take it. **Europe and the US, two instances.** One runs in Europe and one in the US. If the European server goes offline, the replacement goes to another European server, even when the US has an emptier one. **Europe and the US, two instances, then only Europe.** The US instance leaves, and a second European server takes it. With only one server in Europe, it runs there once and takes the next European server that joins. **Anywhere, two instances.** It runs on two servers near the project's other services, in different countries when that's possible. A third region added later changes nothing. --- # The servers page, and full servers The **Servers** page shows where your services run and how they're spread. The machines themselves are Alf's to look after: how much CPU, memory and disk each has left isn't shown, only whether a server takes new services right now. ## The map and the servers Each server is a dot on the map and a card below it: its region, whether it's online, and how many of your services run there. A server that only your organization runs on is marked **Only yours** (see [private servers](/docs/servers#private-servers)). ## Your services One row per service: - **Copies**: how many servers it runs on. With more than one, visitors reach the nearest one that's awake. If it says "2 wanted" under "1 server", the service asked for more servers than had room, and gets the next one by itself when there is. - **Runs on**: each server it's on, with its state there (running, asleep, starting…), and how many containers when it has scaled up under load. A service that's on no server at all shows **No server yet**; it's placed as soon as one can take it. ## When a server is full Alf keeps room on every server on purpose: sleeping services need somewhere to wake up, and when a server fails, its services need somewhere to go. So a server whose CPU or memory use stays above a limit takes **no new services** until its load is down again. Everything already on it keeps running, wakes, scales and can be redeployed as usual. What you see: - The server's card says **Full right now**, and its region says so under **Scaling → Regions**. - New services simply go to another server with room. - If every server in the regions you chose is full, creating the service is refused with a message such as *"No server has room for another service right now"* (the API answers `409` with the code `servers_full`). Nothing is created. - Asking for more servers for a service that already runs is refused the same way when no other server has room; the service keeps running where it is. What helps: choose another region or **Anywhere** under **Scaling → Regions**, or try again a little later. If it keeps happening, tell us at [contact@runalf.com](mailto:contact@runalf.com): more servers are our job. ## Keeping the disk clear Servers tidy up after themselves, so nothing has to be run by hand: | What goes | How often | |---|---| | Containers a finished command left behind | every 10 minutes | | Images nothing refers to any more (what a rebuild leaves) | hourly | | Build cache older than a week | nightly | | Images of a service that moved to another server | nightly, once it's been gone a day | A disk over 85% full doesn't wait for the night: everything runs at once. Two things are never touched. **Volumes** are your data, and go only when you delete the volume or the service. **A sleeping service's container** is kept, because that's what it wakes from — it looks stopped, but it isn't rubbish. Container logs are capped at three files of 50 MB, so one noisy app can't fill a server. Older lines are dropped; [send what you want to keep somewhere else](/docs/metrics-and-logs). --- # Metrics and logs ## Metrics A service's **Overview** charts, per minute and for the last hour, 6 hours, day or week: - Requests (answered, client errors, server errors) and requests per second: what came in at its address and custom domains. Calls from the project's own services are counted too, and shown apart (**From inside the project**); Alf's own health checks aren't, though the app may log them. - Response time (median and 95th percentile) - CPU and memory: what the app holds against its size. Each container runs in a small VM of its own; what the VM itself takes (its kernel and caches) isn't the app's and isn't counted. - Network in and out - Time awake, wake-ups and wake time - Replicas Nobody watches a chart at three in the morning: [alerts](/docs/alerts) say when these numbers go bad. The top of the Overview says what watches the service. Pick a server to see one copy only, or **Table** for the numbers. Metrics are kept for 7 days. ## Logs The **Logs** tab shows the container's recent output (standard output and error) from one of its servers. **Follow** keeps it fresh. A sleeping service has no running container: its last output is still shown until the container is replaced. Under load a service runs [more containers](/docs/scaling#under-load-more-containers), and each writes its own log. The **Container** menu at the top picks which one to follow; **History** keeps what all of them wrote, each line marked with the container it came from. ## Errors The **Errors** view of the Logs tab reads the kept log (below) for what went wrong: each error once, with its stack trace, how many times it happened and when it was first and last seen, the most recent first. The same error is counted as one however its numbers, ids and values change, so "user 81 not found" and "user 7 not found" are one line with a count. A line starts an error when it says so (`Error`, `TypeError`, `panic`, `Traceback`, `FATAL`, `level=error` and the like), and the indented lines that follow it straight after are its trace. ```bash alf errors web # the last 24 hours alf errors web --since 2h --trace 30 ``` Your coding agent has the same as `search_logs` with `errors: true`. ## Requests `alf requests web -f` shows each request a service answers as it comes: when, the status, the method, how long it took, the path (without its query string, which may hold tokens) and the size of the answer. `--status 5xx` (or `4xx`, `404`, `error`) and `--path /api` narrow it down. `--with-logs` puts under each 5xx what the app printed while it answered it, from the kept log. Its servers keep the newest few hundred in memory, so it's for what's happening now; nothing about who asked is kept, except which of the project's own services it was for a call from inside (`← worker`). `--from inside` (or `internet`, or a service's name) narrows to those. A gRPC call shows its status (`grpc 5`), and one that failed counts as the error it is, so `--status error` finds it. What came from inside and wasn't HTTP (a database's protocol, TLS, a port of the app's own) has no requests to show; `alf requests cache --connections` shows those connections instead: when each ended, which service made it, the port, how long it was open, the bytes each way, and whether it closed, was reset, was refused (nothing listened on the port) or couldn't wake the service. `alf top` has every service's numbers for the last few minutes side by side. ## Build logs Each deployment in the **Deployments** tab has its build log, streamed while it builds. ## Looking back **Live** is the container's own output as it happens. **History** is what the servers have shipped over the last **three days** — which outlives the container that wrote it, so a crash this morning or what a replaced version said is still there. Search it for the text of a line, narrow it to a stretch of time, or to only what went to stderr. Your coding agent has the same thing as `search_logs`, and `log_span` tells it how far back the log goes before it asks for a time that isn't kept any more. Lines arrive with each server's report, every fifteen seconds or so, so the newest second or two is in **Live** before it is in **History**. A service that writes an enormous amount has its lines capped per report, and the gap says so in the log itself rather than disappearing quietly. --- # Pricing, usage and billing ## What it costs Every organization is on a plan. A plan's monthly price covers that much usage; what's used beyond it is billed on top, at the prices below. | | **Hobby** | **Business** | |---|---|---| | **A month** | $5, covering $5 of usage | $50, covering $50 of usage | | **Sizes** | up to 2xl: 8 cores, 8 GB | up to 5xl: 32 cores, 64 GB | | **Instances of a service** | up to 3 | up to 10 | | **A volume, at most** | 100 GB | 4 TB | | **Support** | by email, as soon as we can | priority: an answer within one business day | | **Private servers** | — | yes, at their own monthly price | Choose it under **Billing → Plans**; it applies to the month you're in. Moving to Hobby needs your services within Hobby's limits (the page says which aren't). Need more than Business? [Talk to us](/contact). Usage is counted in four numbers: | | | |---|---| | **$6 per GB of memory, a month awake** | A service's size (its memory, and the cores that come with it), billed by the hour it's awake. Asleep costs nothing. | | **$0.10 per GB of a volume, per month** | A [volume](/docs/volumes)'s size, full or not, whether its service is awake, asleep or stopped. | | **$0.10 per GB stored, per month** | Snapshots, buckets and static sites, by what they hold. | | **First $5 free, then your plan** | Once your free $5 is used up, your plan is charged every month: Hobby's $5 covers your first $5 of usage, Business's $50 the first $50. | Requests, data transfer, builds, custom domains and team members aren't charged. Prices are in US dollars, with no VAT on top. ### Sizes Every service runs at a size: its memory and the cores that come with it. It's what the service may use, what its server keeps for it, and what it's billed for each hour it's awake. | Size | Cores | Memory | A month awake | An hour | |---|---|---|---|---| | **xs** | ¼ | 256 MB | $1.50 | $0.0021 | | **s** | ½ | 512 MB | $3 | $0.0041 | | **m** | 1 | 1 GB | $6 | $0.0082 | | **l** | 2 | 2 GB | $12 | $0.0164 | | **xl** | 4 | 4 GB | $24 | $0.0329 | | **2xl** | 8 | 8 GB | $48 | $0.0658 | | **3xl** | 16 | 16 GB | $96 | $0.1315 | | **4xl** | 32 | 32 GB | $192 | $0.263 | | **5xl** | 32 | 64 GB | $384 | $0.526 | Hobby runs up to 2xl, Business up to 5xl. An app gets **s** unless it says; a [database](/docs/databases) its engine's (xs for Postgres and Redis, m for MySQL and MongoDB); a [computer](/docs/computers) m. Change it under **Scaling** (or with `size` in [alf.toml](/docs/alf-toml), or the API): it takes effect at once, on the containers as they run, with **no restart**. Smaller works the same way; whatever in it uses more memory than the new size by then is stopped and started again. A size bigger than the servers it runs on can hold is refused, and the service keeps the size it had: one container can have up to 80% of its server's memory, and no more cores than it has. What a month comes to: an app that sleeps is billed only while it's awake, so an **s** website awake three hours a day costs about 37 cents a month. A database never sleeps, so it costs its size all month: $1.50 for an xs Postgres. On Hobby, an s app awake all month, an xs Postgres and 2 GB of disk come to $4.70, within the $5 the plan covers. Each container counts on its own: a service on two servers, or one that scaled out to three containers under load, is billed two or three times its size for that time. Its server counts each container's time every 15 seconds, from when it starts until it has finished its last requests, so one added for twenty seconds is billed for those twenty seconds. A container that goes over its memory is stopped by the server and started again, so a size that's too small shows as restarts (and as a warning on the service). At its cores it runs slower instead. **Disk speed** isn't limited or charged, whatever the size: a service reads and writes as fast as the server's disk does. ### Volumes and storage A [volume](/docs/volumes) is billed by its size, whether it holds anything or not: 10 GB is $1 a month. Change its size under the service's **Volumes**: bigger, or smaller down to what it holds. Everything else that's kept is billed by what it holds. A gigabyte kept for a whole month is one GB-month, counted once an hour: 10 GB kept for three days is about 1 GB-month, so $0.10. That's the [snapshots](/docs/backups) kept beside volumes, snapshots' copies off the server, what your [buckets](/docs/buckets) hold, and [static sites'](/docs/static-sites) files. The code you pushed isn't charged. ### Servers of your own A [private server](/docs/servers#private-servers) has a flat monthly price, agreed with Alf. What runs on it isn't metered: no compute and no storage charges for it. ## Your free $5, and after After signing up, you add a payment method: it comes before anything is created or deployed (without one the API answers `402` with the code `payment_required`). It's entered on our payment provider's page, and adding it charges nothing. Then every organization starts with $5 of free usage, with no time limit. **Billing** shows how much is left. When it's used up, nothing changes for what runs: your plan applies. From the following month, your plan's price is charged every month ($5 on Hobby, $50 on Business), whether you use anything or not, and it covers that much usage; beyond it you pay what you used. To stop paying, delete the organization: its last invoice is for the month you delete it in, up to then, plan included, and it's made a week later, when the organization is deleted for good ([Deleting an organization](/docs/members#deleting-an-organization)). ## The Billing page Three figures at the top: - **Your plan**: Hobby or Business, what it costs and covers, and how much of your free $5 is left while it lasts. - **If the month ended today**: what you'd be charged right now, for what's been used so far. - **Estimate for the whole month**: where the month is heading if things stay as they are. Below them, the month service by service: each one's size, the hours it was awake, what it cost so far and what the whole month comes to; then volumes, storage, any server of your own, and the sum with what your plan covers and your free credit. Then the two plans side by side, to move between. The estimate is made like this: a service that never sleeps is awake to the end of the month, and one that sleeps is awake as much as over its last full days (up to seven), each at its size now; volumes keep their size, and what's stored now stays stored. It's a guess: a new size, a busy week or a quiet one changes it. ## Billing - A month is invoiced the night after it ends and charged to your payment method. **Billing** lists the invoices, each with a link to view or pay it. - The payment method is entered on our payment provider's page (Stripe); Alf never sees the card. - If a charge fails, the invoice stays open with its link. Until it's settled nothing new can be created or deployed; what runs keeps running. If it stays unpaid, Alf may pause the organization's apps: visitors then get a "paused" page, and nothing is deleted. - While billing hasn't started on Alf, **Billing** says so and shows what the month would cost. ## Usage **Billing → Usage** shows, per month: - **Compute**: what services cost for the time they were awake, in total and per service: a size's memory × the hours awake (the cores come with the size). Invoices count it in GB-hours of memory, which is memory, not disk. **Container hours** beside it is the time alone. - Requests, data in and out, wake-ups and server errors. - Charts per day, and a table per service (deleted services stay in the history). - **Storage**: volumes by their size, and what snapshots, buckets and static sites hold now, with the GB-months over the month. Days are in UTC; figures are brought up to date every 15 minutes. The same figures are in the API: `GET /organizations/{id}/usage`, `GET /organizations/{id}/billing` and the price list at `GET /pricing`. ## A limit on the month Set one under **Billing**. Once the month reaches it, nothing new can be created or deployed and `standing` becomes `capped` — but **what's already running keeps running**. Turning someone's apps off over money they agreed to spend isn't a decision to make automatically. Worth setting wherever a coding agent can deploy. An agent can make a service bigger, scale it out, hold a session container open, or get into a loop; the limit is what lets you hand one the keys without handing it the chequebook. You're told once when a month reaches it, if you have a notification channel for it. The limit can't be lower than your plan's monthly price. What counts towards it is what you use, so a limit at exactly that price is reached once your usage gets there. --- # Notifications Hear about what matters in Slack, Discord, your own webhook or by email. Admins set them up under **Settings → Notifications**. These are the things the platform does; to hear that an application itself is failing requests or answering slowly, see [alerts](/docs/alerts). ## Events | Event | When | | --- | --- | | A deployment failed | A build or start failed, with the error. | | A deployment went live | A new version runs. | | An agent changed something | A coding agent deployed, changed settings, and so on. | | An agent's change waits for approval | See [approvals](/docs/agent-safety). | | A server went offline (and came back) | Once when it goes quiet, and again when it's back. | | A scheduled snapshot of a volume failed | A [backup](/docs/backups) on a schedule didn't happen, with the reason. | ## Without a channel Some things mustn't go unheard: a service that ran out of memory or is close to it, a volume that filled up and stopped its service, a scheduled snapshot that failed, and the month reaching its spending limit. While no channel listens for one of these, the organization's owners get it by email. Once a channel does, it goes there instead. ## Channels - **Slack**: an [incoming webhook](https://api.slack.com/messaging/webhooks) URL (`https://hooks.slack.com/…`). - **Discord**: a channel webhook URL (Channel settings → Integrations → Webhooks). - **Webhook**: any HTTPS URL; Alf POSTs JSON. - **Email**: when it's switched on for the platform. **Send a test** checks a channel. If a delivery fails, the channel shows the last error. ## Webhook format ```json { "event": "deploy_failed", "title": "Deploy of shop / web failed", "text": "pull access denied (…)", "url": "https://runalf.com/app/projects/…", "data": { "deployment_id": "dep_…", "service_id": "svc_…" }, "organization_id": "org_…", "sent_at": "2026-10-10T12:00:00.000Z" } ``` Each delivery has `X-Smartalfred-Event` and `X-Smartalfred-Signature: sha256=…`, an HMAC-SHA256 of the body with the channel's signing secret (shown once, when you add it). Check it before trusting a delivery: ```js const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader)); ``` --- # Alerts Notifications fire on something the platform did: a deploy failed, a server went offline. Alerts fire on what your application is doing — it started answering with 500s, it got ten times slower, it isn't running. They arrive through the same [channels](/docs/notifications), and the all-clear is always sent. Admins set them up under **Settings → Alerts**. A rule made there covers every service of the organization, including ones added later. ## What a rule can watch | Measure | Fires when | | --- | --- | | Failed requests | More than `n`% of requests are answered with a 5xx. | | Response time | The slowest 5% of requests take longer than `n` milliseconds. | | Memory | The service uses more than `n`% of the memory its [size](/docs/containers) gives it. | | Not running | The service is crashed, failing its health check, or its [volume](/docs/volumes) is full. | Each rule has a threshold and a window: the measure has to stay past the threshold for the **whole** window before anything is said. ## One service on its own Open a service: the top of its **Overview** says what watches it, and **Tell me when it breaks** adds the usual three — failed requests, slow responses and not running — for that service alone. Their thresholds are changed back in **Settings → Alerts**, where every rule is listed with the service it watches. While a rule is past its threshold, the service's Overview says so and since when. ## In alf.toml A service's alerts can live beside its code, in [alf.toml](/docs/alf-toml#alerts): ```toml [services.web.alerts] error_rate = "5%" response_time = "3s for 10m" down = true ``` Each deploy makes or sets them; one changed in the dashboard since is asked about, not put back. ## Why it stays quiet Alerting earns its keep by being believed, so the rules hold back: - A bad minute says nothing: the measure has to stay past the threshold for the whole window. - Fewer than 20 requests in the window and error rate and response time aren't judged at all — one failure in three requests is noise. - A rule that keeps firing repeats at most hourly. - The all-clear is sent when it recovers, so nobody is left wondering. ## From a coding agent Agents have the same over MCP: `watch_service` makes the usual three for a service, `create_alert`, `update_alert` and `delete_alert` do the rest, and `list_alerts` says what is firing. An agent can check with `list_notification_channels` that a channel listens for alerts, and add one you give it the address for. See [Agent tools](/docs/agent-tools). ## Through the API ```bash curl -X POST https://api.runalf.com/api/v1/organizations/$ORG/alerts \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"measure":"error_rate","threshold":5,"window_minutes":5}' ``` `measure` is one of `error_rate`, `p95_ms`, `memory_percent` or `failing`; `service_id` narrows a rule to one service. `POST /organizations/$ORG/alerts/watch` makes the usual three in one go. See the [API reference](/docs/api). --- # Coding agents Coding agents like Claude Code, Codex and opencode can deploy your apps and look after them through Alf's MCP server: create services, deploy, read logs and metrics, roll back, try changes in previews. ## Connect with the browser (recommended) The agent opens your browser; you approve it and choose the organization, whether it may only look or also deploy, and which projects. With the [command line](/docs/cli) installed, `alf agents` finds Claude Code, Codex and Cursor on your machine and adds the server to each, asking first. By hand: **Claude Code** ```bash claude mcp add --transport http alf https://api.runalf.com/mcp ``` Then run `/mcp` in Claude Code, pick `alf` and **Authenticate**. **Codex** ```bash codex mcp add alf --url https://api.runalf.com/mcp codex mcp login alf ``` **opencode**: in `opencode.json`: ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "alf": { "type": "remote", "url": "https://api.runalf.com/mcp", "enabled": true } } } ``` then `opencode mcp auth alf`. **Other MCP clients** (Cursor, VS Code, Windsurf, …): add a remote (HTTP) MCP server with the address `https://api.runalf.com/mcp`. Every agent with access, signed in or with a token, is listed under **Agents → Connected agents**, where you can disconnect it. ## Connect with a token For CI, scripts or clients without sign-in: make a token under **Agents** (choose read or read & deploy, optionally one project, an expiry and [limits](/docs/agent-safety)) and send it as `Authorization: Bearer sat_…`. The Agents page shows the exact setup for each client. ## Tell the agent how you work Add a few lines to your repository's `AGENTS.md` or `CLAUDE.md`, for example: ``` ## Deploying This app runs on Alf; use its MCP tools (server "alf"). - After pushing, deploy the service that changed and wait_for_deployment until it's live or failed. - Passwords and keys go in secrets, never in variables or the repository. - Don't delete anything unless I ask for exactly that; take a snapshot of a volume before changing its data. ``` ## What the agent sees Besides the [tools](/docs/agent-tools), agents can read a guide and a summary of each project (services, addresses, how they connect) and use ready-made prompts: `deploy`, `debug_deployment` and `preview_changes` (slash commands in Claude Code). Agents without MCP can read [llms.txt](/llms.txt). ## What the agent did **Agents → What agents did** lists every change agents made in the last 30 days, and [notifications](/docs/notifications) can tell you as it happens. --- # Agent tools What agents can do over MCP. Tokens and sign-ins that may only look get the first group. ## Looking | Tool | What it does | | --- | --- | | `whoami` | The organization, what the agent may do, and its limits. | | `list_projects` | Projects (and previews) it may see. | | `list_services` | A project's services with state and address. | | `get_service` | Everything about a service; secret names only, never values. | | `get_project_file` | A project written out as [alf.toml](/docs/alf-toml), for `push_code` to deploy once changed. | | `list_deployments` / `get_deployment` | Deployments, with the end of the build log. | | `wait_for_deployment` | Waits until a deployment is live or failed (or a candidate is ready), with why it failed and the app's last output. | | `get_logs` | The container's recent output. | | `get_metrics` | Requests, errors, response time, wake-ups, data; memory the app holds, and the newest minute it was awake. | | `list_servers` | Where services run or can run: each server's region, whether it takes new services right now, and which services are on it. | | `list_previews` | A project's previews. | | `list_snapshots` | A volume's snapshots and its schedule. | | `get_approval` | Whether a change waiting for approval was decided. | | `check_domain` | A custom domain's status and DNS. | | `list_templates` | The one-click deploys: what each creates and asks for. | | `list_buckets` | A project's buckets: S3 endpoint, name, key id, what each holds. | | `list_ports` | A service's public TCP and UDP ports: address, who may connect, who was turned away. | | `check_port` | Whether a public TCP port can be reached from outside. | | `list_alerts` | The alert rules, and which are firing right now. | | `list_notification_channels` | Where the organization is told things, and which events each channel listens for. Addresses are shown in part only. | | `get_error_page` | The organization's [own error page](/docs/error-pages), if it has one. | | `get_activity` | What was changed lately, by people and by agents: who, what, when, and whether it worked. | | `get_billing` | Where the organization stands with billing, this month so far and where it's heading. | | `get_usage` | A month's container hours, requests and data, per day and per service. | | `list_promotions` | What was promoted into an environment. | | `get_session_commands` | Every command run in a session, with its exit code. | A token limited to one project gets that project's activity and usage, and only its services in `list_servers`. `get_billing` is about the whole organization, so it doesn't get that. ## Changing (read & deploy) | Tool | What it does | | --- | --- | | `create_project` | A new project. | | `create_service` | A service running an image, or one that waits for code. | | `add_database` | Postgres, MySQL or Redis in a project, backed up and private. See [databases](/docs/databases). | | `create_bucket` / `connect_bucket` / `rotate_bucket_key` | S3 storage for an app's files, given to an app by reference. The key's secret is never handed to the agent. See [buckets](/docs/buckets). | | `connect_database` | Gives an app a database's URL, as a reference, and deploys it. | | `update_service` | Settings (image, Dockerfile, port, scaling, …). | | `set_variables` | Add, change or remove variables and secrets; deploys them unless told to wait. | | `deploy` | Deploys now. | | `rollback` | Back to the last version that ran (or a given one). | | `upload_code` | A one-time link and command that deploys the working directory to one service; says what happens to staged variables; can hold the new version as a candidate. | | `promote_candidate` | Makes a healthy candidate the live version. | | `discard_candidate` | Removes a candidate; the live version was never touched. | | `exec` | One command in a container (migrations and the like); with `session_id`, in a container that's kept. Needs "may run commands". | | `open_session` | A container to work in, so one command sees what the last one left. Needs "may run commands". | | `list_sessions` | What's open on a service. | | `close_session` | Ends one and removes its container. | | `push_code` | A one-time link and command that deploys the working directory to a project: every service of its docker-compose file, or its Dockerfile as one service. Creates what's missing. | | `create_preview` / `extend_preview` / `end_preview` | Preview environments. | | `create_snapshot` / `restore_snapshot` | A backup of a volume before a risky change, and putting one back. | | `add_domain` | A custom domain (you create the DNS record). | | `deploy_template` | Deploys a one-click deploy into a project, as one group. | | `open_port` | A public TCP or UDP port for a database or other non-HTTP service, open to the addresses given. | | `close_port` | Closes a public port. | | `list_groups` / `create_group` / `update_group` / `delete_group` | Groups on a project's canvas: which services, computers and buckets belong together. Removing a group leaves what was in it as it is. | | `update_project` | Renames a project, or changes the part of its addresses that names it. Whether agents need approval there stays a person's to set. | | `discard_staged_variables` | Throws away variable changes that were waiting for a deploy. | | `add_volume` | A volume for a service that exists already. | | `set_snapshot_schedule` | Snapshots of a volume by themselves: how often, how many are kept, and whether each gets a copy off the server. | | `copy_snapshot_offsite` | A copy of a finished snapshot off its server. | | `update_port` | Who may connect to a public port, without closing it: its address stays the same. | | `watch_service` | The usual three [alerts](/docs/alerts) for a service, or for all of them, in one go. | | `create_alert` / `update_alert` / `delete_alert` | An alert rule: what it watches, its threshold and window, on or off. | | `add_notification_channel` / `test_notification_channel` / `remove_notification_channel` | Somewhere the organization is told things: Slack, Discord, a webhook or email. | | `set_error_page` / `remove_error_page` | The organization's own error page. | Notification channels, the error page and an alert on every service belong to the whole organization: a token limited to one project can't change them. It can still make alerts for its own project's services. ## Deleting Only for a token or sign-in that was given **May delete** (see [Keeping agents safe](/docs/agent-safety)): | Tool | What it deletes | | --- | --- | | `delete_service` | A service, with its volumes' data, snapshots, domains and ports. | | `delete_bucket` | A bucket and every file in it. | | `delete_volume` | A volume's data and its snapshots. | | `delete_snapshot` | A snapshot, and its copy off the server. | | `remove_domain` | A custom domain from a service. | | `delete_project` | A whole project. The agent has to give the project's name to confirm. | Without that permission an agent can still end previews, close ports and remove groups: none of those lose data. --- # Approvals, limits and service accounts ## Read or deploy Every token and sign-in either only looks, or may also change things. Only admins can let an agent deploy. Limiting it to one project also gives it that project's previews. ## Approvals In a project's settings (gear next to its name), admins can turn on **Agents need approval**. Agents' changes there then wait instead of running: they appear under **Agents → Waiting for approval** with exactly what the agent asked for (secret values hidden). An admin approves (it runs, as the agent) or rejects. Requests expire after a day. Previews of the project don't need approval. ## Limits A token can carry limits, under **Limits** when you make it: - Changes per hour - Services per project - Replicas per service - Servers per service A call past a limit is refused with the reason, which the agent sees. ## Running commands `exec` and `open_session` start a container of a service with its secrets and run anything in it, so they're as good as a shell. They're off unless an admin ticks **May run commands** on the token, or **Also run commands** when approving a sign-in. A session keeps its container, so an agent can install a tool and then use it. Every command run in one is written down with its exit code, how long it took and how much it printed — see **Sessions** under the service. A session closes itself after fifteen minutes without a command, and its container is metered like any other container of that service. ## Deleting Agents can't delete services, volumes, snapshots, custom domains or projects unless an admin ticks **May delete** on the token, or **Also delete** when approving a sign-in. What's deleted doesn't come back, a volume's data least of all, so give this to an agent only when you want it to clean up, and consider limiting the token to one project. With the permission: - deleting a project needs the project's name from the agent, so a wrong id can't take the wrong project; - every deletion shows in the token's activity, and in a project that requires approval it waits for an admin like any other change; - the tools tell the agent to delete only what you asked for, and to snapshot a volume first if its data might still matter. Without it, an agent can still end previews, close ports and remove groups, which lose no data. ## Service accounts A token normally acts as the person who made it and stops when they leave the organization. Admins can make a token that **belongs to the organization** instead: it acts as the organization's service account (a member that can't log in, hidden from Members) and keeps working. The token list shows whom each token belongs to and who issued it. ## Revoking **Revoke** a token or **Disconnect** a signed-in agent on the Agents page: it stops at once. --- # API Everything the dashboard does, it does through a REST API, and you can use the same one. - **Reference and explorer**: [api.runalf.com/docs](https://api.runalf.com/docs). Every endpoint, its parameters and its request body, with a console to try them. - **OpenAPI document**: [api.runalf.com/openapi.json](https://api.runalf.com/openapi.json) (OpenAPI 3.1), for generating a client or importing into Postman, Insomnia and the like. The reference is generated from the API's own routes and validation rules, so it describes what's running, not what someone remembered to write down. ## A first call Log in for an access token, then send it as a bearer token: ```bash curl -s https://api.runalf.com/api/v1/auth/login \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"…"}' ``` ```bash curl -s https://api.runalf.com/api/v1/auth/me \ -H "authorization: Bearer $ACCESS_TOKEN" ``` The access token is valid for 15 minutes. `POST /api/v1/auth/refresh` trades the refresh token for a new pair (the old refresh token stops working). ## Good to know - Ids look like `svc_…`, `prj_…`, `srv_…`: the prefix says what it is. - Errors are JSON, `{ "error": { "code", "message", "details" } }`. A 422 says which fields are wrong. - Lists come as `{ "data": [...] }`. - Secrets are write-only: their values never come back. - What you may do follows your role in the organization, as in the dashboard. ## For coding agents Agents don't use this API. They use the MCP endpoint at `https://api.runalf.com/mcp`, with an [agent token](/docs/agents) or by signing in: the same abilities, as tools, with limits and approvals. See [Coding agents](/docs/agents). --- # Members and roles Invite people under **Members** by their email. Someone with an Alf account is added at once. Anyone else gets an invitation link (by email, and shown to you to pass on): opening it, they make an account and join. A link works once, for 14 days; until it's used it's listed under **Invited, not joined yet**, where you can withdraw it. Someone who signs up without an invitation can also wait on the first page, which asks them to start an organization or join one: as soon as you add their email, it moves on by itself. | Role | May | | --- | --- | | Member | Create, change and deploy projects and services; end previews; make read-only agent tokens. | | Admin | Also manage servers, members, settings, notifications and agent tokens that deploy; approve agents' changes; delete projects. | | Owner | Also delete the organization. An organization always keeps at least one owner. | You can belong to several organizations and switch between them at the top of the sidebar. ## Deleting an organization An owner deletes it under **Settings**, by typing its address part back. It happens in two steps. **At once.** Its apps stop and their containers are removed from the servers, its addresses stop answering and go free, and nothing of it can be changed any more. Reading it still works, so you can see where it stands. **A week later.** What's left is deleted for good: volumes and the snapshots kept beside them, custom domains, and code you pushed. Until then any owner can undo the deletion under Settings, and everything comes back — the services start again on their own. **Its last invoice** is made then, before anything goes: the month you deleted it in, up to then, billed like a month that ended (your plan included, if your free credit is used up), and charged to your payment method. Nothing after the deletion is billed. Invoices are kept either way: they're accounting records, not yours or ours to throw away. An organization with an unpaid invoice can't be deleted until it's settled. If you'd rather keep the organization and stop paying for something, delete the project or the service instead — see [Deployments](/docs/deployments). ## Deleting your account Under **Your account** (your initial at the top right), with your password. It's for good: you leave every organization you're in, and you're signed out everywhere. An organization never ends up without an owner, so while you're the only owner of one, it waits: - **Other people are in it:** make one of them an owner under **Members**, and it carries on without you. - **Nobody else is:** delete the organization first, as above. Its apps stop at once. An organization that's already being deleted doesn't hold you up. Agent tokens you made for yourself and agents signed in as you stop working with the account; the tokens of an organization's service account keep working, since they're the organization's. What you did in an organization stays in its activity record, with your email address. You can sign up again with the same address later, starting from nothing. --- # Security ## Your account **Turn on two-factor authentication**: it's recommended for everyone, and for an organization's owners especially. Signing in then takes a six-digit code from an authenticator app (1Password, Apple Passwords, Google Authenticator, Authy and others) as well as your password, so a password that leaks isn't enough to get in. - Turn it on under **Your account → Two-factor authentication**: scan the QR code, type the code the app shows and your password. - You get ten **recovery codes**, shown once. Each signs you in once instead of a code from the app, for when your phone is lost. Keep them in a password manager or printed; make new ones there when they run low. - A code works once, and wrong codes count as failed sign-ins: after 10 in a quarter of an hour, the address waits. - Choosing a new password with a link from the email leaves it on: someone who can read your mailbox still needs your phone. - Turning it off, or making new recovery codes, asks for your password and a code again. Each change is confirmed by email. - The command-line tool and coding agents are let in from a browser you're signed in to, so they're covered by it too. **Signing in with GitHub or Google** uses the address they have confirmed, so there's no confirmation email; an account with the same address is linked, and two-factor authentication still asks for its code. An account made this way has no password until you choose one with "Forgot your password?". Also under Your account: every place you're signed in, each with a way to sign it out. ## Your data - **Variables, secrets and webhook URLs** are encrypted at rest. Secret values are never shown again after saving, to people or agents. - **Server and agent tokens** are stored only as hashes; they're shown once. - **Uploaded code** is kept for the newest few deploys of each service, then deleted. ## Your servers - A server connects out to Alf over HTTPS; nothing needs to reach it for visitors. - Your apps' traffic reaches the server over that connection. Apps never see each other's traffic. - Between servers, project networks run through an encrypted WireGuard tunnel. Every project has its own network with its own addresses: other projects, yours or anyone else's, can't reach it, on the same server or across servers. - A [device in a project's network](/docs/network-access) (`alf connect`) is let into that project's network on each of its servers and nothing else: not other projects, not the servers themselves. Its private key never leaves it; it's listed in the project's settings, and revoked there in seconds, or when its owner leaves the organization. - On the shared servers, containers of different organizations run side by side on one machine. Every container runs in a virtual machine of its own ([Kata Containers](https://katacontainers.io)), with its own Linux kernel and the processor's hardware virtualization between it and the machine, so a bug in a kernel isn't a way out of the container: apps, databases, computers, the terminals opened in them and every step of a build, with no exceptions. A server that can't start one runs nothing. Every container is also created without the powers nothing ordinary needs — raw sockets above all, which would let it forge ARP and DNS replies to its neighbours — and with a limit on memory, CPU and processes. See [What a container gets](/docs/containers). - An organization that would rather have machines to itself can have [private servers](/docs/servers#private-servers). That is the only arrangement in which nobody else's code runs on your hardware, and it is the right one if you need to be able to say so. ## Agents - Agents act within one organization, with the access you gave them, and can be limited to a project. - They can't delete anything but previews, can't read secret values, and every change is logged. - Approvals, limits and revocation are on the Agents page. See [Approvals, limits and service accounts](/docs/agent-safety). ## Webhooks Nothing connects to your code host: CI pushes code here with a deploy token you can limit to one project and revoke. Notification webhooks carry a signature you can check. See [Notifications](/docs/notifications). --- # Troubleshooting ## "Didn't open its port in time" The container started, but nothing answered on the port the service expects. Usually the app listens on another port: - Set **Port** under the service's settings to the port your app uses, or make the app listen on `$PORT`. - Check **Logs**: a crash on start shows there. - Slow starters (big runtimes, migrations on start) may need longer than the wake allows: make startup lighter or keep the service running. ## "The new version didn't become ready" A deploy started the new version next to the running one, and it never answered, so the running one was kept (your site stayed up). The failed deployment in **Deployments** says which of these it was, and shows what the app last printed: - **Nothing on its port**: the app listens on another port (it's named, if so), only on localhost, or takes longer than 3 minutes to start. On a service's first deploy, a port it's seen listening on is set for it and it deploys again by itself. - **Health check**: the **health check path** returns an error or doesn't exist. Open it on the running version to compare, or clear the field. - **The app stopped**: it crashed on start. Its last output is right there (often a missing variable or secret: when it says which, the failure names it and how to set it, `alf env set --secret NAME`). Fix the cause and press **Deploy**. See [When a deploy fails](/docs/deployments#when-a-deploy-fails). ## Can't connect to a public port Press **Test** next to the port (Networking → Public ports): - "No answer": something in front of the server drops it. That's on Alf's side: write to contact@runalf.com with the port. - "The server refused the connection": the service isn't running there right now (asleep, or its deployment failed), or the server is being updated. Check the service, and try again in a minute. - Reachable, but your client is cut off at once (`ECONNRESET`, "connection closed"): your address isn't on the port's allowlist. Look at **Turned away lately** under the port: the address you just tried from is there. Press **Allow** next to it. (Often it's your IPv4 address while the list has your IPv6 one.) - Connected, then an error from the database: that's the database talking, e.g. a wrong password or `pg_hba.conf` not allowing the user. See [Databases and other TCP services](/docs/tcp-ports). ## A build failed Open the deployment in **Deployments** for its log. For the usual causes the failure says what went wrong and what fixes it (so does `alf deploy`, and `alf doctor` afterwards): - A file the Dockerfile copies wasn't sent: `.gitignore` or `.dockerignore` leaves it out, or it isn't in the repository. `alf deploy` names the line that leaves it out. - The lockfile doesn't match `package.json` (or `pyproject.toml`, the Gemfile, `composer.json`, `go.mod`): install here and commit the lockfile it writes. - A `build` or `start` script the Dockerfile runs isn't in `package.json`. - A dependency needs a newer Node.js, Python or Go than the image has: use a newer base image. - The base image doesn't exist: check the `FROM` line's name and tag. - "there is no Dockerfile": check **Dockerfile** and **Build directory** under settings (paths are from the root of the pushed code: in a git repository, the repository's root). - Other errors in your build steps show as in `docker build`. ## Not sure what's wrong `alf doctor` (in the app's directory, or with its name) says what Alf knows about a service: a failed deploy and why, running out of memory, a full volume, its error rate, domains that don't work, missing variables. Each finding with what to do next. Agents get the same from `get_service` with `diagnose: true`. ## "The server this runs on can't do this yet" The server the service runs on doesn't have the version a feature needs yet. Servers update themselves within minutes of a release: try again shortly. If it stays, write to contact@runalf.com. ## No code has been pushed to this service yet A service built from code waits for its first push: **Deployments → Push its code**, or the [deploy command](/docs/deploying-code) with `--service`. ## The deploy command says the code is too large It sends what git tracks (or, outside a repository, everything but `.git` and `node_modules`). Build output, dependencies and data files usually don't belong in the build: add them to `.gitignore`. ## A service can't reach another one that sleeps Calls by name from inside the project wake a sleeping service, like a visitor does. If they fail instead ("connection refused", "could not resolve"), open the sleeping service's **Networking** tab: it says whether calls from inside wake it. They don't when its server's smartalfred-server is too old, or private networking isn't working there (the **Servers** page shows why). Until that's fixed: set the service to **Keep running**, or call its public address. ## "No server has room for another service right now" Servers keep room free for sleeping services to wake into and for each other's services when one fails, so a busy server takes no new services until its load is down. Creating a service is refused when every server in its regions is in that state. Choose another region or **Anywhere** under **Scaling → Regions**, or try again a little later; what already runs isn't affected. See [When a server is full](/docs/capacity#when-a-server-is-full). ## "Your free $5 is used up" or "An invoice is unpaid" Creating and deploying wait for billing to be sorted out: add a payment method, or settle the open invoice, under **Billing**. Everything that runs keeps running meanwhile. See [Pricing, usage and billing](/docs/usage). ## A service keeps restarting A container that uses more memory than it has is stopped and started again. Its **Overview** says when it has been above 90% of its memory, and channels that listen for "nearly out of memory" hear about it. Give it more memory under **Scaling**; the standard is 512 MB. ## A visitor sees an Alf page instead of the app When an app can't be served, its visitors get a short page that says why and, under **Is this your app?**, where to look. Pages for things that pass try again by themselves every few seconds. Programs (an API client, `curl`) get the same as JSON or plain text, with the reason in the `X-Smartalfred-Page` header. | Page | Status | What it means, and what to do | |---|---|---| | **There's no app at this address** | 404 | Nothing is deployed at that hostname. Check the service's address (it changes when the service, the project or the organization is renamed), or the **Domains** tab for a domain of your own. | | **This app isn't deployed yet** | 503 | Its first version is still being built or fetched. **Deployments** shows how far it is, or why it failed. | | **This app is taking too long to wake up** | 503 | It was asleep and didn't open its port in time. Once is nothing; if it keeps happening see ["Didn't open its port in time"](#didn-t-open-its-port-in-time) above, or give it a bigger size. | | **The app didn't answer** | 502 | It runs but didn't respond: it listens on another port than the service's, only on `127.0.0.1`, crashed while answering, or took too long. Its logs say which. | | **This app isn't running yet** | 503 | It isn't on a server yet. If every server is full, it's placed as soon as one has room: see [When a server is full](/docs/capacity#when-a-server-is-full). | | **This app can't be reached right now** | 503 | Its server isn't connected. [status.runalf.com](https://status.runalf.com) shows whether something is wrong; services without a volume move to another server after about a minute. | | **This app is paused** | 402 | Alf paused the organization's apps, usually over billing. See **Billing**. | ## A custom domain stays "Waiting for DNS" - The CNAME must point at `domains.smartalfred.com`; **Check** shows what your DNS says now. - On Cloudflare, the record must be **DNS only**. - Root domains need ALIAS/ANAME or CNAME flattening. - New records can take a few minutes, sometimes longer, to be visible. ## Services can't reach each other - Use the service's short name and the container port: `db:5432`, not the public address. - Services must be in the same project. - Across servers, UDP 51820 must be reachable between them; the Servers page shows if the private network is off and why. ## An agent says it may not do something - "This token is read-only": it needs read & deploy access. - "may not run commands": an admin must allow `exec` and sessions for it. - An `approval_id`: the project needs approvals; see **Agents → Waiting for approval**. - A limit: the message names it. ## Still stuck? Check [status.runalf.com](https://status.runalf.com), or write to contact@runalf.com.