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.
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, 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.
| It finds | It deploys |
|---|---|
| A docker-compose file | Its services, as the file says (see docker-compose stacks). 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: 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. |
| 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.
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.
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".
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:
Procfile: each line besides web is a process of its own (worker: …, clock: …), and release is the release command;SOLID_QUEUE_IN_PUMA);beat where it has a schedule) or RQ in a Python app;app/Jobs, or Horizon) and its scheduler;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.
--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.
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) 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 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.
When the code has migrations launch knows how to run, it sets them as the service's release command: 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 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.
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.