Chapters

DOC-05 / Technical reference · Chapter 09

Deployment & infrastructure

Describes the Synedre OS release pipeline: asymmetry between ./deploy (AI, preprod) and ./ship (Alex, production), the YAML dispatcher, the build-host pattern without build VPS, and the associated rules (secrets, commit-before-deploy, drift, smoke).

Asymmetry ./ship vs ./deploy

Two entry points exist at the repository root. They do not do the same thing and do not have the same owner.

./deploy ./ship
Target Pre-production (or mothership runtime / PROD for founder sites without preprod) Production
Owner AI / worker / cascade, systematically Alex only, manual gesture
Git Auto-commit of dirty state + stays on preprod branch git checkout maingit pullgit merge preprod -X theirsgit push origin main, then back to preprod (EXIT trap)
Dirty guard Silent auto-commit, unless on main (hard refusal) Blocking: refusal if working tree is dirty, unless --allow-dirty
Schema drift check Single hardwired preprod target; any other tenant → explicit skip. --skip-drift bypass available. Hub schema check (blocking) + prod tenant DB if target is known. No --skip-drift equivalent: hub drift is strictly blocking.
Automatic drift application The drift correction engine generates and applies missing idempotent DDL statements in a single transaction (CREATE TABLE IF NOT EXISTS, ALTER TABLE ADD COLUMN IF NOT EXISTS). Active by default. Can be disabled on a one-off basis. Applied before any build or reload. — (drift applied manually before ship)
Post-deployment smoke HTTP check of tenants, chained with a non-blocking visual smoke. Can be disabled. Process environment check (anti-false-positive) then HTTP smoke + JSON content checks.
Migrations Displays pending SQL files, non-blocking, not auto-applied — no database runner wired.
Closing Proposes closing test-type worksites (post-ship).

Doctrine:

  • ./ship <tenant> = Production, never autonomous, Alex-exclusive. The AI never triggers ./ship.
  • ./deploy <tenant> = always the AI, systematically and without asking — including on the mothership where ./deploy is equivalent to a live rebuild of the runtime (no dedicated preprod).
        ┌─────────────┐   ./deploy <tenant>   ┌──────────────┐
  AI →  │  preprod     │ ──────────────────→   │  preprod /    │
        │  branch      │   (auto, N times)       │  live runtime │
        └─────────────┘                         └──────────────┘
                │
                │  Alex reviews preprod, validates
                ▼
        ┌─────────────┐   ./ship <tenant>      ┌──────────────┐
 Alex → │ merge preprod│ ──────────────────→   │  PRODUCTION   │
        │  → main      │   (manual only)         │              │
        └─────────────┘                         └──────────────┘

Exception — public site without preprod: ./deploy is refused for this tenant — the public site has no preprod. It is modified exclusively via ./ship.

Exception — founder tenants without preprod: for certain founder sites, ./deploy points directly to production. The deploy = preprod boundary does not hold for these cases. The only operation that remains manual is ./ship (preprod→main merge + full ceremony).

Detail of the ./ship pipeline (numbered steps)

./ship <tenant> executes the following steps in order:

  1. Flag parsing: --allow-dirty (dirty guard bypass), --no-close (skip post-ship), the rest is forwarded to the dispatcher.
  2. Blocking dirty guard: repository cleanliness check; exits if dirty, unless --allow-dirty.
  3. EXIT trap cleanup: return to the preprod branch guaranteed even on error; any checkout failure is reported loudly (to avoid getting stuck on main).
  4. Root-owned file cleanup: removal of artifacts left by previous deployments that would otherwise block writes.
  5. Hub drift check (blocking): no bypass. Followed by the prod tenant DB drift check if the target is known.
  6. preprod → main merge: checkout main, pull, merge preprod -X theirs, push origin main.
  7. Migrations: display of pending SQL files (see next section). Non-blocking, not auto-applied.
  8. Agent audit: active agent status check (non-blocking).
  9. Prod deployment: call to the deployment dispatcher with tenant parameters.
  10. Prod smoke: process environment check before HTTP smoke + JSON content checks.
  11. Post-ship close: proposes closing test-type worksites (skipped via --no-close).

--skip-drift asymmetry: ./deploy accepts --skip-drift to bypass the drift check in preprod (urgent hotfix case). ./ship has no equivalent — hub drift is strictly blocking there. Production is never shipped on a divergent schema.

Database migrations during ship

The ship displays pending SQL files but never applies them: no database runner is wired. The block is purely informational and non-blocking — it prints the commands to be executed manually.

