Agora

Troubleshooting

Every failure sail can emit — the literal message, what triggered it, and the fix — grouped by Docker, ports and worktrees, env files, service shells, install detection and tunnels.

Sail fails loud and with a hint. Every message below is emitted verbatim by a command; under --json (the default inside an AI agent) the same content arrives as { "error": "…", "hint": "…" }. The exit code is 1 for a sail-level failure and the child's own code when a child process is what failed (see Commands).

Docker and the stack

docker is missing or unusable

docker is not installed or not on PATH
docker compose is not usable: <docker's stderr>

Hint: Install Docker (or start the daemon) and try again. Both come from the docker preflight every stack command runs before it does anything else. The first means spawning docker threw ENOENT — fix PATH for the shell running node ace. The second means docker compose version --short ran and exited non-zero: the daemon is down, or the compose v2 plugin is missing. Sail always shells out to docker compose, never the legacy docker-compose.

No compose file

No compose file found at /path/to/app/compose.yml

Hint: Run "node ace sail:install" first to generate it. Checked by every command that touches containers (up, down, ps, logs, exec and the service shells) — not by install, info, sync-env, prune or share. The path is the app root, so a cd into a subdirectory is never the cause; a missing or renamed compose.yml is. See Install.

The stack will not come up

sail:up failed:
<last 8 non-blank lines of docker's output>

Hint: Is the Docker daemon running? See `node ace sail:logs` for service output. sail:up runs docker compose up --detach --wait --remove-orphans, so this also fires on a healthcheck timeout: the container started but never became healthy — a corrupted volume, a Postgres data directory from an older major after an image bump, an OOM kill. The tail is the cause; the full story is node ace sail:logs <service>.

down / logs / ps / prune failures

sail:down failed:
<tail>

Could not read logs:
<tail>

Could not list containers: docker compose ps failed: <stderr>

Could not list compose projects: docker compose ls failed: <stderr>

All four carry Is the Docker daemon running?; the logs one adds Check the service name with `node ace sail:ps` . They mean the CLI reached the daemon and got an error back. sail:prune reports per-project failures without aborting the rest of the run:

Could not stop "myapp-feature-login":
<tail>

sail:exec

No command given

Hint: Usage: node ace sail:exec <service> -- <command...> (e.g. node ace sail:exec redis -- redis-cli ping). Use the -- separator: without it ace parses your command's own flags as its own.

sail:exec failed (exit 1):
<tail>

Hint: Check the service name with `node ace sail:ps` . The container command's own exit code is propagated. A wrong service name lands here too — compose reports "no such service" in the tail.

--follow with --json

Cannot combine --follow with --json output

Hint: Stream text with `node ace sail:logs --follow`, or poll once with `node ace sail:logs --tail 100 --json` . An endless JSON stream helps nobody. Inside an AI agent --json is implicit, so a bare sail:logs --follow always hits this — poll with --tail / --since instead (Agents).

Volume deletion aborted

Aborted — containers left running, volumes kept.

Not an error: sail:down --volumes asked for confirmation on an interactive terminal and you declined (or the prompt threw). Under --json the same abort arrives as { "status": "aborted", "message": "…" }. Outside a TTY, and inside an agent, the explicit flag itself counts as consent and no prompt appears.

Ports and worktrees

Another stack holds a host port

Cannot start: host port(s) already in use: :5621 (held by shop-main-postgres-1), :6567

Hint: Stop the other stack (`node ace sail:down` in that worktree), pick a worktree with free ports, or run `node ace sail:prune` for orphans.

sail:up probes the host ports before touching Docker. Ports its own running containers already publish are subtracted, so re-running up on a live stack stays a silent success — what is left over belongs to someone else. Holder names come from docker ps; a port held by a non-Docker process shows with no (held by …) suffix. Without this preflight compose would start containers whose published ports are shadowed and the app would end up talking to another worktree's database.

The probe only covers the services your compose.yml declares, at this worktree's offset — an unrelated MySQL on 3306 never blocks a postgres-only stack.

The holder can also be another app's stack: main checkouts always use the base ports, so two different sail apps on one machine cannot both be up from their main checkouts if they share a service. Stop the other app's stack, or run one of them from a worktree. See Host ports.

Worktree ports do not shift

No error — this one is silent. Worktree detection needs git ≥ 2.36 (git worktree list --porcelain -z); on older gits sail sees no worktree and behaves as the main checkout: bare project name, offset 0, base ports. If node ace sail:info reports portOffset: 0 inside a linked worktree, check git --version first. Same symptom when the app root is not in a git repository at all. See Worktrees.

Env files

Nothing to sync

No sail services enabled for this app

Hint: Run "node ace sail:install" first to generate compose.yml. From sail:info and sail:sync-env when the resolved stack has zero services — no compose file, or one declaring nothing sail recognises.

Encrypted .env.local

.env.local looks encrypted — ports NOT synced there, set them from `node ace sail:info --env` through your secret manager instead

