Skip to content

Concepts

Meridian is an imperative deploy tool: one CLI process connects over SSH, writes Podman Quadlets, asks user systemd to reload, and uses kamal-proxy for proxied traffic. Blue/Green remains the default; strategy: recreate adds a deliberate-downtime path for stateful single-instance services. This page is the mental model for reading deploy logs, debugging failures, and running multiple apps on one host.

Deploy Flow

Default Blue/Green Flow

Proxied web deploys use Blue/Green when strategy is omitted and run this sequence on each selected host:

  1. Validate locally and run pre_deploy.
  2. Acquire the deploy lock before remote mutation starts.
  3. Verify the setup-created service network.
  4. Transfer the selected image by registry pull, stream, or incremental.
  5. Upload the new color .container, file syncs, and asset units.
  6. Run systemctl --user daemon-reload.
  7. Wait for co-network accessories to pass readiness probes.
  8. Run remote before_start hooks.
  9. Run the asset builder when assets: is configured.
  10. Start and directly healthcheck the inactive color.
  11. Run kamal-proxy deploy to atomically switch traffic.
  12. Stop the old color and remove its inactive Quadlet file.
  13. Record active-color, release-state.json, and manifest.json.
  14. Run final hooks and release the deploy lock.

Recreate Flow

strategy: recreate is one serial transaction across every app role on one host. Images, networks, candidate Quadlets, systemd reload, and accessory readiness are prepared while the old release still runs. A first deploy has no existing route, so it skips maintenance.

On a redeploy Meridian runs kamal-proxy stop <service>, stops all active secondary roles, and then stops the old web color. Only after those stops complete does it upload files:, run after_upload/before_start, and start the candidate web color. The web healthcheck must pass before cron, worker, or other secondary roles start. Meridian then updates the proxy target and runtime state while the route remains in maintenance, removes the old Quadlet, and finally runs kamal-proxy resume <service>.

Old and new web colors are never active together. Accessories are not app roles: they remain running throughout the transaction and only participate through their readiness checks.

If anything fails after maintenance begins, Meridian does not restart the old release, roll back an image, or resume traffic. Persistent data may already have been migrated. An unhealthy candidate is stopped; a healthy candidate is kept for diagnosis and repair. The route remains intentionally blocked until the operator repairs the service or restores image, database, and volumes from a matching backup, then resumes it manually.

For field-level details, see servers.<role>.proxy.healthcheck, accessories.<name>.ready, and hooks.

What Is A Quadlet

A Quadlet is a declarative Podman file under ~/.config/containers/systemd/. After systemctl --user daemon-reload, systemd generates normal user units from those files, so containers are started, stopped, logged, and restarted through systemd.

text
~/.config/containers/systemd/
  my-app.network
  my-app-blue.container
  my-app-green.container
  my-app-workers.container
  my-app-postgres.container
  my-app-assets.volume
  my-app-assets-builder.container
  my-app-assets-server.container
  kamal-proxy.container
  meridian-proxy.network
FilePurpose
<service>.networkPrivate Podman network for one app and its accessories.
<service>-<color>.containerBlue or green app container for a proxied managed role.
<service>-<role>.containerStable restart-in-place container for a non-proxied managed role.
<accessory>.containerStandalone accessory service such as Postgres or Redis.
<service>-assets-*Asset volume, builder, and static-server units when assets: is configured.
kamal-proxy.containerShared host-level proxy container.
meridian-proxy.networkShared network kamal-proxy and proxied app containers join.

Use meridian quadlet to preview generated files locally. meridian setup owns uploading and starting <service>.network on every host that needs the private service network; deploys, one-off runs, and service-networked accessories verify that the materialized Podman network <service> exists before they use it.

Deploy-Managed Static Assets