A tenant → migration scope mapping prevents mixing mothership migrations with tenant migrations:

Tenant Targeted migrations folder
Mothership / OS runtime mothership scope
An e-commerce tenant (e.g. tenant A) Tenant A's own scope
A vape tenant (e.g. tenant B) Tenant B's own scope
Founder sites (client, alexandrecarette, codemyshop) Each site's own scope
all Special case: aggregates all targets
other Fallback: scope = tenant name

Special case all: aggregates all SQL files from all scopes, excluding _applied/ and applied/ subfolders (already-run migrations). Other scopes simply list the SQL files in the corresponding folder.

Routing of ./deploy and ./ship by tenant

Each tenant is routed to a dedicated script or to the generic deployment dispatcher.

Tenant (argument) ./deploy ./ship
Mothership (default) Concurrency lock health check, then dedicated OS runtime deployment script (--prod --all) Same dedicated OS runtime script (--prod --all)
Public site without preprod Refused — no preprod Generic dispatcher (--all)
Founder site synedre.com Generic dispatcher → direct PROD Generic dispatcher ⚠️ process environment check may block if the tenant is not declared in the checker
Founder site CodeMyShop Generic dispatcher → direct PROD (no more ansible/preprod); smoke enabled Generic dispatcher
CodeMyShop demo Generic dispatcher → demo staging
E-commerce tenant A (preprod) Generic dispatcher (--target=preprod --all); smoke disabled (preprod behind basic auth) Generic dispatcher without --target → prod
Vape tenant B Generic dispatcher; smoke enabled ⚠️ Falls into the generic fallback case — likely to error if no prod VPS is defined
Founder site client Generic dispatcher → PROD; smoke enabled Generic dispatcher
all 5 background parallel jobs: mothership, CodeMyShop (PROD), e-commerce tenant A (preprod), vape tenant B, client. Post-jobs smoke across all. ⚠️ The all smoke tests the CodeMyShop demo but the job deploys the production storefront — the prod storefront is not verified in this path. 3 parallel jobs: OS runtime, public site without preprod, CodeMyShop. Smoke on all three.
--list / -l Displays the client VPS inventory (see dedicated section)
other (unknown) Fallback to legacy ansible script (--preprod) Fallback to legacy ansible script (--prod)

Convergence toward a single dispatcher: since the deployment pattern was standardized and legacy scripts were purged, all remote tenants except the mothership go through the generic dispatcher, for both ./deploy and ./ship. The mothership retains its dedicated script because self-deployment of the runtime is architecturally distinct: local Docker build, with no network transfer to an external VPS.

⚠️ all smoke blind spot: during a ./deploy all, the deployment targets the CodeMyShop production storefront, but the subsequent smoke checks the staging demo. The production storefront is not verified in this path — a point to fix in the smoke loop.

The YAML-driven dispatcher

The standard deployment pipeline — used for all remote tenants, except for the mothership deployment itself — is driven by a per-tenant YAML configuration file. A main deployment script orchestrates the entire sequence by reading that file, validating it, and chaining the steps in a deterministic order.

Configuration file resolution

At invocation, the dispatcher locates the YAML file to apply according to two rules:

  • When the target designates the mothership, the dispatcher attempts to load a dedicated configuration file. This file is documented in the official schema, but does not exist on disk: the mothership follows a separate self-deployment pipeline activated upstream. The corresponding branch in the dispatcher is therefore inert in practice.
  • For any other tenant, the dispatcher loads the deploy.yaml file located in the tenant directory. If a named target is specified (option --target=<X>), the file deploy.<X>.yaml is used instead.

Note: the name synedre (without suffix) identifies the founding tenant of the synedre.com site; it follows the standard tenant path and does not activate the mothership branch.

Dispatcher steps

