Vortos
Deploy

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 fingerprint

Exit 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 --strict

Built-in checks

CheckWhat it verifies
CredentialCheckEvery configured credential provider can actually issue, via assertIssuable() — without minting anything
DriverSetCheckThe configured target, strategy, and edge router drivers are all registered
CapabilityDescriptorCheckThe configured strategy's required capabilities are satisfied by the configured target
SchemaCompatibilityCheckThe desired build's schema fingerprint is compatible with what's currently applied
MigrationDriftCheckNo pending migrations exist that aren't reflected in the manifest's schema fingerprint
PendingMigrationPhaseCheckNo 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)
UnpublishedStubCheckNo 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
TargetArchCheckA build exists for the architecture the target actually runs (Pipeline ARM64/AMD64 awareness)
WorkerTopologyCheckThe declared workerTopology is reachable — fails closed if ExternalSupervisor is set on a deploy-in-image host that has no supervisord
RootlessWorkerCheckIn 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"
EnvFileReadabilityCheckEvery 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
DeployStateDurabilityCheckThe 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
FileSecretsCheckEvery declared file-shaped secret is present in the store before the deploy tries to materialize it
CanaryAnalyzerReadyCheckIf using the canary strategy, the configured analyzer can actually evaluate SLOs
IacDriftCheckThe 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.

On this page