Chapters
On this page
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 main → git pull → git merge preprod -X theirs → git 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./deployis 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:
- Flag parsing:
--allow-dirty(dirty guard bypass),--no-close(skip post-ship), the rest is forwarded to the dispatcher. - Blocking dirty guard: repository cleanliness check; exits if dirty, unless
--allow-dirty. - EXIT trap
cleanup: return to thepreprodbranch guaranteed even on error; any checkout failure is reported loudly (to avoid getting stuck onmain). - Root-owned file cleanup: removal of artifacts left by previous deployments that would otherwise block writes.
- Hub drift check (blocking): no bypass. Followed by the prod tenant DB drift check if the target is known.
preprod → mainmerge: checkout main, pull, mergepreprod -X theirs, push origin main.- Migrations: display of pending SQL files (see next section). Non-blocking, not auto-applied.
- Agent audit: active agent status check (non-blocking).
- Prod deployment: call to the deployment dispatcher with tenant parameters.
- Prod smoke: process environment check before HTTP smoke + JSON content checks.
- Post-ship close: proposes closing
test-type worksites (skipped via--no-close).
--skip-driftasymmetry:./deployaccepts--skip-driftto bypass the drift check in preprod (urgent hotfix case)../shiphas 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
./deployand./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.
⚠️
allsmoke 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.yamlfile located in the tenant directory. If a named target is specified (option--target=<X>), the filedeploy.<X>.yamlis 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:
- Argument parsing: the option
--target=<X>(the=sign is mandatory), the flag--all(mandatory) and the optional flag--cleanare extracted and validated. - 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.
- Standard argument check: the
CLEAN_CACHEandHAS_ALLflags are consolidated. - Start banner: display of context and initialization of the deployment timer.
- Schema drift check: if the
driftsection is present in the YAML, the engine compares the actual state of the database against the expected schema and reports any discrepancies. - 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. - Background hook launch: the agent listener is started as a background task.
- i18n route generation (pre-build): if the
seed_i18nsection 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. - Nuxt build: compilation of the front-end application.
- i18n seed: if the section is present, injection of translation data.
- Source maps: if the corresponding hook is enabled, upload of source maps.
- Packaging: creation of a compressed archive of the build in the local machine's temporary directory.
- Background Git push: if the hook is enabled, push of the repository as a background task.
- Archive upload: secure transfer of the archive to the client VPS.
- 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). - Recurring job installation (cron): if the
cronsection 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. - Background hook wait: synchronization with the Git push and agent audit tasks.
- Health check: availability polling loop on the target URL until the configured timeout expires.
- 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
--cleanflag is passed), offline dependency installation, then a blocking smoke test on the structural invariants of the produced URLs before anynuxi build. If the incremental build fails, a retry with a purged cache is automatically triggered. - Archiving: the client's
.outputdirectory is packed into a compressed archive.pigzis used if available on the mother ship, with automatic fallback togzip. - Transfer: the archive is copied to the VPS via SCP, then deleted locally.
- Docker restart: SSH connection to the VPS,
.output → .output_oldrotation, 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 inrunningstate — otherwise rollback. - PM2 graceful reload:
pm2 reloadwith 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_startparameter), followed by a process list save. - Health check loop: repeated HTTP polling until a
200status 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.
-
HTTP checks: polling of each URL declared for the client; any
5xxcode is blocking, a4xxgenerates 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. -
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
200for 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. - 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:
- CSS minifier version below the required threshold in the lockfile (missing transitive PostCSS dependencies → silently broken build).
- 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
./deployon 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, ornode_modules) is copied into the container viadocker cp— no network transfer. - Incremental build cache preservation: before archive extraction, the existing
.nuxtcache 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
.outputto the host → forced container recreation (re-reads environment files). This step was introduced after an incident where environment variables were not reloaded on a simplerestart. - 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 installstep 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/healthendpoint. 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 ismain, 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.