The dispatcher executes the following steps in order:

  1. Argument parsing: the option --target=<X> (the = sign is mandatory), the flag --all (mandatory) and the optional flag --clean are extracted and validated.
  2. YAML resolution and validation: a Python validator reads the YAML file, checks its compliance with the schema, and emits the configuration variables as key-value pairs evaluated by the shell. Any validation error causes the pipeline to halt immediately.
  3. Standard argument check: the CLEAN_CACHE and HAS_ALL flags are consolidated.
  4. Start banner: display of context and initialization of the deployment timer.
  5. Schema drift check: if the drift section is present in the YAML, the engine compares the actual state of the database against the expected schema and reports any discrepancies.
  6. Automatic drift apply: enabled by default, this mechanism generates and applies in a single transaction the missing idempotent DDL statements (CREATE TABLE IF NOT EXISTS, ALTER TABLE ADD COLUMN IF NOT EXISTS). The schema is updated before any build or reload. This step can be temporarily disabled via a dedicated environment variable.
  7. Background hook launch: the agent listener is started as a background task.
  8. i18n route generation (pre-build): if the seed_i18n section is present, the dispatcher queries the tenant's category and localized metadata tables (via SSH or container) and writes the JSON file of localized route segments consumed by the international routing module at build time. This step is non-blocking: if the database is unreachable, the build falls back to hardcoded values.
  9. Nuxt build: compilation of the front-end application.
  10. i18n seed: if the section is present, injection of translation data.
  11. Source maps: if the corresponding hook is enabled, upload of source maps.
  12. Packaging: creation of a compressed archive of the build in the local machine's temporary directory.
  13. Background Git push: if the hook is enabled, push of the repository as a background task.
  14. Archive upload: secure transfer of the archive to the client VPS.
  15. Remote reload: depending on the variant declared in the YAML, the dispatcher performs either a graceful reload via the process manager (pm2), or a restart of the application container (docker compose restart).
  16. Recurring job installation (cron): if the cron section is present, the dispatcher installs the scheduled entries in the SSH user's crontab on the client VPS (never on the mothership). Each block is tagged and idempotent: the old block is removed before rewriting. An empty list triggers removal of the block. Doctrine: recurring jobs run as close as possible to the tenant's database.
  17. Background hook wait: synchronization with the Git push and agent audit tasks.
  18. Health check: availability polling loop on the target URL until the configured timeout expires.
  19. End banner: display of the summary and total elapsed time.

YAML configuration file schema

Each configuration file is validated by the Python validator before any deployment. The recognized sections are as follows:

Section Required Description
name Yes Deployment identifier (lowercase letters, digits, hyphens, underscores).
build Yes Local client path (local_client required) and Node environment (node_env, default production).
ssh Yes Target host required; SSH user (default ubuntu) and key path optional.
remote Yes Deployment variant: pm2 or docker. See consistency guardrails below.
health Yes Check URL required; maximum wait timeout (default 45 seconds).
drift No Declaration of schema drifts to detect and apply automatically.
seed_i18n No Enables pre-build i18n route generation and translation injection.
hooks No Optional hooks: agent audit, source map upload, Git push.
cron No List of recurring jobs to install on the client VPS.

Per-variant consistency guardrails

The validator enforces strict consistency rules depending on the chosen variant:

  • Variant pm2: the process application name (pm2_app) is required; Docker-specific fields (container, subdir) are forbidden.
  • Variant docker: the target container name (container) is required; PM2-specific fields are forbidden.

Validation of the cron section

Each entry in the cron section is validated individually:

  • key: kebab-case identifier, unique within the file.
  • schedule: crontab expression with exactly 5 fields.
  • command: non-empty command to execute.
  • comment: free-form description (optional).

A cron section declared empty ([]) is passed to the installer as a cleanup signal: the corresponding block is removed from the tenant's crontab.

Configuration file example

The file below illustrates a typical configuration for a tenant deployed via the PM2 process manager, with Git push enabled:

name: mon-tenant
build:
  local_client: un composant interne
  node_env: production
ssh:
  host: <client VPS address>
remote:
  variant: pm2
  dir: /var/www/un composant interne
  pm2_app: mon-tenant-nuxt
  pm2_remote_user: codemyshop
health:
  url: https://mon-tenant.example.com
hooks:
  git_push: mon-tenant

Reminder: the SSH host (host) must be filled in with the actual address or hostname of the client VPS. No real IP address should be committed to a publicly shared configuration file.

Pattern: build on the mother ship, deploy to the VPS

The central invariant for remote deployments is as follows: the client VPS never compiles. The Nuxt bundle is produced entirely on the mother ship, compressed, transferred, extracted in place, and then the application process is reloaded. The pipeline's opening banner explicitly states this operating mode.

