docs/maml.md
Deployer supports recipes written in MAML, a minimal,
human-readable, machine-parsable configuration format. MAML extends JSON with
comments, multiline raw strings, optional commas, unquoted keys, and ordered
objects, while remaining strict about types and structure. Files use the
.maml extension.
The schema for a MAML recipe is declared in PHP at
MamlRecipe::schema()
and validated on load. Validation errors point at the offending span with a
source snippet.
{
# Import other recipes (php, maml, or yaml).
import: [
"recipe/common.php"
]
config: {
repository: "[email protected]:example/example.com.git"
}
hosts: {
"example.com": {
remote_user: "deployer"
deploy_path: "~/example"
}
}
tasks: {
# Build the project
build: [
{ cd: "{{release_path}}" }
{ run: "npm ci" }
{ run: "npm run build" }
]
}
after: {
"deploy:failed": "deploy:unlock"
}
}
Generate a starter recipe interactively with:
dep init
and choose maml when prompted for the recipe language.
A MAML document is a single value, normally a top-level object { ... }.
# to end of line."..."), with the usual escapes
(\t, \n, \r, \", \\, \u{XXXX})."""..."""), no escapes, newlines and
whitespace preserved verbatim. Useful for embedding scripts.5, -3) and floats (1.5, 1e9).true, false, null (lowercase only).[ ... ], comma- or newline-separated.{ key: value }, comma- or newline-separated. Keys may be
unquoted identifiers (letters, digits, _, -) or quoted strings. Hosts
with dots ("example.com") and hook names ("deploy:failed") must be
quoted.Trailing commas are allowed everywhere. Duplicate keys inside an object are not.
A recipe is an object with these optional keys, validated by the schema:
| Key | Description |
|---|---|
import | String or array of strings. Paths to other recipes (.php, .maml, .yaml). |
config | Object. Becomes calls to set(). |
hosts | Object. Each entry becomes host() (or localhost() when local: true). |
tasks | Object. Each entry becomes a task(). |
before | Object mapping task → hook(s). Becomes before(). |
after | Object mapping task → hook(s). Becomes after(). |
fail | Object mapping task → fallback task. Becomes fail(). |
Any other top-level key is rejected with a schema error.
importPull in other recipes. PHP recipes run as plain require, MAML and YAML
recipes are parsed and applied. This is how a MAML recipe gains access to
custom PHP tasks, callbacks, and helpers it cannot express directly.
{
import: "recipe/laravel.php"
}
{
import: [
"recipe/common.php"
"deploy/custom.php"
"deploy/extras.maml"
]
}
configA flat object. Each key is forwarded to set($key, $value). Values may be
strings, numbers, booleans, arrays, or nested objects, anything MAML can
express.
{
config: {
repository: "[email protected]:example/example.com.git"
keep_releases: 5
ssh_multiplexing: true
shared_dirs: ["storage", "bootstrap/cache"]
}
}
config does not accept PHP closures. To set values that need runtime
evaluation, import a .php recipe and call set() from there.
hostsEach entry creates a host. Keys with dots (example.com) must be quoted.
Inside, every key/value is forwarded to Host::set(), so all standard host
options are available (remote_user, deploy_path, port, identity_file,
labels, ssh_arguments, etc.).
{
hosts: {
"prod.example.com": {
remote_user: "deployer"
deploy_path: "/var/www/prod"
labels: { stage: "production" }
}
"staging.example.com": {
remote_user: "deployer"
deploy_path: "/var/www/staging"
labels: { stage: "staging" }
}
}
}
Set local: true to register the entry as a localhost via localhost():
{
hosts: {
"dev": {
local: true
deploy_path: "/tmp/dev"
}
}
}
tasksA task entry is one of:
{
tasks: {
deploy: [
"deploy:prepare"
"deploy:vendors"
"deploy:publish"
]
}
}
Each step is an object with exactly one action key (cd, run,
runLocally, upload, download) or one or more task-config keys (desc,
once, hidden, limit, select). Steps are executed in declaration
order. Task-config steps modify the task itself and do not interrupt the
chain of actions.
{
tasks: {
build: [
{ desc: "Build assets" }
{ once: true }
{ cd: "{{release_path}}" }
{ run: "npm ci" }
{ run: "npm run build" }
]
}
}
Leading # comments directly above a task key become the task's description
(joined with newlines). The desc step takes precedence if both are
present.
{
tasks: {
# Deploy the application
# Runs migrations, builds assets, restarts services
deploy: [
{ run: "echo deploying" }
]
}
}
Set these inside step objects to control task metadata:
| Key | Type | Effect |
|---|---|---|
desc | string | Sets the description (shown in dep list). |
once | bool | Run on a single host only. |
hidden | bool | Hide from dep list. |
limit | number | Maximum hosts to run on in parallel. |
select | string | Host selector expression (see Selector). |
{
tasks: {
migrate: [
{ desc: "Run database migrations" }
{ once: true }
{ limit: 1 }
{ select: "stage=production" }
{ run: "php artisan migrate --force" }
]
}
}
cdChange the working directory for subsequent run steps in the same task.
{ cd: "{{release_path}}" }
runExecute a command on the remote host. Equivalent to
run(). All optional keys mirror the PHP function:
{
run: "php artisan migrate --force"
cwd: "{{release_path}}"
env: {
APP_ENV: "production"
}
secrets: {
DB_PASSWORD: "s3cret"
}
timeout: 600
idleTimeout: 120
nothrow: false
forceOutput: true
}
| Option | Type | Default |
|---|---|---|
cwd | string | host's cwd/deploy_path |
cd | string | (alias of cwd) |
env | map<string, string> | none |
secrets | map<string, string> | none |
timeout | number (seconds) | 300 |
idleTimeout | number (seconds) | none |
nothrow | bool | false |
forceOutput | bool | false |
Use a raw string for multiline commands:
{
run: """
set -e
php artisan down
php artisan migrate --force
php artisan up
"""
}
runLocallyRun a command on the local machine. Mirrors
runLocally().
{
runLocally: "git rev-parse HEAD"
cwd: "."
shell: "/bin/bash"
timeout: 60
}
Supports the same options as run plus shell, except cd (use cwd).
uploadTransfer files to the remote host. Mirrors
upload(). src may be a single path or an array of
paths.
{
upload: {
src: "build/"
dest: "{{release_path}}/public/"
}
}
{
upload: {
src: ["dist/app.js", "dist/app.css"]
dest: "{{release_path}}/public/assets/"
}
}
downloadTransfer files from the remote host to the local machine. Mirrors
download().
{
download: {
src: "{{deploy_path}}/shared/.env"
dest: ".env.production"
}
}
before, after, failHooks attach tasks to other tasks. The value may be a single task name or an
array of task names. Quote names that contain :.
{
before: {
deploy: ["deploy:prepare", "build"]
}
after: {
"deploy:failed": "deploy:unlock"
deploy: "deploy:cleanup"
}
fail: {
deploy: "deploy:rollback"
}
}
For arrays, hooks attach in declaration order.
MAML covers the declarative parts of a recipe: config, hosts, tasks built
from standard steps, hooks. Anything that needs runtime PHP (closures, the
set('var', fn () => ...) pattern, custom step types, conditional logic)
belongs in a .php recipe imported from MAML, or vice-versa.
From a PHP recipe, import MAML using import():
import('deploy.maml');
From a MAML recipe, list the PHP file under import:
{
import: ["deploy/extras.php"]
}
The same applies to YAML, see YAML.
When a recipe does not match the schema, Deployer raises a
SchemaException with the offending span and a snippet of source. Common
causes:
config: "string" instead of an object, or tasks: [...]
instead of an object.Fix the structure, re-run, and the error trace will pinpoint the line.
dep init generates a starter deploy.maml.dep config prints config in MAML by default; use --format=json or
--format=yaml for other formats.