When deploy.yml declares an assets: block, Meridian publishes your built front-end bundle as part of the deploy:

  1. A one-shot builder container runs assets.command in the app image.
  2. Its assets.output_dir output is copied into a timestamped release directory on the <service>-assets volume.
  3. A current symlink is repointed to the new release.
  4. A generated Caddy static server serves current and is registered with kamal-proxy under the <service>-assets route on assets.host.

Old releases are retained (assets.retain_releases) so fingerprinted URLs from the previous version keep resolving during the rollout window. The framework's asset URL setting must point at assets.host; see assets and, for fingerprinted-URL mistakes, CSS url() 404s.

Per-Service Runtime State

Meridian stores runtime state per service, not globally:

text
~/.local/state/meridian/services/my-app/
  active-color
  manifest.json
  release-state.json
  lock/
    meta.json
  audit.log
FilePurposeRead byWritten by
active-colorCurrent proxied color, blue or green.status, exec, rollbackproxied deploy, rollback
manifest.jsonOwnership manifest for proxy routes, assets, ports, accessories, generated files, and state paths.check, proxy removedeploy
release-state.jsonCurrent and previous proxied releases; only Blue/Green releases are image-rollback-safe.status, rollbackproxied deploy, rollback
lock/meta.jsonDeploy lock holder, timestamp, and optional message.lock status, deploydeploy, lock acquire, lock release
audit.logLine-oriented history of deploy, rollback, proxy, accessory, and lock operations.auditmutating commands

This layout lets multiple Meridian services share a host without sharing state. Non-proxied managed roles still contribute their role-named Quadlet to manifest.json ownership, but do not read or write active-color or release-state.json.

Same-Host Multi-App Topology

Each app owns its private service network. Proxied app containers also join the shared meridian-proxy network, where one kamal-proxy can reach all apps.

text
                           public HTTP(S)
                                |
                                v
                         kamal-proxy.container
                                |
                         meridian-proxy.network
                         /                    \
              my-app-green               my-blog-blue
                   |                           |
            my-app.network              my-blog.network
              /        \                    /        \
        postgres     dragonfly          sqlite      redis

Accessories attach to their app's private network, not to meridian-proxy, unless you explicitly configure something else. manifest.json collision checks make meridian check fail if two services claim the same proxy host/path, asset host, published host port, accessory name, generated file, or state path.

For a worked setup, see Multi-App Hosting.

Blue/Green

Meridian keeps one active color and one candidate color for the proxied managed role. web is a reserved role name: it is the only role that may declare proxy:, so exactly one role per service is deployed this way. If active-color says blue, the next deploy starts green; if it says green, the next deploy starts blue.

text
before deploy:  active-color=blue   proxy -> my-app-blue
during deploy:  blue serves traffic, green starts and passes health
after switch:   active-color=green  proxy -> my-app-green
cleanup:        old blue unit is stopped and its Quadlet is removed

The inactive Quadlet is removed after a successful switch, but release metadata keeps the previous rollback-safe release. meridian rollback reads release-state.json, starts the previous color if needed, runs kamal-proxy in reverse, rewrites active-color, swaps current/previous release metadata, and records an audit entry.

Recreate

Recreate reuses the same color-named Web Quadlets and healthcheck, but it changes their ordering: the old color is stopped before the candidate starts. This creates intentional downtime and prevents two versions from sharing a mutable database or volume. The first implementation is single-host-only, requires every app role to be managed, rejects assets:, and does not support selective deploys or automatic rollback.

Non-Proxied Managed Roles

Every managed role other than web — and web itself when it has no proxy: — has one stable unit named <service>-<role>.service. Deploy uploads the matching <service>-<role>.container and restarts it in place, so a short interruption is expected. status, logs, and exec target that role unit directly instead of consulting active-colour state.

On the first role-named deploy to a host that has no configured proxied managed role, Meridian stops and removes legacy <service>-blue and <service>-green Quadlets if they exist. It skips that cleanup when the same host still owns a proxied role, protecting the live blue/green units.

MIT License