Einführungspreis: 100 € zzgl. Steuern. Danach 200 € zzgl. Steuern.Yard für 100 € kaufen zzgl. Steuern

Sail-Integration

Architektur und Verhalten der Docker-Integration.

Aktualisiert · Nov. 2026Technische Referenz · Englisch
Technische Referenz auf Englisch verfügbar. Diese Referenz enthält die ursprünglichen englischen technischen Inhalte und Befehle.

Technical reference synchronized with Yard’s source. File names and development commands refer to the native app repository.

How Yard runs a Laravel Sail project (Docker Compose) without asking the user to learn anything new: myapp.test keeps working, with HTTPS, Studio, yard php, the dev server and Insights, while PHP and the databases run in Sail's containers.

Status: implemented and tested end to end (see §8). Workspaces for Sail projects are out of scope (§7).


1. Why Yard and Sail collide

Yard is "native binaries + nginx in front + *.test". Sail is "Docker Compose + everything in containers + localhost:80". Used side by side without integration:

Collision Effect
Ports Sail publishes 80, 3306, 6379, 5432, 7700, 9000, 8108, 5173… on 0.0.0.0 — the same defaults as Yard's nginx and services.
Domain Sail serves http://localhost; there is no myapp.test or HTTPS with Yard's CA.
Commands The dev server, queue, Tinker, Studio terminal, agents, yard php run the host PHP: a different PHP, extensions and database host than the container.
Yard features Insights (auto_prepend_file), Xdebug routing and the profiler live in Yard's FPM pools, not in the container.

2. Experiments (Docker Desktop 29.9.2, Apple Silicon, Sail from a real project)

# Question Result Consequence
E1 Can a container reach a host service bound to 127.0.0.1 via host.docker.internal? Yes (HTTP 200). Yard Mail (2525) and the Insights ingest (2304) are reachable from Sail without rebinding them.
E2 What happens when Docker publishes 0.0.0.0:P while a host process holds 127.0.0.1:P? No error. The container starts; 127.0.0.1:P keeps going to the host process. Collisions are silent: sail up "works", but localhost shows Yard's 404 and TablePlus on 127.0.0.1:3306 talks to Yard's MySQL. Port checks must probe with connect() (YardControl.isPortInUse), not rely on bind errors.
E3 Does the sail script honour SAIL_FILES from .env, with a path containing a space (Application Support)? Yes, when quoted. SAIL_FILES replaces the default file, so it must list compose.yaml too. Yard can add an override compose file that also applies when the user runs ./vendor/bin/sail in a terminal.
E4 Does SIGTERM to a foreground sail up stop the containers? No. The script exits, containers keep running. Don't supervise sail up as a long-running process. Use sail up -d / sail stop and poll docker compose ps.
E5 Does killing docker compose exec … (or the sail wrapper) stop the process in the container? No — it keeps running in the container, and killing the wrapper also orphans the host-side compose client. Long-running commands in the container (Vite, queue:listen) are started as setsid -w sh -c 'echo $$ > /tmp/yard-<tag>.pid; exec <cmd>' and stopped with kill -TERM -<pgid> inside the container. Verified: all children die and the host client exits.
E6 Can a GUI app find docker? On this Mac /usr/local/bin/docker was a dangling link to an uninstalled OrbStack, and ~/.docker/bin was not on a usable PATH. Yard validates Docker at known locations and skips dangling links, while preserving the invocation path and docker basename for multicall providers such as OrbStack. Its invocation directory comes first on PATH, followed by the resolved target directory for credential helpers.
E7 Do container environment variables reach the web requests? No. Sail's web server is php -d variables_order=EGPCS artisan serve, which starts its php -S workers without any variable outside its allow-list (PHP_INI_SCAN_DIR, YARD_* are dropped). They do reach sail artisan …. Yard's ini is mounted straight into the image's conf.d (/etc/php/<v>/cli/conf.d/99-yard.ini), and the values live in a mounted env.php that sail-prepend.php reads.
E8 Does Insights capture anything? It didn't — on any site, native included. dump-loader.php started with if (class_exists('YardInsights', false)) return;, but PHP binds that top-level class at compile time, so the guard always returned before boot(). Reproduced with Yard's PHP 8.4 and php -S. Fixed: the guard is a constant (YARD_INSIGHTS_LOADED). Verified on PHP 8.4 (host) and 8.5 (container); including the file twice is still safe.

