Podman
This recipe builds a container on the remote host and switches traffic only after the new container is healthy. It uses Podman, Caddy, and a Unix socket, but the important part is the hook sequence. It is one implementation under Recipes. The Hooks page is the contract.
Orbit does not contain Podman-specific deployment logic. These hooks keep that knowledge in the project that needs it. Every slot runs the same hooks with its own paths and application environment.
The result has these properties:
each deployment gets a separate image, container, and socket
the old container stays active while the new one starts
a failed health check does not change the active route
durable data and application settings stay outside the source tree
orbit statuschecks the same stable socket that Caddy uses
The complete source is in the Podman slot example.
Before you begin
Complete the Guide host setup first. On the remote host, install Podman, curl, and OpenSSL. Orbit setup prepares the Caddy integration that this example uses.
The application must return a successful response from GET /health. The example starts Stario on a Unix socket, but the same sequence works with another HTTP server that can listen on that socket.
The hook will:
Build an image from the synchronized source.
Start a new container on a release-specific socket.
Check the new container through that socket.
Point a stable socket at the healthy release.
Reload Caddy.
Stop the previous container.
1. Add the deployment files
Add the deployment files to the repository:
ops/ .containerignore Containerfile deploy.sh down.sh status.shThe three shell files are Orbit hooks. Make them executable:
chmod +x ops/deploy.sh ops/down.sh ops/status.shOrbit checks ops/deploy.sh before it synchronizes the project. The other two hooks are optional, but this example needs them for clean removal and useful status.
2. Define the container image
This Containerfile builds a Stario application with uv:
FROM python:3.14-slim-bookworm AS builder WORKDIR /workspaceRUN pip install --no-cache-dir uvCOPY . /workspaceRUN --mount=type=cache,target=/root/.cache/uv uv sync --no-dev FROM python:3.14-slim-bookwormWORKDIR /workspaceCOPY --from=builder /workspace /workspaceENV PATH="/workspace/.venv/bin:$PATH"CMD ["stario", "serve", "app.main:bootstrap"]The deployment hook overrides the command so that Stario listens on /sockets/app.sock. For another server, change the command and keep the same socket contract.
3. Name the release
At the start of ops/deploy.sh, fail if a required Orbit value is missing:
#!/bin/bashset -euo pipefail : "${SLOT_NAME:?}": "${SLOT_SRC:?}": "${SLOT_DATA:?}": "${SLOT_ENV:?}": "${SLOT_RUN:?}": "${SLOT_DEPLOY_ID:?}": "${SLOT_GIT_REVISION:?}": "${ORBIT_CADDY:?}"Build names from SLOT_DEPLOY_ID. It is unique for each deploy, even when you deploy the same Git revision twice:
revision=${SLOT_GIT_REVISION:0:12}app_label=${SLOT_NAME//./-}release_dir=${SLOT_RUN}/sockets/sha/${SLOT_DEPLOY_ID}release_socket=${release_dir}/app.sockstable_socket=${SLOT_RUN}/app.sockcontainer=orbit.${app_label}.${SLOT_DEPLOY_ID}image=${app_label}:${SLOT_DEPLOY_ID}SLOT_GIT_REVISION is useful for labels and logs. Do not use it as the release identity because Orbit can deploy local changes that are not in that commit.
4. Build and start
Build from the synchronized source and start the new container:
cd "$SLOT_SRC"mkdir -p "$release_dir" "$SLOT_DATA" podman build \ -t "$image" \ -f "$SLOT_SRC/ops/Containerfile" \ --ignorefile "$SLOT_SRC/ops/.containerignore" \ "$SLOT_SRC" podman run \ -d --name "$container" \ -v "${release_dir}:/sockets:z" \ -v "${SLOT_DATA}:/data:z" \ --env-file "$SLOT_ENV" \ -e STARIO_UNIX_SOCKET=/sockets/app.sock \ --label "orbit.slot=${SLOT_NAME}" \ --label "orbit.deploy_id=${SLOT_DEPLOY_ID}" \ --label "orbit.revision=${revision}" \ "$image" \ stario serve app.main:bootstrapThis shows the boundary between Orbit values and application values:
Orbit exports
SLOT_*andORBIT_*to the hook.orbit envwrites application values to the file at$SLOT_ENV.podman --env-filepasses that file to the container.The hook adds
STARIO_UNIX_SOCKETbecause it owns the socket layout.
Orbit does not source $SLOT_ENV into the hook. A value in that file is available to the container, but it is not automatically available as a shell variable in deploy.sh.
5. Check before switching
The new container writes its socket in the release directory. Check that socket before you change the stable route:
healthy=0for _ in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' \ --max-time 2 \ --unix-socket "$release_socket" \ http://localhost/health) || code=failed if [[ $code =~ ^2[0-9]{2}$ ]]; then healthy=1 break fi sleep 0.5done if ((healthy == 0)); then podman logs "$container" >&2 || true podman rm -f "$container" || true rm -rf "${release_dir:?}" exit 1fiA failure exits the hook with a non-zero status. Orbit reports that result. The old stable socket and Caddy route have not changed, so the previous container can continue to serve traffic.
6. Publish the release
Point the stable socket at the healthy release:
ln -sfn "sockets/sha/${SLOT_DEPLOY_ID}/app.sock" "$stable_socket"Then write the slot Caddy configuration:
cat >"${SLOT_RUN}/Caddyfile" <<EOF${SLOT_NAME} { reverse_proxy unix/${stable_socket}}EOF caddy fmt --overwrite "${SLOT_RUN}/Caddyfile"caddy validate --config "$ORBIT_CADDY"caddy reload --config "$ORBIT_CADDY"Host setup makes the main Caddyfile import /srv/orbit/slots/*/run/Caddyfile. The hook only owns the route for its slot.
After Caddy accepts the new route, stop older containers with the same slot label. The complete hook includes this cleanup and handles a repeated deployment safely.
7. Add status and removal
ops/status.sh checks the stable socket and exits with the Orbit status contract:
#!/bin/bashset -euo pipefail socket=${SLOT_RUN}/app.sock[[ -S $socket ]] || { printf 'stopped\n'; exit 1; } code=$(curl -sS -o /dev/null -w '%{http_code}' \ --max-time 2 \ --unix-socket "$socket" \ http://localhost/health) || code=failed [[ $code =~ ^2[0-9]{2}$ ]] && { printf 'healthy\n'; exit 0; }printf 'unhealthy %s\n' "$code"exit 1ops/down.sh selects containers by the stable slot label, stops them, removes the socket and route, and reloads Caddy. It does not need to know which deploy is active.
This is why the hooks receive both kinds of value: deploy metadata creates isolated releases, while stable slot paths let status and removal work across releases.
8. Create the slot and deploy
Prepare the host once, then create and deploy the slot:
orbit setup ship --email you@example.comorbit create ship app.example.comorbit env ship app.example.com --from .env.productionorbit deploy ship app.example.comorbit status ship app.example.comLater deployments from the remembered Git remote and branch only need:
orbit deployTo stop the application but retain its data and environment:
orbit remove ship app.example.comUse --purge only when you also want to delete $SLOT_DATA and $SLOT_ENV.