Eight-step sequence

  ┌─────────────────── MOTHER SHIP (build) ───────────────────────────┐
  │ 1. Dependency installation (delta, offline mode preferred)        │
  │ 2. URL invariant smoke tests                                      │
  │ 3. Nuxt build for the target client  →  .output/                 │
  │ 4. Compressed archive (pigz if available, otherwise gzip)         │
  └───────────────────────────┬───────────────────────────────────────┘
                               │  secure transfer (SCP)
                               ▼
  ┌─────────────────── CLIENT VPS ────────────────────────────────────┐
  │ 5. Rotation  .output → .output_old        (rollback ready)       │
  │ 6. Extraction of the compressed archive                           │
  │ 7. Process reload (graceful reload or restart)                    │
  │ 8. If online → deletion of .output_old                            │
  │    Otherwise → display of restore command + stop                  │
  └───────────────────────────┬───────────────────────────────────────┘
                               │
                               ▼  HTTP 200 polling (configurable max delay)
                         health check

Pipeline function details

  • Nuxt build: purge of the output directory (and incremental cache if the --clean flag is passed), offline dependency installation, then a blocking smoke test on the structural invariants of the produced URLs before any nuxi build. If the incremental build fails, a retry with a purged cache is automatically triggered.
  • Archiving: the client's .output directory is packed into a compressed archive. pigz is used if available on the mother ship, with automatic fallback to gzip.
  • Transfer: the archive is copied to the VPS via SCP, then deleted locally.
  • Docker restart: SSH connection to the VPS, .output → .output_old rotation, archive extraction into the target directory, removal of a superfluous CommonJS module identified during a prior incident (CJS tree-shake), application container restart, verification that the container is in running state — otherwise rollback.
  • PM2 graceful reload: pm2 reload with environment variable refresh — preserves active connections. Optionally manages a dedicated PM2 user on the VPS. An optional symlink to the back-office uploads folder may be (re)created to prevent missing static file errors.
  • PM2 hard restart: full deletion and recreation of the PM2 process (first deployment or explicit configuration via the pm2_start parameter), followed by a process list save.
  • Health check loop: repeated HTTP polling until a 200 status code is received or the maximum timeout expires (45 seconds by default).
  • Opening and closing banners: visual framing of the pipeline with per-phase duration breakdown.
  • Scheduled task installer: installs the per-client cron job on the remote VPS (see §3).

Rollback behaviour

.output_old is retained for the entire duration of the reload. It is purged only if the process restarts successfully. On failure, the script only displays the suggested restore command (mv .output_old .output) and exits with an error code — the restore remains manual, the move is never executed automatically. To force retention of .output_old even on success, the SHIP_KEEP_ROLLBACK=1 variable can be set.

Post-deployment smoke: three validation layers

The post-deployment verification script comprises two formal sub-levels of verification, supplemented by a third non-blocking visual level.

  1. HTTP checks: polling of each URL declared for the client; any 5xx code is blocking, a 4xx generates a non-blocking warning. Covered clients include the main cockpit, the demo showcase, several production tenants, and the institutional site. An inner loop covers six clients in all mode; two others are checked individually from the delivery pipeline.
  2. Content / JSON checks: validates the shape of the JSON returned by the main navigation API. This layer was introduced after an incident where a page returned HTTP 200 for two days with a JSON payload carrying { error: true }, rendering navigation entirely empty and the footer displayed as raw keys. Without this level, database-driven SSR renders could fail silently. Clients with database-backed navigation are covered; clients without global navigation (headless cockpit, showcase without configured navigation) are intentionally excluded.
  3. Automated visual verification (non-blocking): Playwright screenshot followed by a multimodal verdict. The module is cleanly skipped if no visual verification configuration file exists for the client (zero added latency). Active by default, can be disabled via environment variable.

False-positive prevention: PM2 environment variable check

An HTTP 200 status code is not sufficient to validate a PM2 deployment. An incident demonstrated that a client could be redeployed without any database connection variables: all data-driven SSR routes returned 500, yet the page served a syntactically valid empty skeleton — the HTTP smoke therefore returned 200 for two days.

For this reason, the delivery script runs a PM2 environment checker before the smoke check: it connects via SSH to the remote VPS and inspects the presence of critical variables in the active PM2 process environment.

Return code Meaning Behaviour
0 All critical variables are present Pipeline continues
1 Missing variables Blocking — pipeline stops
2 SSH connection or PM2 process unreachable Blocking — pipeline stops

Non-PM2 targets (headless mother ship, pure Docker environments) automatically return 0 and are not inspected.

Lockfile consistency check before build

Before any main cockpit deployment, the pipeline runs a lockfile consistency check detecting two blocking discrepancies:

  1. CSS minifier version below the required threshold in the lockfile (missing transitive PostCSS dependencies → silently broken build).
  2. Nuxt version below the required threshold in the dependency manifest (risk of mismatch with the bundling tool).

