Configuration Reference
Complete reference for Jiji configuration files (.jiji/deploy.yml or
jiji.<environment>.yml).
Configuration File Structure
Jiji uses YAML to define your infrastructure. The default file is
.jiji/deploy.yml; you can also create environment-specific configs like
jiji.staging.yml or jiji.production.yml.
# Use default .jiji/deploy.yml
jiji deploy
# Use environment-specific config
jiji -e staging deploy # Uses jiji.staging.yml
jiji -e production deploy # Uses jiji.production.yml
# Use a custom config file
jiji -c /path/to/custom.yml deployWhen no config file is specified, Jiji searches upward from the current
directory for .jiji/deploy.yml (or jiji.{environment}.yml when
-e/--environment is given), continuing up the directory tree until a
config file is found or the filesystem root is reached.
Minimal Configuration
project: myapp
builder:
engine: docker
registry:
port: 31270
ssh:
user: deploy
servers:
server1:
host: server1.example.com
services:
web:
image: nginx:latest
servers:
- server1
ports:
- "80"Project
project (required)
Unique identifier for your application. Used to namespace container names, audit logs, and deployment locks.
project: myappBuilder
Controls how container images are built and where they’re stored.
builder:
# Container engine (required): docker or podman
engine: docker
# Enable build cache (optional, default: true)
cache: true
# Remote builder SSH connection (optional)
# Omit this field to build on the local machine.
remote: ssh://builder@192.168.1.50:22Local development:
builder:
engine: docker
cache: trueRemote building (offload to a dedicated build server):
builder:
engine: docker
remote: ssh://builder@build-server.example.com
cache: trueIf the configured engine (Docker or Podman) is not already installed on
the remote builder host, jiji build installs it automatically. It also
upgrades Podman when the installed version is older than the required
5.8.4. This is the same distro-aware installer jiji server setup uses for
a deployment host, and jiji prints a status line confirming what it
installed or upgraded.
On Debian and Ubuntu, the installer uses the pinned
mgoltzsche/podman-static
5.8.4 bundle and verifies its archive checksum before installation. This
bundle is an unofficial, single-maintainer Podman distribution. jiji
doesn’t do anything else to a builder host beyond that: no network,
WireGuard, or proxy setup, only the engine itself. Multi-architecture
tooling (Buildx for Docker, manifest support for Podman) is not
auto-installed - if your build needs more than one platform, install
Buildx/manifest support on the builder yourself first; jiji only detects
and reports whether it’s there. The build context is streamed over SSH
into a staging directory on the builder, which is always cleaned up
afterward, including when the build fails.
When builder.registry.server is set, Jiji uses a remote registry. Jiji logs
in on the builder itself because that machine runs the push. When server is
absent, Jiji opens a reverse SSH tunnel from the builder’s
127.0.0.1:<registry.port> to the local registry. This is the same fixed-port
design that jiji deploy uses for a deployment host. See
Local Registry Details
for details. Two concurrent jiji build runs for the same project against the
same builder will race for that port; the loser fails with an actionable
message. jiji build has no deployment lock, so either serialize
concurrent builds against one builder, or give each run its own
builder.registry.port.
CI/CD (always fresh builds):
builder:
engine: docker
cache: falseRegistry
Where built images are stored and pulled from, configured under
builder.registry.
Local registry - a loopback registry with SSH reverse tunnels to
deployment hosts, useful for development. jiji runs it on
localhost:31270, tunnels it to each remote server for the duration of the
deploy, and tears the tunnel down afterward.
builder:
registry:
port: 31270 # optional, defaults to 31270Remote registry:
builder:
registry:
server: ghcr.io
username: myuser
password: GITHUB_TOKEN| Registry | server | Auto namespace | Result |
|---|---|---|---|
| GHCR | ghcr.io | username | ghcr.io/username/project-service:version |
| Docker Hub | docker.io | username | docker.io/username/project-service:version |
| Custom | registry.example.com:5000 | none | registry.example.com:5000/project-service:version |
password can be a literal value, an ALL_CAPS secret name resolved from
.env/host-env (see Environment Variables below), or $(a local command)
- useful for registries like AWS ECR or GCP Artifact Registry that issue short-lived tokens, see Registry Reference.
SSH
ssh:
user: deploy # required
port: 22 # optional, default 22
connect_timeout: 30 # optional, seconds, default 30
command_timeout: 300 # optional, seconds, default 300Authentication - ssh-agent is used by default if no keys are specified.
Configure one or more identities with keys:
ssh:
user: deploy
keys:
- ~/.ssh/id_ed25519
- /path/to/deploy_keyEach keys entry accepts a literal path or inline private key, an ALL_CAPS variable
resolved from .env (or the host environment with --host-env), or
$(a local command). Resolved values beginning with a private-key PEM header
are used as inline key material; other values are treated as paths.
# Inline key material can use the same keys list
ssh:
user: deploy
keys:
- |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----# Disable ssh-agent, use only the listed keys
ssh:
user: deploy
keys_only: true
keys:
- ~/.ssh/deploy_keyProxy / jump hosts
# ProxyJump
ssh:
user: deploy
proxy: bastion.example.com
# or deploy@bastion.example.com, or bastion.example.com:2222# ProxyCommand, connect through an arbitrary command
ssh:
user: deploy
proxy_command: "ssh -W %h:%p bastion.example.com"proxy_command supports the %h/%p/%r/%% tokens (other OpenSSH
tokens aren’t substituted) and is mutually exclusive with proxy on the
same server. The command is spawned as a subprocess and its stdio becomes
the SSH transport, matching real OpenSSH behavior.
SSH config file
ssh:
user: deploy
config: true # load ~/.ssh/config
# or: config: ~/.ssh/custom_config
# or: config: [~/.ssh/config, ~/.ssh/work_config]Host-specific settings (wildcards supported), HostName/User/Port/
IdentityFile, ProxyJump/ProxyCommand, and ConnectTimeout/
IdentitiesOnly are all inherited. Explicit ssh: fields in deploy.yml
take precedence over the SSH config file.
Advanced
ssh:
user: deploy
max_concurrent_starts: 30 # limit concurrent SSH connections
pool_idle_timeout: 900 # seconds before idle connections close
dns_retries: 3 # retry DNS lookups with backoff
log_level: error # debug, info, warn, error, fatal
options: # optional, raw SSH client options
StrictHostKeyChecking: "no"options: is a map of raw SSH client option names to values, applied
alongside the structured ssh: fields above.
Network
Enables the private, encrypted WireGuard mesh with automatic .jiji
service discovery. See Network Reference for the
full design.
Most projects must omit network:. Jiji derives stable project-specific
ranges from project:.
| Setting | Default | Description |
|---|---|---|
enabled | true | Enable the private network |
management_cidr | Project-derived /24 | WireGuard host-address pool override |
container_cidr | Project-derived /16 | Routed container-address pool override |
dns_forwarders | 1.1.1.1, 8.8.8.8 | Resolvers used for any query outside this project’s own .jiji zone |
The .jiji domain is internal-only and not customizable - service
discovery always resolves names like myapp-api.jiji.
Every deployed service container’s resolv.conf only ever has this
project’s own agent as its nameserver, so a normal internet hostname (an
API your app calls, a package registry, etc.) is resolved by forwarding the
query to dns_forwarders rather than by the container also getting the
host’s own DNS servers. Override this list to point at a home router,
Pi-hole, or other local resolver instead of the public default.
The derived ranges depend only on project:, not the checkout directory.
Jiji checks the host routes and the range markers of other projects before it
changes the network. Use overrides for a LAN, VPN, cloud VPC, or rare project
slot collision. See Network Reference.
# Avoid CIDR conflicts with an existing network
network:
management_cidr: "172.20.0.0/24"
container_cidr: "172.21.0.0/16"# Disable networking entirely
network:
enabled: falsejiji deploy, jiji service restart, and jiji service rollback require
the private network. Use enabled: false only when the configuration will not
run these service operations.
Service discovery example
servers:
server1:
host: server1.example.com
server2:
host: server2.example.com
server3:
host: server3.example.com
services:
api:
servers:
- server1
- server2
database:
servers:
- server3# From the api container, connect to database
DATABASE_URL: postgresql://user:pass@myapp-database.jiji:5432/myapp
# From the database container, connect to api
API_URL: http://myapp-api.jiji:3000
# Reach only the api replica on server1
API_SERVER1_URL: http://myapp-api-server1.jiji:3000Servers
servers:
app-amd:
host: app-amd.example.com
arch: amd64
app-arm:
host: app-arm.example.com
arch: arm64
user: ubuntu
port: 2222
keys:
- ~/.ssh/app-armhost is required and is the SSH destination unless an enabled OpenSSH
configuration supplies a matching HostName. -H/--hosts filters match
both the config key name (app-arm) and the configured host value
(app-arm.example.com).
arch is optional and defaults to amd64. The supported values are
amd64 and arm64. When a service targets servers with both architectures,
Jiji builds it for both linux/amd64 and linux/arm64. Multi-platform
images must be pushed, so jiji build --no-push is unavailable for that
service.
Each server can override these top-level ssh: defaults:
| Field | Resolution order |
|---|---|
user | server, top-level ssh.user, matching OpenSSH User |
port | server, non-default ssh.port, matching OpenSSH Port, 22 |
keys | server keys, top-level ssh.keys, matching OpenSSH IdentityFile |
key_passphrase | server, top-level ssh.key_passphrase |
keys is always a list, including when only one identity is configured. The
former singular key_path field has been removed. Other SSH options, including
timeouts, proxy settings,
keys_only, DNS retries, and pool limits, are global. key_passphrase is
a literal value, it is not resolved from .env or
the host environment.
Services
Each service is a deployable unit - a containerized application, either from a pre-built image or built from source.
services:
web:
image: nginx:latest # pre-built image
servers:
- server1 # required: target servers
- server2
ports: # optional
- "80"
- "443"Image names are normalized before Docker or Podman operations. nginx:latest becomes docker.io/library/nginx:latest, and owner/image:tag becomes docker.io/owner/image:tag. Already-qualified registries such as ghcr.io/owner/image, localhost:5000/image, and private registry hostnames remain unchanged.
Servers and scale
services:
web:
image: nginx:latest
servers:
- server1 # literal deploy target list
- server2
- server3
scale: 2 # optional, default: 1servers: is the literal deploy target list: every listed server gets a
deployment, always. scale: is the instance count on each listed server,
not a total across them — scale: 2 here runs 2 on server1, 2 on server2,
and 2 on server3 (6 total), not 2 total. Change the runtime count without
editing config with jiji service scale N -S web (or --reset to return to
the configured value).
Scheduled jobs (crons)
Each service can define multiple scheduled commands. The map key is the stable job name.
services:
worker:
image: ghcr.io/example/worker:latest
servers:
- server1
- server2
environment:
secrets:
- DATABASE_URL
crons:
sync-reports:
schedule: "7 */2 * * *"
command: ["npm", "run", "sync:reports"]
remove-expired:
schedule: "0 3 * * *"
command: ["npm", "run", "remove-expired"]
timezone: America/Denver
timeout: 30m
overlap: forbid
missed_runs: skip| Setting | Default | Description |
|---|---|---|
schedule | required | Five-field cron expression: minute, hour, day of month, month, day of week |
command | required | Command string or argument list for the one-off container |
timezone | UTC | IANA time-zone name |
timeout | 1h | Maximum run time, with an s, m, or h suffix |
overlap | forbid | Skip a due run while the prior run remains active |
missed_runs | skip | Do not replay times missed while the owning agent was offline |
Each run uses the service image, environment, secrets, mounts, resources,
project network, and .jiji DNS. It does not use the service command,
ports, proxy routes, health checks, or restart policy.
Run jiji deploy after a cron configuration change. See
Scheduled Jobs for ownership, failure behavior,
retention, and commands.
Build configuration
Use build: instead of image: to build from source (only one of the two
is allowed per service). A bare string is shorthand for context::
services:
web:
build: ./web # shorthand for build: { context: ./web }services:
web:
build:
context: . # optional, default: .
dockerfile: Dockerfile # optional, default: Dockerfile
target: production # optional, build target for multi-stage Dockerfiles
args:
NODE_ENV: production
VERSION: 1.2.3
servers:
- server1context: can be omitted entirely from the detailed form - it defaults to
the project root, same as leaving it out of the shorthand:
services:
site:
build:
dockerfile: Dockerfile # context defaults to the project root
servers:
- server1args: is a mapping of build-arg names to values, not a list of
KEY=value strings.
Secret values (API tokens, private registry credentials) don’t belong in
args: - a build argument ends up readable in docker history and image
metadata forever. Use secrets: instead, which mounts the value into the
build via Docker/Podman’s --secret flag and never writes it into the
image:
services:
api-backend:
build:
context: ./api
secrets:
- NPM_TOKEN
- PIP_INDEX_PASSWORDEach name is resolved the same way environment.secrets names are - from
the selected .env file, then the host environment if --host-env is
passed - but never from environment.clear: a build secret exists
specifically to bypass the cleartext build-arg path, so mixing in a
cleartext source would defeat the point. The Dockerfile reads it as a
mounted file, not an environment variable:
RUN --mount=type=secret,id=NPM_TOKEN \
NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) npm installClassic (non-buildx) docker build only understands --secret when
BuildKit is active; Jiji sets DOCKER_BUILDKIT=1 automatically for that
one build invocation when secrets: is configured, so this needs no setup
step from you. buildx build and podman build support --secret
natively. Works for local builds, builder.remote, and multi-architecture
builds - the CLI stages each secret to a mode-0600 temporary file (locally)
or a mode-0600 file on the builder host (remotely, piped over SSH stdin,
never embedded in a command string) and removes it once the build
finishes, whether it succeeded or failed.
dockerfile: is resolved relative to context:, matching Docker
Compose - if context: ./web, dockerfile: Dockerfile (the default)
already means web/Dockerfile; there’s no need to repeat the web/
prefix. A dockerfile: value can still climb out of the context with
../ for local builds (the local engine reads the filesystem directly),
but a remote builder can only stage what’s inside the configured context,
so that combination is rejected there.
Port mappings
For services behind the proxy, specify only the container port - this lets the old and new container coexist during a deploy, since neither binds a host port:
ports:
- "80"
- "3000"Binding a host port (host:container) means only one container can hold
that port at a time, which rules out zero-downtime deploys for that
service - pair it with stop_first: true (see
Stateful services) or the candidate will
simply fail to bind the port and the deploy will fail outright. Use this
form only for services that need direct host port access without the proxy
(databases, non-HTTP services):
ports:
- "80:80"
- "53/udp" # container port with protocol
- "53:53/udp" # host:container with protocol
- "127.0.0.1:8080:80" # bind to localhost onlyVolume mounts
volumes:
- "/data/web/logs:/var/log/nginx"
- "web_storage:/opt/uploads"
- "./data:/opt/extra_data:ro"Note: Named volumes (those not starting with
/or./) are automatically prefixed with the service name to prevent conflicts between services. For example, if a service namedwebdefinesweb_storage:/opt/uploads, the actual volume created isweb-web_storage. Host path mounts are not modified.
File and directory mounts
# String format: local:remote[:options], options can be ro, z, or Z
files:
- "nginx.conf:/etc/nginx/nginx.conf:ro"
directories:
- "html:/usr/share/nginx/html:ro"Or the hash format, for custom permissions and ownership:
files:
- local: config/secret.key
remote: /etc/app/secret.key
mode: "0600"
owner: "nginx:nginx"
options: "ro"Environment variables
Shared variables (project-level, applied to every service):
environment:
clear:
APP_ENV: production
LOG_LEVEL: info
secrets:
- API_KEY
- DATABASE_PASSWORDService-specific variables, merged with the shared set:
services:
web:
environment:
clear:
NODE_ENV: production
PORT: 3000
secrets:
- API_KEYsecrets entries are ALL_CAPS names resolved from .env files (or host
environment variables with --host-env), never written literally into
config. Numbers and booleans under clear are converted to strings for
container compatibility (DEBUG: true becomes "true").
Resolved secrets are staged on the target server at
.jiji/{project}/env/{service}-{server}.env, mode 0600, written over SSH
so the value never appears as a command-line argument or in shell history.
This file is not removed by jiji service remove; it persists (root-owned,
mode 0600) until jiji server teardown clears the project’s .jiji/
directory.
Custom .env location
secrets_path: config/secrets # optional, default: .envJiji looks for {secrets_path}.{environment} first (e.g.
config/secrets.production with -e production), then falls back to
secrets_path itself. With the default secrets_path, that means
.env.{environment} then .env.
External secrets adapters
The schema also accepts a top-level secrets: block describing an external
adapter:
secrets:
adapter: doppler
project: myapp
config: productionThis currently parses but has no effect: no adapter implementation reads
it yet, so configuring it changes nothing and produces no warning. Use
.env-resolved secrets: entries under environment: (above), or a
$(command) value for builder.registry.password (see
Registry Reference), for anything that needs a
secret today.
Proxy
Enables jiji-proxy routing for a service.
proxy:
port: 3000
hosts:
- myapp.example.com
ssl: false# Multiple hostnames
proxy:
port: 3000
hosts:
- myapp.example.com
- www.myapp.example.com
ssl: trueWildcard subdomains - a hosts entry may be a single-label wildcard, so
one service can catch every direct subdomain of a domain
(foo.example.com and bar.example.com both match; the nested
deep.foo.example.com doesn’t, since * only covers one label; neither
does the bare example.com):
proxy:
port: 3000
hosts:
- "*.example.com"A wildcard host cannot use ssl: true (jiji-proxy’s automatic certificate
provisioning can’t issue a wildcard certificate); provide your own
certificate via ssl: { certificate_pem, private_key_pem } instead, or
skip TLS for that host. See the Proxy Reference
for details.
HTTP health check:
proxy:
port: 3000
hosts:
- myapp.example.com
healthcheck:
path: /health
interval: 10s
timeout: 5s
deploy_timeout: 60sCommand health check (cmd takes precedence if both cmd and path
are set - not enforced as a validation error, so pick one):
proxy:
port: 3000
hosts:
- myapp.example.com
healthcheck:
cmd: "test -f /app/ready" # exit 0 = healthy
cmd_runtime: docker # optional, defaults to builder.engine
interval: 10s
timeout: 5s
deploy_timeout: 60sOther command examples: pgrep -f myapp, /app/healthcheck.sh,
curl -f http://localhost:3000/health.
Path prefix routing:
proxy:
port: 3000
hosts:
- myapp.example.com
path_prefix: /apiMultiple targets (multiple ports on one service):
proxy:
targets:
- port: 3900
hosts:
- s3.example.com
healthcheck:
path: /health
- port: 3903
hosts:
- admin.example.com
ssl: true
healthcheck:
cmd: "test -f /ready"Raw TCP proxying publishes a non-HTTP service on a dedicated public TCP
port. port is the backend container port and listen_port is the public
port accepted by jiji-proxy:
services:
postgres:
image: postgres:18
servers:
- db1
- db2
proxy:
port: 5432
listen_port: 15432
healthcheck:
cmd: "pg_isready -U appuser -d app"Setting listen_port selects raw TCP mode. It cannot be combined with
HTTP-only path_prefix or ssl. Ports 0, 80, and 443 are reserved,
and every TCP route on a shared host needs a unique public port. hosts is
optional metadata for a TCP target, not a routing key. Open the configured
listen_port in the server firewall and point clients at any ingress server
that owns the route. See the Proxy Reference
for routing and multi-project details.
Container Runtime options
services:
worker:
image: myapp:latest
servers:
- server1
command: ["./run-worker.sh", "--queue", "high"]
# or as a string: command: "./run-worker.sh --queue high"services:
monitoring:
image: prometheus:latest
servers:
- server1
network_mode: bridge # bridge (default), service:<other-service-name>, or hostnetwork_mode: none is rejected by validation: every service needs a
reachable address for DNS and health checks. Use service:<name> to
explicitly share another service’s namespace, or crons: for isolated
one-off/scheduled work. Docker’s native container:<id> syntax is also
rejected outright by validation.
Host networking (network_mode: host) — shares the host’s own network
namespace instead of getting a project-bridge address.
services:
app:
image: nginx:latest
servers:
- server1
network_mode: host
ports:
- "8080" # what the service listens on; never rendered as -pports: accepts at most one entry, a bare container-side port number only
(never a host:container mapping or a /udp suffix): it’s metadata for the
per-server uniqueness check below, not a port mapping — the app must
already bind the port it wants directly on the host. Two host-mode
services whose servers overlap can’t declare the same port; this check
only sees services within the same project, so a different project’s
host-mode service on a shared machine can still collide, surfacing as a
failed container start instead of a validation error. proxy: and
scale above 1 are rejected, the same as for service:<name> sharing.
Host networking removes jiji’s mesh isolation for that container: it can
reach, and be reached by, anything the host’s own network can, bypassing
the project bridge entirely.
Container namespace sharing (network_mode: service:<name>) — shares
another (“upstream”) service’s container network namespace instead of
getting its own dynamically-leased address. This is the standard “VPN
killswitch” pattern: a torrent client sharing a VPN gateway container’s
network stack, so all its traffic is forced through the tunnel.
services:
gluetun:
image: qmcgaw/gluetun:latest
servers:
- server1
proxy:
port: 8080
hosts:
- torrents.example.com
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
servers:
- server1
network_mode: service:gluetunNaming the upstream via network_mode is itself the dependency declaration
— there’s no separate depends_on field. Redeploying gluetun (via
jiji deploy, jiji service restart, or jiji service rollback)
automatically redeploys qbittorrent too, sequenced strictly after
gluetun’s own deploy finishes. A dependent can’t set scale above 1 or
configure its own proxy: block: traffic reaches it through the upstream’s
own route, at the upstream’s address. The upstream cannot itself be a
network_mode: service:<name> dependent because chained sharing is not
supported. The dependent’s servers must be a subset of the upstream’s.
services:
api:
image: myapp:latest
servers:
- server1
cpus: 2 # or a string: "1.5"
memory: "512m" # "512m", "1g", "2gb"services:
ml-worker:
image: pytorch:latest
servers:
- gpu-server
gpus: "all" # or "0", "0,1", "device=0"services:
video-processor:
image: ffmpeg:latest
servers:
- server1
devices:
- "/dev/video0"
- "/dev/snd"services:
system-tool:
image: debug-tools:latest
servers:
- server1
privileged: true # use with cautionservices:
network-tool:
image: nettools:latest
servers:
- server1
cap_add:
- NET_ADMIN
- SYS_PTRACEStateful services (stop_first)
services:
database:
image: postgres:15
servers:
- db-server
stop_first: trueUse stop_first: true for services that can’t run two instances at once -
file-based locks (LevelDB, SQLite), or coordinator registration by a unique
ID. This trades zero downtime for a brief stop-then-start window during
deploy. Default is false.
Restart policy
services:
worker:
image: myapp/worker:latest
servers:
- server1
restart: on-failure # unless-stopped (default), always, on-failure, noPassed straight through to the container engine’s --restart flag.
Image retention (retain)
services:
web:
build:
context: .
servers:
- server1
retain: 5 # optional, default: 3Only relevant for build-configured services (a static image: service is
never pruned). Sets how many recent image tags each server keeps before
removing the rest, as long as they aren’t still referenced by a running
container. After every successful deploy, restart, or rollback, jiji
pushes this value to every server in the service’s servers: list, and
each server’s jiji-agent prunes to it continuously in the background -
no need to run anything manually for routine cleanup. Run jiji service prune (or jiji service prune --retain N) to prune immediately instead
of waiting for the next reconcile tick, or to override this value for one
run.
Project limits and validation
Jiji rejects a configuration that exceeds these project limits:
| Resource | Maximum |
|---|---|
| Servers | 32 |
| Services | 500 |
| Logical replicas, across all services | 2,000 |
| Cron jobs on one service | 32 |
| Cron jobs across the project | 1,000 |
Scaled services must use project bridge networking. A service with more than
one replica cannot use local volumes, managed files or directories, privileged
mode, devices, or GPUs. A stop_first service must remain a singleton.
For network_mode: service:<name>, the dependent must remain a singleton. Its
server list must be a subset of the upstream server list. Chained namespace
sharing is not supported.
Raw TCP proxy ports must be unique within one project. Ports 80 and 443 are
reserved for HTTP ingress. A raw TCP target cannot set path_prefix or ssl.
Jiji-proxy also rejects conflicts with routes from other projects on the same
host when it applies the route.
Complete example
project: myapp-production
builder:
engine: docker
cache: false
registry:
server: ghcr.io
username: myorg
password: GITHUB_TOKEN
ssh:
user: deploy
keys:
- ~/.ssh/production_key
proxy: bastion.example.com
environment:
clear:
APP_ENV: production
LOG_LEVEL: warn
servers:
web1:
host: web1.example.com
web2:
host: web2.example.com
api1:
host: api1.example.com
db1:
host: db1.example.com
services:
web:
build:
context: ./web
dockerfile: Dockerfile.production
servers:
- web1
- web2
ports:
- "3000"
proxy:
port: 3000
hosts:
- app.example.com
- www.app.example.com
ssl: true
healthcheck:
path: /health
interval: 10s
api:
build:
context: ./api
servers:
- api1
ports:
- "4000"
environment:
secrets:
- DB_PASSWORD
proxy:
port: 4000
hosts:
- api.example.com
ssl: true
path_prefix: /api
healthcheck:
path: /api/health
database:
image: postgres:15
servers:
- db1
volumes:
- "/data/postgres:/var/lib/postgresql/data"
environment:
clear:
POSTGRES_PASSWORD: DB_PASSWORDValidation
Jiji validates configuration before it runs a command. It collects applicable validation errors after YAML deserialization succeeds. A YAML or type error can stop deserialization before those checks run. Common validation failures include:
Error: A project supports at most 32 servers
Error: Registry credentials require builder.registry.server
Error: A service with stop_first must remain a singletonA typo’d field name is not always caught. servers:, builder:,
environment:, build:, and the top-level document reject unknown YAML
keys silently rather than erroring - a typo like pott: instead of
ports: inside a service is just ignored. ssh:, network:, proxy:,
individual server entries, and crons: entries do reject unknown keys.
When a setting doesn’t seem to take effect, double-check spelling against
this reference rather than assuming Jiji would have flagged it.
Run with -v/--verbose for more detailed validation output:
jiji -v deploy