Getting Started
Prerequisites, installing sail into an existing AdonisJS app, the first boot, and the everyday loop — with pointers to the deep pages.
Prerequisites
| Requirement | Why |
|---|---|
Docker Engine with the Compose v2 plugin (docker compose version) | Sail shells out to docker compose; the legacy docker-compose binary is not used. Docker Desktop, Colima, OrbStack and a plain Linux daemon all work. |
| Node.js ≥ 22 | The package's engines floor (22, 24 and 26 are tested in CI). Sail itself uses nothing newer than Node 22; @adonisjs/core 7 declares engines.node >=24, so on 22 your package manager may warn about the framework, not about sail. |
| AdonisJS ^7.3 | Peer dependency — sail registers ace commands, nothing else. |
| git ≥ 2.36 (optional) | Only for worktree isolation: sail needs git worktree list --porcelain -z. Older git silently behaves as the main checkout. |
Sail never starts the app in a container and never boots with it: there is no provider and no config file, so nothing it does can affect production.
Install
Add the package — the only thing this codemod does is register the commands
barrel in adonisrc.ts, which makes sail:* show up in node ace list:
node ace add @adonis-agora/sailGenerate the stack:
node ace sail:installWith no flags, sail scans the app to decide what to run: --services= flags
win; otherwise package.json dependencies (pg → postgres,
@adonisjs/redis → redis, …), then config/database.ts (client: 'pg'),
config/redis.ts, config/mail.ts (smtp mailer), config/drive.ts (s3
disk), then any other config/*.ts — an active store: 'lucid' /
default: 'lucid' in an Agora config (telescope, media, authz, durable…)
counts as evidence for the configured database. Commented-out alternatives
are ignored, SQLite needs nothing, and a dependency contradicted by its
config (drive installed but fs-only) is skipped with a note instead of
provisioning an idle container.
Then it wires, all append-only: the managed compose.yml, the missing
start/env.ts validations, main-checkout defaults into .env /
.env.example, the varlock schema when detected, and an AGENTS.md
section. Existing declarations and values are never overwritten.
Installing & detection documents the full precedence order and every file that gets touched.
Start everything:
node ace sail:up && node ace migration:runup starts detached and waits for the services' healthchecks, so the
chained migration never races the database. It also syncs the worktree's
ports into .env.local (plus .env.test.local, which is what Adonis reads
under NODE_ENV=test) — Adonis loads those over .env on its own, so just
boot the app normally afterwards.
Check what you got:
node ace sail:ps # container state, health, published ports
node ace sail:info # ports, dashboards and the connection envsail:info is the source of truth for how to reach each service. If you run
Mailpit or an object storage backend with a console, it prints their dashboard URLs too.
Everyday commands
node ace sail:info # ports, dashboards, connection env (--json / --env)
node ace sail:ps # container state, health, published ports
node ace sail:logs --tail 100 # or --json; --follow streams text
node ace sail:logs redis --since 10m
node ace sail:exec redis -- redis-cli ping # one-shot, exit code propagates
node ace sail:psql # REPL (also sail:mysql, sail:redis)
node ace sail:psql -- -c 'select 1' # one-shot — the form agents must use
node ace sail:down # stop this worktree's stack
node ace sail:prune --dry-run # stacks left behind by deleted worktrees--follow refuses --json (an endless JSON stream helps nobody). down --volumes also deletes the data (with confirmation when interactive).
prune without --dry-run stops stacks whose compose file is gone from
disk; other apps' projects are never touched. Every flag, payload and exit
code is on the commands reference.
Worktrees
Nothing to configure. In a linked worktree the compose project becomes
myapp-feature-login (isolated containers and volumes) and every host port
shifts by a deterministic offset derived from the worktree name — so the
same worktree name resolves to the same ports on every machine. The
committed compose.yml is identical everywhere
(${SAIL_POSTGRES_PORT:-5432} interpolations); sail injects the offset
ports at runtime, and a bare docker compose up still works with the
defaults.
node ace sail:info # the source of truth — never hardcode 5432Worktree detection needs git ≥ 2.36 (git worktree list --porcelain -z).
On older git sail silently behaves as the main checkout (base ports, bare
project name).
See Worktrees & ports for the offset math, the
.env.local sync and sail:prune.
Service shells
sail:psql, sail:mysql and sail:redis open the client's REPL on a
terminal. With arguments they run a captured one-shot — and without a TTY
(or with --json) the bare form fails with the exact re-run instead of
hanging:
node ace sail:psql -- -c 'select version()'
node ace sail:mysql -- -e 'show tables'
node ace sail:redis -- pingThey connect over the container network, so worktree port offsets never
matter, and they refuse services install did not enable.
Sharing the app
sail:share exposes the worktree's serve port on a public
*.trycloudflare.com URL via cloudflared (no account; needs the
cloudflared binary) — for previews and, mainly, webhook integrations. It
stays attached until interrupted; with --json it prints the { url, local }
payload on stdout and keeps tunnel logs on stderr. Details and the security
caveat: Sharing the app.
Varlock
Nothing to configure — sail detects .env.schema or the varlock
dependency and adapts. install declares the service keys in the schema
(append-only, your types win, connection coordinates marked @public so
varlock's log redaction stops mangling 127.0.0.1), and the port sync
targets the same .env.local every app uses, so
varlock run -- node ace serve --hmr keeps working unchanged. Encrypted
.env.local files are detected and skipped with a warning instead of
corrupted. Full reference: Varlock.
Agents
Inside an AI coding agent every command defaults to JSON, never prompts, and
stays idempotent with meaningful exit codes. If install cannot detect
anything it fails with the exact --services= flags to re-run. install
keeps an AGENTS.md section (<!-- sail:start … -->) describing this
workflow, and the sail-basics skill shipped in the package teaches it: ask
sail for ports, sync before migrating, poll logs with --tail instead of
--follow. See AI agents.
When something breaks
Start at Troubleshooting — it catalogues every error sail can print, verbatim, with the cause and the way out.
Sail
Docker-powered local dev services for AdonisJS — per-worktree isolated containers, deterministic shifted ports, and agent-friendly commands.
Install
What `node ace add @adonis-agora/sail` registers, how `sail:install` derives the service list from package.json, start/env.ts and config/*.ts, and every file it writes — append-only, idempotent, Docker not required.