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 status checks 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:

  1. Build an image from the synchronized source.

  2. Start a new container on a release-specific socket.

  3. Check the new container through that socket.

  4. Point a stable socket at the healthy release.

  5. Reload Caddy.

  6. Stop the previous container.

1. Add the deployment files

Add the deployment files to the repository:

text
ops/
  .containerignore
  Containerfile
  deploy.sh
  down.sh
  status.sh

The three shell files are Orbit hooks. Make them executable:

bash
chmod +x ops/deploy.sh ops/down.sh ops/status.sh

Orbit 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:

dockerfile
FROM python:3.14-slim-bookworm AS builder
 
WORKDIR /workspace
RUN pip install --no-cache-dir uv
COPY . /workspace
RUN --mount=type=cache,target=/root/.cache/uv uv sync --no-dev
 
FROM python:3.14-slim-bookworm
WORKDIR /workspace
COPY --from=builder /workspace /workspace
ENV 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:

bash
#!/bin/bash
set -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:

bash
revision=${SLOT_GIT_REVISION:0:12}
app_label=${SLOT_NAME//./-}
release_dir=${SLOT_RUN}/sockets/sha/${SLOT_DEPLOY_ID}
release_socket=${release_dir}/app.sock
stable_socket=${SLOT_RUN}/app.sock
container=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:

bash
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:bootstrap

This shows the boundary between Orbit values and application values:

  • Orbit exports SLOT_* and ORBIT_* to the hook.

  • orbit env writes application values to the file at $SLOT_ENV.

  • podman --env-file passes that file to the container.

  • The hook adds STARIO_UNIX_SOCKET because 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:

bash
healthy=0
for _ 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.5
done
 
if ((healthy == 0)); then
  podman logs "$container" >&2 || true
  podman rm -f "$container" || true
  rm -rf "${release_dir:?}"
  exit 1
fi

A 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:

bash
ln -sfn "sockets/sha/${SLOT_DEPLOY_ID}/app.sock" "$stable_socket"

Then write the slot Caddy configuration:

bash
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:

bash
#!/bin/bash
set -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 1

ops/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:

bash
orbit setup ship --email you@example.com
orbit create ship app.example.com
orbit env ship app.example.com --from .env.production
orbit deploy ship app.example.com
orbit status ship app.example.com

Later deployments from the remembered Git remote and branch only need:

bash
orbit deploy

To stop the application but retain its data and environment:

bash
orbit remove ship app.example.com

Use --purge only when you also want to delete $SLOT_DATA and $SLOT_ENV.

Theme

Made by Adam with Stario