deploy:doctor
The fail-closed preflight that gates every deploy — checked individually by humans, and as a machine-readable report by CI.
deploy:doctor
deploy:doctor is the fail-closed preflight every deploy run executes automatically before touching infrastructure. You can also run it on its own — exactly the same checks, the same exit code contract — to verify an environment is deploy-ready without actually deploying.
php bin/console deploy:doctor --env=production[OK] credential.ssh-ca-oidc OIDC token source reachable, CA signer configured
[OK] driver-set.target 'ssh-compose' driver registered
[OK] schema.compatibility target schema fingerprint compatible with current
[OK] target-arch.k8s build available for configured architecture
[FAIL] migration.drift pending migrations not reflected in schema fingerprintExit code 0 means clear — 1 on any failure. This is the same PreflightReport::isClear() value both the human-readable output and --json mode read, and the same value deploy itself consults before proceeding to plan and execute. There's exactly one source of truth for "is this environment ready," consumed by two callers.
# machine-readable, for CI gating
php bin/console deploy:doctor --env=production --json
# treat warnings as failures too
php bin/console deploy:doctor --env=production --strictBuilt-in checks
| Check | What it verifies |
|---|---|
CredentialCheck | Every configured credential provider can actually issue, via assertIssuable() — without minting anything |
DriverSetCheck | The configured target, strategy, and edge router drivers are all registered |
CapabilityDescriptorCheck | The configured strategy's required capabilities are satisfied by the configured target |
SchemaCompatibilityCheck | The desired build's schema fingerprint is compatible with what's currently applied |
MigrationDriftCheck | No pending migrations exist that aren't reflected in the manifest's schema fingerprint |
PendingMigrationPhaseCheck | No pending migration is destructive-but-un-annotated — fails closed on a DROP/RENAME/TYPE/TRUNCATE without #[DeployPhase], so it's caught before a deploy is attempted (see Phase Gate) |
UnpublishedStubCheck | No installed vortos/* module migration stub is un-published — fails closed when vortos:migrate:publish would emit something, turning "a framework bump ships schema-dependent code without the schema → SQLSTATE 42703 at runtime" into a preflight refusal. Publish (or deploy --auto-publish) to clear it |
BackupToolchainCheck | (only when vortos-backup is installed) If VORTOS_BACKUP_ENGINE is set, the DB client toolchain is present — unless VORTOS_BACKUP_TOOLCHAIN_EXTERNAL=true (or config/deploy.php ->backupToolchainExternal()), in which case it passes informationally because the toolchain lives on the backup role image, not the lean deploy image |
TargetArchCheck | A build exists for the architecture the target actually runs (Pipeline ARM64/AMD64 awareness) |
WorkerTopologyCheck | The declared workerTopology is reachable — fails closed if ExternalSupervisor is set on a deploy-in-image host that has no supervisord |
RootlessWorkerCheck | In the single-image RideColor model, the worker's supervisord config is rootless — fails closed on user=root or a pidfile/socket/log under a root-owned dir, which would crash-loop the non-root worker with "Can't drop privilege as nonroot user" |
EnvFileReadabilityCheck | Every declared runtime env file is readable by the deploy one-shot uid — fails closed on a 0600 env file the nested cutover docker compose up (running as the image uid) would hit "permission denied" on |
DeployStateDurabilityCheck | The deploy-state store is durable for the topology — fails closed on DEPLOY_STATE_STORE=file in the docker run --rm one-shot (its var/deploy-state is destroyed each run, breaking color alternation + rollback), or on redis selected with no REDIS_* connection configured |
FileSecretsCheck | Every declared file-shaped secret is present in the store before the deploy tries to materialize it |
CanaryAnalyzerReadyCheck | If using the canary strategy, the configured analyzer can actually evaluate SLOs |
IacDriftCheck | The infrastructure IaC expects to exist hasn't drifted from what's actually provisioned |
Every check returns a PreflightFinding rather than throwing — deploy:doctor's job is to collect every problem in one pass, not stop at the first one. The aggregate PreflightReport is what callers actually consult.
Doctor never mutates anything
Every check here is read-only by design — CredentialCheck calls assertIssuable(), never issue(); MigrationDriftCheck reads applied state, never runs a migration. Running deploy:doctor against production repeatedly, including in a monitoring loop, is always safe.