Also verified in the sources:

  • vendor/bin/sail sources .env, exports WWWUSER/WWWGROUP (used by the image build) and defaults APP_SERVICE to laravel.test. sail share uses http://host.docker.internal:$APP_PORT, so APP_PORT must stay a plain number (no 127.0.0.1: prefix trick).
  • Sail's compose.stub already adds extra_hosts: host.docker.internal:host-gateway and reads SAIL_XDEBUG_MODE.
  • laravel-vite-plugin (1.x) binds 0.0.0.0:${VITE_PORT} and writes http://localhost:${VITE_PORT} to public/hot when LARAVEL_SAIL is set.
  • php artisan serve (Sail's web server) runs PHP as SAPI cli-server, which dump-loader.php treats as an HTTP request.

3. Design principles (ease of use first)

  1. Zero configuration. Yard detects a Sail project (laravel/sail in composer.json + a compose file). The inspector shows a Sail section with one switch: Run with Sail. Yard never switches a project on its own.
  2. One click. Turning it on assigns free ports, updates .env, creates the .test proxy (HTTPS if the site is secured) and starts the containers.
  3. Start on visit. If the containers are stopped, myapp.test shows a "Starting Sail…" page that starts them and reloads when the app answers — the same idea as the existing 502 "Yard is fixing this site" page. If Docker Desktop / OrbStack is installed but not running, Yard opens it first.
  4. No lock-in. Ports are pinned in .env, so ./vendor/bin/sail up in a terminal gives the same result and myapp.test keeps working. Turning the switch off restores every .env value Yard changed.
  5. Yard never installs Docker (no Homebrew, nothing compiled by Yard). It explains what's missing.
  6. Same commands. yard php … / yard composer …, Studio's terminal, Tinker, queue and dev server, and the coding agents run inside the container for Sail sites, so docs and agent prompts stay the same.

4. Architecture

Browser ─► myapp.test (Yard DNS) ─► nginx (Yard CA cert)
                                     ├─ /yard-studio → Studio (4421)
                                     └─ / → proxy_pass http://127.0.0.1:<APP_PORT>  ─► laravel.test container
                                          502 → /yard-error/sail.html → POST /api/sail/:site/up
container ─► host.docker.internal:2525 (Yard Mail), :2304 (Insights ingest)
Piece File Responsibility
DockerEnvironment YardKit/Sail/Docker.swift Find a working docker (skips dangling links), status (missing / stopped / running / failed), open Docker Desktop/OrbStack, PATH for child processes.
SailProject YardKit/Sail/SailProject.swift Detection, compose files, published port variables (${VAR:-default} on the host side), app service, command builders (sail …, docker compose exec with a pid file, stop).
SailManager YardKit/Sail/SailManager.swift Enable/disable (ports, .env, backup, override file, registry, nginx), status (docker compose ps --format json), up/stop.
SiteRegistry.sail YardKit/Sites/Site.swift name → SailSite (app port, assigned ports, original .env values). In sites.json, like isolation.
nginx NginxConfigWriter.sailServer Proxy with Studio location + badge, error_page 502 → sail.html.
API YardApp/Sail/SailModule.swift GET /api/sail, GET /api/sail/:site, POST /api/sail/:site/:action (enable, disable, up, stop, restart).
UI YardApp/Sail/SailSection.swift Inspector section: Docker state, switch, start/stop/restart, every service with its state, health, ports and live logs (docker compose logs --follow), last start output. The Environment section shows the container's PHP and Node versions.
CLI yard/Commands/SailCommands.swift yard sail:enable, sail:disable, sail:status, sail:up, sail:stop; yard php/composer delegate to Sail.

4.1 Ports

On enable Yard reads every published port variable from the compose files (APP_PORT, VITE_PORT, FORWARD_DB_PORT, FORWARD_REDIS_PORT, …). A value is reassigned when it:

  • is one of Yard's own ports (HTTP/HTTPS, API, Studio, dump, mail, DNS) or a Yard service port, or
  • is used by another Sail site registered in Yard, or
  • answers a TCP connect() on 127.0.0.1 while this project's containers are not running (E2).

New ports are searched from 8100 for APP_PORT and from default + 10000 otherwise (3306 → 13306), skipping ports in use. Ports that are free are left as they are, so projects that already set their own ports (e.g. APP_PORT=8081, FORWARD_DB_PORT=33060) are not touched.

4.2 .env changes (all reverted on disable)

Key Value
Port variables that collide Free ports (§4.1)
APP_URL https://myapp.test (or http://)
SAIL_FILES "compose.yaml:<Yard>/sail/<site>.yml" (quoted, E3)

The original values (or "unset") are stored in sites.json (SiteRegistry.sail[name].originalEnv), not in a file in the project. If the user edited one of those keys after Yard did, Yard keeps the user's value.

4.3 Override compose file (<Yard>/sail/<site>.yml)

Adds Yard's features to the app service without touching the project's compose file. Only read-only mounts — no environment variables, because artisan serve drops them (E7):

Host Container Purpose
runtime/php /opt/yard/php sail-prepend.php, dump-loader.php
sail/home/config /opt/yard/config Only insights.json (mirrored by InsightsFlag.write); Yard's real config dir holds integration tokens and is never mounted
sail/sites/<site> /opt/yard/site env.php: YARD_HOME, YARD_SITE, YARD_SITE_PATH=/var/www/html, YARD_HOST_SITE_PATH, YARD_DUMP_HOST=host.docker.internal
sail/sites/<site>/yard.ini /etc/php/<v>/cli/conf.d/99-yard.ini auto_prepend_file = /opt/yard/php/sail-prepend.php; <v> from image: sail-<v>/app or runtimes/<v>, else every version 8.0–8.6

sail-prepend.php exports env.php into $_SERVER/getenv(), sets HTTPS=on for requests that came through Yard's proxy over HTTPS (X-Yard-Proxy + X-Forwarded-Proto: https; Laravel doesn't trust proxies by default, so URLs would otherwise be http:// — mixed content), then loads the dump loader, which sends to YARD_DUMP_HOST with Host: 127.0.0.1 (accepted by Yard's DNS-rebinding check).

4.4 Commands

Yard feature Native site Sail site
yard php … / yard composer … Yard PHP vendor/bin/sail php … / sail composer …
Tinker (Studio) php artisan tinker --execute docker compose exec -T -u sail <app> php artisan tinker --execute
Queue (Studio) php artisan queue:listen same, through the pid-file wrapper (E5)
Dev server php artisan dev or npm run dev npm run dev in the container (Vite only; artisan serve already runs there), pid-file wrapper
Studio terminal host shell, YARD_SITE_PHP = Yard PHP host shell, YARD_SITE_PHP = vendor/bin/sail php
Agents system prompt says yard php artisan … unchanged — yard php delegates to Sail; the prompt says the app runs in Sail

5. Decisions

Decision Choice Why
Databases/Redis: Sail's or Yard's? Sail's containers. Yard only moves their forwarded ports. Keeps the project portable and identical for teammates; using Yard services would mean editing DB_HOST and diverging from compose.yaml.
Stop containers when Yard quits? No. Matches Docker semantics (E4) and the user's terminal workflow; the inspector and yard sail:stop stop them.
HTTPS Uses the site's existing HTTPS switch; TLS ends at nginx. Nothing changes in the container.
Vite on an HTTPS site http://localhost:<VITE_PORT> from public/hot. Chrome and Firefox treat localhost as potentially trustworthy (no mixed-content block). Safari may block it; workaround: npm run build, or open the site over HTTP.
First sail up builds the image (minutes) Output streamed to the inspector and the "Starting Sail…" page. Avoids "it's frozen". It's the project's image, not a Yard binary, so it doesn't conflict with "nothing compiled by Yard".
yard.yml sail: true enables Sail mode on yard init. Shareable with the team.

Supervised queue, scheduler and broadcasting workers pass the complete dockerCommandLine to the shell. That command already puts Docker's environment assignments before its own exec; prepending another exec would make the shell treat PATH=… as the executable. SailProjectRuntimeTests launches all three roles through a disposable recording Docker client, including restart, environment/quoting and tagged stop.

6. Failure states shown to the user

State Message / action
No Docker found "Sail needs Docker Desktop or OrbStack." + download link
Docker client works but the engine cannot be reached Connection diagnostic + Open Docker (Yard also opens it on start)
Docker executable fails, times out or returns unusable engine information failed status with diagnostic + Retry; Yard does not open an app for a broken executable.
No compose file (laravel/sail required, sail:install not run) "Run php artisan sail:install first."
vendor/ missing "Run composer install first."
Build/start failed Last lines of the output + Show Output

7. Out of scope / next steps

  • Workspaces for Sail projects (each clone needs its own COMPOSE_PROJECT_NAME and ports).
  • New Application → Sail (run artisan sail:install --with=… with Yard's PHP).
  • Xdebug switch mapped to SAIL_XDEBUG_MODE.
  • Yard Mail as an opt-in mailer for Sail sites (MAIL_HOST=host.docker.internal, MAIL_PORT=2525).
  • Vite over HTTPS with Yard's certificate inside the container.

8. Implementation status

See §4 for the files. Unit tests: Tests/YardKitTests/SailTests.swift (detection, port parsing and allocation, .env round trip incl. a file without a final newline, user SAIL_FILES, Yard service ports, override file, PHP version detection, compose output parsing, warning filter, nginx config incl. nginx -t).

End-to-end (2026-11-05, Docker Desktop 29.9.2): a copy of a real Laravel 12 + MySQL + Redis + Mailpit Sail project, with an isolated YARD_HOME (path with a space), the app on its own ports and a real nginx:

Check Result
yard sail:enable APP_PORT 80 → 8100 (Yard's nginx), Redis 6379 → 16379 (another project's container); free ports untouched
yard php -r … / yard php artisan migrate Ran in the container (PHP 8.5.12, Linux)
https://sailshop.test through Yard's nginx 200, certificate from Yard's CA, Studio badge injected, asset URLs https://
yard secure APP_URL switched to https:// automatically
Containers stopped → open the site "Starting Sail…" page → POST /api/sail/:site/up → serving → 200
Studio Tinker / queue Ran in the container; stopping the queue killed the worker in the container and the host client
Insights Batches from the container reached the ingest port with the right site and host path (after the E8 fix)
Untrusted Origin on the Sail API 403
yard sail:disable Containers stopped, .env byte-identical to the original, override files removed
Inspector "Laravel Sail" section (switch, state, Restart/Stop, ports) and a "Sail" chip on the card

Not covered end to end: Vite in the container (same exec/stop mechanism as the queue, which was verified) and Safari with HMR on an HTTPS site (§5). Seen once: right after Yard's PHP runtime files were replaced under a running container, one request got a 502 (upstream prematurely closed connection); the next requests were fine.

DockerEnvironmentTests uses a compiled multicall fixture with two symlink levels and paths containing spaces. It checks invocation identity, credential-helper PATHs, dangling links, launch failures, daemon connection errors, timeouts and old status JSON. YARD_DOCKER_CLIENT_PROBE=/path/to/docker adds a read-only --version probe of an installed CLI; it does not contact the engine or manage containers.