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| Option | Default | Description |
|---|---|---|
path | - | HTTP endpoint to check (must return 2xx) |
interval | 2s | Time between checks |
timeout | 5s | Max time to wait for a response |
deploy_timeout | 30s | Max 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: 60sIf 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 --buildTargeted 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.3jiji 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.comThis 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 insteadScaling
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: 3The 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-runScale 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 -yResource Management
services:
api:
cpus: 1.5 # or "1.5"
memory: "512m" # b, k, m, g, kb, mb, gbservices:
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_MODULEservices:
system-tool:
privileged: trueVPN 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:gluetunNaming 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/dataA 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/templatesOr 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 volumeNote: Named volumes (those not starting with
/or./) are automatically prefixed with the service name to prevent conflicts. For example,app_storagebecomesdb-app_storagewhen used by thedbservice.
Environment Variables
services:
api:
environment:
clear:
NODE_ENV: production
LOG_LEVEL: info
secrets:
- DATABASE_URL
- API_KEYSecrets 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=secret123jiji 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 .envImage Retention
services:
api:
retain: 5 # keep 5 versions, default is 3jiji service prune
jiji service prune -S api --retain 3Only 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.3Watch 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 --followBack 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 deployReview the audit trail regularly, not just when something’s already broken - see Logs Reference.
Pre-Deployment Checklist
- Test containers locally first
- Review
deploy.ymlfor the change you’re about to ship - Confirm server connectivity:
jiji server exec "echo ok" -H server1 - Confirm registry access:
jiji registry login - Confirm secrets resolve:
jiji secrets print - 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 10Cleanup
jiji service prune
jiji service prune --retain 3
jiji service prune -S apiCI/CD Integration
#!/bin/bash
set -e
jiji deploy --build -e production -ySee the CI/CD Integration guide for full pipeline examples.