A non-blocking warning is emitted if Vite version overrides are detected in the root configuration. On a blocking error, the script exits with code 1 and an explicit corrective message — the deployment is cancelled. The suggested fix is to regenerate the lockfile from scratch.

Main cockpit deployment: local self-deployment

The main cockpit does not go through the remote deployment pipeline described above. It is a local self-deployment: the mother ship compiles inside its own application container. A dedicated script orchestrates the entire process in --prod --all mode.

Specifics compared to remote deployments

  • No pre-production environment since mid-2026: running ./deploy on the main cockpit is a direct push to live production, without the delivery ceremony of ./ship.
  • Local transfer via Docker copy: a minimal source archive (application code + dependency manifests, without .nuxt, .output, or node_modules) is copied into the container via docker cp — no network transfer.
  • Incremental build cache preservation: before archive extraction, the existing .nuxt cache inside the container is set aside and restored after extraction. This mechanism is the primary performance lever: the build remains incremental rather than a full cold build.
  • Build inside the container: the Nuxt build runs inside the active container via docker exec. The old server remains online throughout the entire build duration. No PM2 process manager is used in this pipeline: the container starts the Node server directly.
  • Rotation sequence: graceful container stop → copy of the new .output to the host → forced container recreation (re-reads environment files). This step was introduced after an incident where environment variables were not reloaded on a simple restart.
  • Self-contained codebase: the main cockpit no longer extends the shared base — the transferred archive is approximately 16 MB lighter than the previous configuration.
  • npm optimisation by fingerprint: a SHA-256 fingerprint of the dependency lock file is stored in the container. If the lockfile has not changed since the last deployment, the npm install step is skipped entirely.
  • Build UUID health check: the health check is not limited to an HTTP 200. The expected build UUID (read from the build manifest on the host side) is compared against the one returned by the container's /api/health endpoint. This ensures that the new build — and not the old one kept alive by a partial reload — is actually serving requests.
  • Accelerated variant: an opt-in option offloads the build to the host rather than the container, with an atomic swap of the output directory (~35% time saving).
  • Delivery ceremony ./ship: adds branch merging, push, agent audits, production smoke, and workstream closure, but ultimately calls the same local deployment script.

Note: unlike client VPSes, the main cockpit container never goes through the SCP transfer phase or PM2. The build and application serving are entirely containerised on the same physical host.

Topology Inventory — Single Source of Truth

The complete infrastructure topology (environment list, SSH access, associated databases) is stored in a single reference table, hosted in the mothership's central database. This table is the single source of truth: no architecture file, no script, no agent should duplicate or hard-code this information — any other document may be outdated.

The inventory listing command queries this table and displays an aligned table in the console. The query filters active entries, orders results by descending criticality, and formats columns for quick reading.

-- Illustrative example (actual columns, anonymized values)
SELECT
  rpad(tenant_identifier, 14)         AS tenant,
  rpad(environment, 11)               AS env,
  rpad(coalesce(domain, '-'), 40)     AS domain,
  CASE WHEN stack_nuxt THEN 'nuxt' ELSE 'ps' END AS stack
FROM topology_inventory
WHERE active = 1
ORDER BY criticality DESC, tenant_identifier, environment;

Notable columns of the inventory table

Column Role
Tenant identifier Primary logical key identifying the client environment
Environment production | staging | infra | legacy | audit
SSH access Client VPS address, user account, and SSH key path
Database References to the DB container, database name, and the identifier of the environment variable holding the password (never the value itself)
Web runtime Application container, PrestaShop presence, Nuxt presence
Business criticality Criticality level, monthly recurring revenue, subscribed plan
Automatic ship authorized Boolean authorizing automatic deployment — must be confirmed before use
Deployment codename Identifier used to compose the exact deployment command
Client codename Foreign key referencing the client federation table (centralized registry)

Active inventory state

The inventory includes several categories of active entries:

  • Active production tenants: critical environments hosting live shops or sites, on one or more client VPS instances.
  • Staging environments: staging copies associated with primary tenants.
  • Mothership infra: the entry designating the mothership's own central VPS.
  • Orphan productions: some tenants carry purpose='production' while being dormant — these are secondary projects with no active maintenance. They must not be aggregated under the "legacy" label.
  • Heterogeneous dual entry: a tenant may appear as two distinct rows (e.g. one Nuxt production row and one legacy row without Nuxt), reflecting two coexisting technical generations. Do not merge them.
  • Decommissioned entry: a tenant that was the subject of a completed one-off engagement has been set to active=0; its operational folder has been removed, but its history is retained in the centralized client registry.

