Skip to Content
DocsGuidesDeployment

Deployment Guide

Zero-Downtime Deployment

Every logical replica has a stable replica ID. Each replacement gets a unique deployment ID and a durable address lease. The candidate starts beside the current Active deployment, so the running container is not interrupted:

Health Checks

HTTP health check

services: api: proxy: healthcheck: path: /health interval: 10s timeout: 5s deploy_timeout: 60s
OptionDefaultDescription
path-HTTP endpoint to check (must return 2xx)
interval2sTime between checks
timeout5sMax time to wait for a response
deploy_timeout30sMax time to wait for the container to become healthy

Command health check

services: worker: proxy: healthcheck: cmd: "test -f /tmp/ready" # exit 0 = healthy cmd_runtime: docker # optional, defaults to builder.engine interval: 10s timeout: 5s deploy_timeout: 60s

If both path and cmd are set, cmd takes precedence; this isn’t rejected by validation, so pick one deliberately rather than relying on the precedence rule.

The health check always runs directly against the candidate’s leased address, never through DNS or jiji-proxy, so a healthy result really means the new container is ready, not just that routing happens to work.

No health check configured

A service with no healthcheck: block at all still gets a gate: Jiji polls the container engine’s own readiness state (docker inspect / podman inspect status) until it reports running, up to deploy_timeout. This confirms the container started, not that the application inside it is actually accepting requests - configure path or cmd for a real application-level check.

Deployment Strategies

Full deploy:

jiji deploy --build

Targeted deploy:

jiji deploy -S api jiji deploy -S "api,worker" jiji deploy -S "web*" jiji deploy -H web1 jiji deploy -H "prod*" jiji deploy -S api -H "prod*"

Version pinning:

jiji build --version v1.2.3 jiji deploy --version v1.2.3

jiji deploy prints the deployment plan (project, environment, target servers/endpoints, build/version/proxy flags) and asks for confirmation before touching anything - before build, before network reconciliation, before any SSH connection. Pass -y/--yes to confirm automatically:

jiji deploy --build -y

-y/--yes is required when running non-interactively (CI/CD, scripts) - without a terminal attached to answer the prompt, jiji deploy refuses to hang and exits with an actionable error instead.

Rollback

The previous container keeps serving traffic until a new one passes its health check, so a failed deploy on its own doesn’t cause an outage. To go back to an already-built version on purpose:

jiji service rollback --version v1.2.2 jiji service rollback --version v1.2.2 -S api jiji service rollback --version v1.2.2 -S api -H server1.example.com

This runs the same catalog-driven replacement as jiji deploy: candidate lease, health check, catalog admission, proxy reconciliation, and old deployment cleanup. It targets the image tag you pass instead of building a new one. For a build:-configured service it resolves that tag from builder.registry directly (no rebuild, trusting a prior jiji build/jiji deploy --build already pushed it); for a static image: service, --version is applied the same way jiji deploy --version applies it.

Deployment Locks

jiji deploy, jiji service restart, and jiji service rollback automatically lock only the replica and ingress owners they mutate. They release their own locks after success or failure. An unrelated offline host does not block a targeted service deployment.

jiji lock acquire/release are a separate, whole-project lock used around network-layer maintenance (jiji network setup/backup/restore/ recover/compact) - acquiring it does not block a deploy, restart, or rollback, since those use the finer-grained per-replica lock above instead:

jiji lock acquire "Network maintenance window" jiji lock acquire "Network maintenance window" --timeout 300 # seconds to wait (default 300) jiji lock acquire "Taking over stuck network maintenance" --force jiji lock status jiji lock status --json jiji lock show jiji lock release # release the project lock jiji lock release --replica <replica-id> # release one stuck replica lock instead

Scaling

servers: is the literal deploy target list — every listed server gets a deployment. scale: sets the instance count on each listed server, not a total across them:

services: web: image: nginx:alpine servers: - app1 - app2 - app3 scale: 3

The config above runs 3 instances on app1, 3 on app2, and 3 on app3 (9 total), not 3 total. Change the replicated runtime desired count without editing configuration:

jiji service scale 5 -S web jiji service scale 0 -S web jiji service scale --reset -S web jiji service scale 3 -S web --dry-run

Scale commits desired placement first, then adds or retires replicas. If an owner is offline, retry the same command after it returns. Healthy catalog records keep serving throughout partial progress.

The explicit commands above are for maintenance windows or intentionally blocking deployments. Normal CI usage does not need manual lock management:

jiji deploy --build -e production -y

Resource Management

services: api: cpus: 1.5 # or "1.5" memory: "512m" # b, k, m, g, kb, mb, gb
services: ml-worker: gpus: "all" # or "0", "0,1", "device=0"
services: video-processor: devices: - "/dev/video0" - "/dev/snd"
services: vpn-client: cap_add: - NET_ADMIN - SYS_MODULE
services: system-tool: privileged: true

