Agora

Getting Started

Prerequisites, installing sail into an existing AdonisJS app, the first boot, and the everyday loop — with pointers to the deep pages.

Prerequisites

RequirementWhy
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 ≥ 22The 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.3Peer 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/sail

Generate the stack:

node ace sail:install

With 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:run

up 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 env

sail: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 5432

Worktree 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 -- ping

They 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.

On this page

Partner

Your product as the official integration for Sail, right here.

Become a partner →