Manual Deploy What Deploy Does

What Deploy Does

elements man deploy/what-happens Read as markdown

The steps elements deploy runs, its -json output, why it is fast, and how machines are provisioned.

What elements deploy Does

Each phase is sequential. An error in any phase aborts the deploy. The previous release keeps serving traffic until the new release is fully ready.

  1. Local prep: validate services, run preDeploy commands, build each service binary.
  2. Provision: verify Linux + systemd, create the elements system user, install Elements, bring up the Postgres cluster, create the project dir.
  3. Connect: open SSH tunnel to each machine's deploy server.
  4. License: check out a license token on each machine.
  5. Sync: diff manifest, transfer only changed files via project-server-to-project-server protocol.
  6. Build: compile, package install, asset emit on each machine.
  7. Migrate test db: run pending migrations against the test db (first machine only).
  8. Test: run all tests on each machine.
  9. Migrate app db: run pending migrations against the app db (first machine only).
  10. Release: atomically swap .elements/release to point at the new build.
  11. Update state: record successful deploy in the per-machine state file.

The release in step 10 is the "go-live" point. The old release continues serving until the new release directory is fully written and the swap completes.

elements deploy -json

Run elements deploy with -json from an agent. The command returns structured JSON, exits cleanly, and never opens a TUI. Without -json, elements deploy runs as a TUI and buffers when piped through tail, head, or less.

elements deploy production -json
elements deploy staging -json

elements will not deploy if there are local build errors.

The loop is one command: run elements deploy -json, read the diagnostics, report them. The output is the deploy's own account of itself and covers both the local build and every machine, so a failure needs no second source. Do not ssh to a machine to confirm what a diagnostic already says, and do not go checking systemd units, /elements, dns, or the clock. When the diagnostics do not explain a failure, that gap is itself the thing to report.

Every diagnostic carries a hosts field, rendered as a [<host>] tag at the end of the message in the TUI. Use it to triage where the failure happened:

  • No hosts field (and no [<host>] tag in the TUI): the failure is local. Fix it in the source tree; the deployment machine was never involved.

  • hosts: ["production0"] (rendered as [production0] in the TUI; the list can contain one or more machine names): the failure happened on those machines after the source was synced. The message is the machine's own report, relayed over the tunnel. Fix the cause in the source tree or in config.jsoc and deploy again.

Local failure shape:

{
  "diagnostics": [
    {
      "category": "error",
      "message": { "data": "Type 'string' is not assignable to type 'number'." },
      "path": "app/pages/home/services.ts",
      "loc": { "line": 12, "column": 18 }
    }
  ]
}

Remote failure shape:

{
  "diagnostics": [
    {
      "category": "error",
      "message": { "data": "migration 20260514120000-add-orders-table.migration.sql failed: relation \"orders\" already exists" },
      "hosts": ["production0"]
    }
  ]
}

A successful deploy returns an empty diagnostics array.

What Makes Deploys Fast

Same principle as the build loop: only do work if you have to. The deploy machine runs the same Elements project server you run locally. The two project servers speak a peer-to-peer protocol over the SSH tunnel, so the deploy does the smallest set of steps required to produce a correct release.

  • Source diffing is fast. The local server has a live manifest of every file in the build graph. So does the remote server. The deploy diffs the two manifests and ships only the deltas. Most incremental deploys transfer a few KB.
  • The remote build does the minimum work required. The remote project server stays alive across deploys. When new files arrive, it does only the work necessary to produce a correct release.

The remote project server keeps its build state across deploys. Most deploys ship a few KB of source diff, do an incremental compile on the deployment machine, and swap the release. There is no full rebuild from scratch on every deploy.

A typical incremental deploy:

  1. Local build does only the work the changed files require.
  2. File sync transfers only the files whose content hash has changed since the last deploy, often only a few KB.
  3. Remote build does only the work the changed files require, on the deployment machine.
  4. Tests run only for the changed test subgraph.
  5. Release swap is an atomic directory rename, with no restart pause and no warm-up.

Provisioning happens only on the first deploy to a fresh machine. After that Elements is installed, the system user exists, systemd is wired, the Postgres cluster is up, and the project server is alive.

Provisioning Details

What Elements does on the first deploy to a new machine:

  1. SSH in as the configured user.
  2. Verify the OS is Linux with systemd and curl available.
  3. Create the dedicated service users (each Elements service runs as its own low-privilege user; no service runs as root).
  4. Install the Elements binaries at /elements/bin/.
  5. Bring up the bundled Postgres cluster (data directory /elements/db).
  6. Create the project directory at /elements/projects/<project>/ (the same layout as your local ~/elements/projects/<project>).
  7. Install OS packages from deploy.packages via apt.
  8. Set up systemd units for the app service and any custom services.
  9. Start the built-in load balancer with automatic SSL.

State is recorded in a per-machine deploy state file. Subsequent deploys read the state and skip already-completed steps. Upgrading the Elements version triggers a re-install of the Elements binaries.