VPN Killswitch (Container Namespace Sharing)

network_mode: service:<name> shares another (“upstream”) service’s container network namespace instead of giving the dependent its own address — the standard way to force one container’s traffic through another’s tunnel, with no leak path if the upstream isn’t running.

services: gluetun: image: qmcgaw/gluetun:latest cap_add: - NET_ADMIN proxy: port: 8080 hosts: - torrents.example.com qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest network_mode: service:gluetun

Naming gluetun in qbittorrent’s network_mode is itself the dependency declaration — redeploying gluetun automatically redeploys qbittorrent too, sequenced after gluetun’s own deploy finishes. See the Configuration Reference for the full behavior and constraints.

Multi-Architecture Support

Jiji builds and deploys mixed-architecture clusters (amd64 and arm64):

servers: x86-server: host: amd64.example.com arch: amd64 # optional, defaults to amd64 arm-server: host: arm64.example.com arch: arm64 services: api: servers: - x86-server - arm-server build: context: .

Each server gets an image built for its own architecture; pulling and deployment happen transparently per host.

Stateful Services

Services that can’t run two instances at once - most databases - need stop_first, which trades zero downtime for a brief stop-then-start window:

services: postgres: stop_first: true volumes: - /data/postgres:/var/lib/postgresql/data

A stop_first service must remain a singleton - scale above 1 is rejected by validation.

A service with a fixed host-port binding (ports: ["80:80"], not a bare container port) needs stop_first: true for the same reason: the engine can’t bind two containers to the same host port at once. Without stop_first, the candidate simply fails to bind the port and the deploy fails safely - the old container is never touched - rather than degrading gracefully into a stop-then-start window automatically.

File and Directory Transfers

services: api: files: - ./config.json:/app/config.json - ./secrets.env:/app/.env:600 directories: - ./templates:/app/templates

Or the hash format for custom ownership/permissions:

services: api: files: - local: config/secret.key remote: /etc/app/secret.key mode: "0600" owner: "nginx:nginx"

Volume Mounts

services: db: volumes: - /data/postgres:/var/lib/postgresql/data - /config:/app/config:ro - app_storage:/app/data # named volume

Note: Named volumes (those not starting with / or ./) are automatically prefixed with the service name to prevent conflicts. For example, app_storage becomes db-app_storage when used by the db service.

Environment Variables

services: api: environment: clear: NODE_ENV: production LOG_LEVEL: info secrets: - DATABASE_URL - API_KEY

Secrets are ALL_CAPS names resolved from a .env file in your project root (.env.production takes precedence over .env when -e production is used):

# .env DATABASE_URL=postgres://user:pass@host:5432/db API_KEY=secret123
jiji deploy jiji deploy -e production # uses .env.production jiji secrets print # check what's resolved before deploying jiji secrets print --show-values jiji --host-env deploy # fall back to host env vars, not just .env

Image Retention

services: api: retain: 5 # keep 5 versions, default is 3
jiji service prune jiji service prune -S api --retain 3

Only build-configured services (build:, not a static image:) are ever pruned.

Best Practices

Tag every release. Deploy with an explicit version instead of a moving tag, so jiji service rollback always has something concrete to go back to:

git tag v1.2.3 && git push --tags jiji deploy --build --version v1.2.3

Watch it happen. Follow logs in a second terminal while a deploy runs, rather than only checking after the fact:

# Terminal 1 jiji deploy --build # Terminal 2 jiji service logs -S api --follow

Back up stateful data before major changes, especially for services using stop_first where there’s no zero-downtime safety net:

jiji server exec "tar -czf /backup/data-$(date +%Y%m%d).tar.gz /data" -H server1 jiji deploy

Review the audit trail regularly, not just when something’s already broken - see Logs Reference.

Pre-Deployment Checklist

  1. Test containers locally first
  2. Review deploy.yml for the change you’re about to ship
  3. Confirm server connectivity: jiji server exec "echo ok" -H server1
  4. Confirm registry access: jiji registry login
  5. Confirm secrets resolve: jiji secrets print
  6. Confirm there is no maintenance lock: jiji lock status

Post-Deployment Verification

jiji service logs -S api --since 5m jiji server exec "docker ps" jiji server exec "curl localhost:3000/health" jiji proxy logs --since 5m jiji network plan jiji audit --lines 10

Cleanup

jiji service prune jiji service prune --retain 3 jiji service prune -S api

CI/CD Integration

#!/bin/bash set -e jiji deploy --build -e production -y

See the CI/CD Integration guide for full pipeline examples.

Last updated on