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:
- Validate locally and run
pre_deploy. - Acquire the deploy lock before remote mutation starts.
- Verify the setup-created service network.
- Transfer the selected image by registry pull,
stream, orincremental. - Upload the new color
.container, file syncs, and asset units. - Run
systemctl --user daemon-reload. - Wait for co-network accessories to pass readiness probes.
- Run remote
before_starthooks. - Run the asset builder when
assets:is configured. - Start and directly healthcheck the inactive color.
- Run
kamal-proxy deployto atomically switch traffic. - Stop the old color and remove its inactive Quadlet file.
- Record
active-color,release-state.json, andmanifest.json. - 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| File | Purpose |
|---|---|
<service>.network | Private Podman network for one app and its accessories. |
<service>-<color>.container | Blue or green app container for a proxied managed role. |
<service>-<role>.container | Stable restart-in-place container for a non-proxied managed role. |
<accessory>.container | Standalone accessory service such as Postgres or Redis. |
<service>-assets-* | Asset volume, builder, and static-server units when assets: is configured. |
kamal-proxy.container | Shared host-level proxy container. |
meridian-proxy.network | Shared 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:
- A one-shot builder container runs
assets.commandin the app image. - Its
assets.output_diroutput is copied into a timestamped release directory on the<service>-assetsvolume. - A
currentsymlink is repointed to the new release. - A generated Caddy static server serves
currentand is registered with kamal-proxy under the<service>-assetsroute onassets.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| File | Purpose | Read by | Written by |
|---|---|---|---|
active-color | Current proxied color, blue or green. | status, exec, rollback | proxied deploy, rollback |
manifest.json | Ownership manifest for proxy routes, assets, ports, accessories, generated files, and state paths. | check, proxy remove | deploy |
release-state.json | Current and previous proxied releases; only Blue/Green releases are image-rollback-safe. | status, rollback | proxied deploy, rollback |
lock/meta.json | Deploy lock holder, timestamp, and optional message. | lock status, deploy | deploy, lock acquire, lock release |
audit.log | Line-oriented history of deploy, rollback, proxy, accessory, and lock operations. | audit | mutating 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 redisAccessories 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 removedThe 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.