sail:sync-env treats it as a failure (exit code 1, its one job could not be done); sail:up prints the same line as a warning and still exits 0, because the stack did come up. Both mean the file contains varlock( ciphertext: appending a plaintext # sail:start block next to ciphertext would corrupt it, so sail skips that file. .env.local and .env.test.local are judged independently. Feed the values in through varlock instead — see Varlock.

This skip is the only case where sail leaves ports unsynced. Everything outside the # sail:start / # sail:end markers is always preserved, so a plaintext file with hand-written entries is safe to sync repeatedly.

Service shells

All three messages interpolate the service, so sail:mysql and sail:redis produce the same text with their own names and examples.

The postgres service is not enabled

Hint: Run "node ace sail:install --services=postgres" first. The shells refuse services the compose file does not declare instead of failing deep inside docker compose exec.

Cannot open an interactive postgres shell with --json output

Hint: Pass a command instead: node ace sail:psql --json -- -c 'select 1'. A REPL cannot be machine-readable, and inside an agent --json is implicit — so a bare sail:psql always lands here. The one-shot form is the agent path. Note where the flag sits: everything after -- is handed to the client, so ace flags belong before the separator.

No interactive terminal — cannot open a postgres shell

Hint: Pass a command instead: node ace sail:psql -- -c 'select 1'. Raised in CI, under a pipe, or anywhere process.stdin.isTTY is not true: sail prints the exact re-run rather than hanging on a REPL nobody can type into.

sail:psql failed (exit 1):
<tail>

Hint: Check the service is up with `node ace sail:ps` . The command exits with the client's own code, so a failing query fails your script with the status the query produced, and the tail keeps a SQL error distinguishable from a dead container. See Services.

Install detection

Nothing detected

Could not detect any service from package.json or config/*.ts

Hint: Re-run with explicit services, e.g. node ace sail:install --services=postgres --services=redis (valid: postgres, mysql, redis, mailpit, rustfs, seaweedfs, garage, minio). The scan found no evidence and the session cannot prompt (no TTY, or an AI agent). On an interactive terminal you get a multi-select instead: picking nothing prints No services selected — nothing to do. and exits cleanly, while an aborted prompt fails with Service selection was aborted plus the same re-run hint.

Unknown service name

Unknown service(s): postgresql

Hint: Valid services: postgres, mysql, redis, mailpit, rustfs, seaweedfs, garage, minio. --services is validated before anything is written.

Codemods could not edit the app

codemods unavailable — add to start/env.ts by hand: DB_HOST: Env.schema.string({ format: 'host' }); …
not written — add to start/env.ts by hand: …
not written — add to .env by hand: …

Env wiring never blocks the compose file: the compose file and the rest of the install have already succeeded by this point. Install decides which of these to print by reading the files back after the edit, not by trusting it, so the common causes all surface here rather than being reported as success — no @adonisjs/assembler installed, no tsconfig.json at the app root, a start/env.ts without Env.create, an unparseable or read-only file, or a .env that does not exist yet.

Object storage needs one edit to config/drive.ts

Object storage: add `endpoint: env.get('S3_ENDPOINT')` and `forcePathStyle: true` to the s3 disk in config/drive.ts — the local backend needs both

A note, not a failure, printed when object storage is selected and the s3 disk lacks either setting: the stock drive.stub has neither, so the SDK would talk to AWS with virtual-host URLs. Sail does not rewrite third-party config bodies. The bucket needs nothing from you — sail:up creates it.

Object storage was not provisioned

config/drive.ts picks the disk from the environment (DRIVE_DISK) — sail cannot tell whether s3 is the active one; pass --object-storage=rustfs if it is

The stock drive config reads default: env.get('DRIVE_DISK'), usually fs in development, so sail does not guess. Run node ace sail:install --object-storage=<backend> once; it records the choice in package.json (sail.objectStorage), and later plain installs keep it.

A removed service keeps coming back

Detection re-adds what the app evidently uses. Record the exclusion: node ace sail:install --exclude=mailpit (or --exclude=object-storage) writes sail.exclude to package.json, and plain installs leave it out from then on.

sail:up could not create the bucket

sail:up started the stack but could not create the 'local' bucket in rustfs:

The stack is up, but the one-off bucket step inside the container failed — usually because the backend was not ready yet or its credentials were edited in compose.yml while the volume kept the old ones. node ace sail:logs rustfs shows why; sail:up again retries it (it is idempotent). The .env files are not synced on this failure.

minio/minio cannot be pulled

MinIO no longer publishes minio/minio. Point the minio block's image at cgr.dev/chainguard/minio:latest (same entrypoint, mc included), or move to another backend: delete the minio block and its volume from compose.yml and run node ace sail:install --object-storage=rustfs.

Tunnels

cloudflared missing

cloudflared is not installed or not on PATH. Install cloudflared (`brew install cloudflare/cloudflare/cloudflared`, or `npm i -D cloudflared`) and re-run

sail:share probes cloudflared --version before anything else. See Sharing.

No tunnel URL in time

cloudflared did not print a tunnel URL in time

Hint: Check your network and re-run (tunnel logs above, on stderr). Only in --json mode, after 60 seconds with no *.trycloudflare.com URL in the tunnel output; the child is killed and the command exits 1. Usually egress filtering or a captive portal — cloudflared's own logs, already on stderr, say which.

Nothing listening, or the wrong port shared

Nothing is listening on :4103 — start the app first (e.g. `node ace serve --hmr`), then re-run (continuing anyway — the app may still be booting)
worktree port :4103 is closed but base PORT :3335 answers — sharing :3335 (core worktree-port not active?)

Both are informational and the tunnel opens anyway. The first means the app had not bound the port yet (if the public URL 502s, this was why). The second means the worktree's offset port is closed while plain PORT answers — what an @adonisjs/core without the worktree-port patch does inside a worktree — so sail shares the port that actually serves.

On this page

Partner

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

Become a partner →