Operational doctrine: before each deployment (./deploy) or production release (./ship), consult the tenant management dashboard to copy the exact command associated with the target codename. Never recompose the command from memory.

Secrets Management Doctrine — Five Levels

Each secret has exactly one canonical file. The presence of the same key in two distinct files is treated as a bug, not as a security redundancy.

The five levels of secret files

# File Scope Git-tracked
1 Tenant environment file Single tenant — read by its application container and deployment configuration No
2 Mothership core environment file Mothership central services No
3 Host environment file Host scripts: scheduled tasks, automation, SSH access to client VPS instances No
4 Shared environment file Cross-project: AI provider API keys, API encryption key, master SMTP configuration No (outside repository)
5 Example files (*.env.example) Public open-source templates — contain no real values Yes

Load order and precedence rule

At central service startup, environment files are loaded in the following order: shared file → core file → host file. If a key is present in both the core file and the host file, the host file takes precedence. It is therefore forbidden to place the same key in both of these levels.

P0 anti-leak rule

No plaintext secret must appear in any git-tracked file. When a versioned file needs to reference a secret, it cites only the environment variable name and the .env* file that carries it — never its value. For example, a tenant's deployment configuration references the name of the variable holding the database password; the topology inventory table likewise stores the variable name, not the password itself.

Note: the three-level summary view (infra / core / tenants) serves as the quick entry point into the main documentation. The full five-level doctrine — which adds the cross-project shared file and the public templates — is the authoritative reference in case of discrepancy.

Pre-Deployment Commit Rule

A change applied locally but never committed does not survive a deployment: the repository synchronization mechanism restores the committed version and overwrites any floating change. A past incident (a production URL fix left uncommitted, overwritten during a subsequent deployment) caused several hours of broken production. This rule is therefore classified P0.

Cleanliness guard behavior

A repository state verification script is executed before each deployment operation. It returns three states: clean, dirty, or out-of-repository. The check is scoped by target: a modified file on the mothership side does not block a tenant deployment, and vice versa.

  • During a preprod deployment (./deploy): if the state is dirty, a silent automatic commit is generated (chore(auto): pre-deploy snapshot <ISO timestamp>) and pushed in the background. Exception: if the current branch is main, the deployment is refused — no automatic commit on the main branch.
  • During a production release (./ship): if the state is dirty, the command is blocking and returns an explicit error prompting the operator to commit first. A bypass parameter exists for intentional emergency fixes only.

A session-end hook also blocks session closure if the repository is not clean. The workflow rule is as follows: no completed work remains uncommitted — the agent stages and commits, the human operator never directly runs low-level git commands.

Appendix — Reference Files

Component Role
Preprod deployment entrypoint Root entry point for staging deployments and tenant routing
Production release entrypoint Root entry point for production releases — human operator only — with full validation ceremony
Deployment dispatcher Reads the YAML deployment configuration and orchestrates remote deployments for all tenants (excluding the mothership)
Deployment helper library Reusable functions: build, packaging, upload, reload, health check, banner, scheduled tasks
Deployment configuration parser Parses and validates the YAML configuration file and exposes deployment variables (sections: build, SSH, drift, i18n initialization, remote, health, hooks, scheduled tasks)
Mothership self-deployment script Deploys the mothership central service via an in-container build followed by an atomic stop/copy/start swap
Accelerated deployment variant Performs the build directly on the host then executes the atomic swap — faster for short iteration cycles
Git cleanliness guard Verifies repository state before deployment or production release (clean / dirty / out-of-repository)
Dependency consistency guard Verifies lockfile version consistency before mothership deployment (minimum required versions for CSS tooling and the framework)
HTTP smoke test script Verifies endpoint availability and content after deployment — eight case branches covered; all mode covers all active tenants
Process environment checker Validates process manager environment configuration before production smoke tests — several tenants covered; some environments without a process manager are excluded by design
Deployment configuration schema Specification of the YAML deployment format with four canonical examples
VPS operations runbook Operational procedures for SSH, process manager, Docker, and certificate renewal
Secrets doctrine Complete reference for the five-level secrets management policy
Topology inventory table Single source of truth for infrastructure topology (see §6)

Warning: the historical workflow document is partially outdated. It still describes a mothership preprod environment (dedicated port, staging subdomain) and a separate ship script as the active pipeline, whereas the mothership preprod has been decommissioned and deployment now goes through the self-deployment script described above. In case of conflict, the code is authoritative, not the narrative documentation.