JuhJuh File
JuhJuh File: Define your infrastructure as code. One file for services, resources, secrets, VMs, and deployments. Full specification with progressive examples.
The JuhJuh File describes your entire application infrastructure in code. Services, databases, caches, secrets, VMs, deployments. Drop it in your repository root. JuhJuh reads it, provisions everything, and deploys your stack.
No clicking through wizards. No manual orchestration. One source of truth.
File discovery¶
JuhJuh searches for your config in this order:
juhjuh(extensionless, recommended)juhjuh.yamljuhjuh.yml.juhjuh/directory (multiple YAML files, merged alphabetically)
The extensionless juhjuh name is the recommended convention. It keeps your project root clean and signals a JuhJuh-native file.
For projects with larger configurations, the .juhjuh/ directory splits your config across multiple files. See Multi-file configuration for details.
Start simple¶
The smallest valid JuhJuh File:
name: my-app
version: "1"
resources:
postgres:
type: postgres
version: "16"
services:
web:
type: web
image: registry.example.com/my-app:latest
port: 8000
links: [postgres]
This defines a web service backed by a database. JuhJuh handles networking, health checks, volumes, and orchestration. You can deploy this immediately through the dashboard or the CLI.
Everything below builds on this foundation. Add what you need, skip what you do not.
Root fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Project name. Used for naming containers, networks, and volumes |
version |
No | Config version (default: "1") |
variables |
No | Reusable values with type safety. Referenced as ${var.name} |
config |
No | Cross-cutting platform settings |
resources |
No | Infrastructure resources (databases, caches, queues) |
envGroups |
No | Reusable sets of environment variables |
templates |
No | Reusable service definitions |
services |
No | Running processes (web, worker, cron, proxy, daemon, job) |
environments |
No | Per-environment overrides |
vms |
No | VM provisioning specs |
vaults |
No | Secret variable declarations for CLI import |
deployments |
No | Deployment targets mapping services to VMs |
networks |
No | Organization-level network configuration |
firewall |
No | VM-level firewall rules (ports opened to the internet) |
hooks |
No | Lifecycle commands |
monitoring |
No | Observability configuration |
variables¶
Define reusable values once. Reference them anywhere in the file with ${var.name}. Variables support type checking and defaults, so you can parameterize your entire config without repeating yourself.
variables:
project_name:
type: string
default: my-app
description: Project identifier used in container and volume names
app_image:
type: string
default: registry.example.com/my-app
worker_memory:
type: string
default: 384M
worker_cpu:
type: integer
default: 1
debug_mode:
type: boolean
default: false
Variable definition fields¶
| Field | Required | Description |
|---|---|---|
type |
No | string, integer, or boolean (default: string) |
default |
No | Default value if none provided |
description |
No | Human-readable explanation |
How variables resolve¶
JuhJuh resolves ${var.name} references in two modes:
Full-value replacement preserves the original type. If a variable is type: integer with default: 2, and you write cpu: ${var.worker_cpu}, the resolved value is the integer 2, not the string "2".
Inline replacement always produces a string. If you write image: ${var.app_image}:latest, the result is "registry.example.com/my-app:latest".
Variables are resolved after all files are merged (in multi-file mode) and before schema validation. Any unresolved ${var.name} reference after resolution raises an error listing every unresolved path.
Type coercion¶
| Declared type | Behavior |
|---|---|
string |
Converts to string. None becomes "" |
integer |
Converts to int. Raises an error if not coercible. None becomes 0 |
boolean |
true, 1, yes (case-insensitive) become True. Everything else becomes False. None becomes False |
Using variables¶
services:
web:
type: web
image: ${var.app_image}:latest
compute:
memory: ${var.worker_memory}
cpu: ${var.worker_cpu}
config¶
Cross-cutting settings that apply to all services.
config:
organization: my-team
region: your-region
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
network:
name: my-app-network
driver: bridge
volumes:
postgres_data:
name: my-app-postgres-data
redis_data:
name: my-app-redis-data
| Field | Description |
|---|---|
organization |
Your JuhJuh organization slug |
region |
Default deployment region |
logging |
Container log driver and options |
network |
Network name and driver |
volumes |
Named volume definitions |
mounts |
Bind mount configurations |
build |
Build-time settings (context, args) |
deploy |
Default deployment strategy for all services |
resources¶
Infrastructure resources your services depend on: databases, caches, message queues.
resources:
postgres:
type: postgres
version: "16"
database: my_app
config:
max_connections: 200
shared_buffers: 256MB
persistence:
volume: postgres_data
path: /var/lib/postgresql/data
init_script: ./scripts/init-db.sh
redis:
type: redis
version: "7"
config:
appendonly: "yes"
maxmemory: 96mb
maxmemory_policy: allkeys-lru
Supported resource types¶
| Type | Default port | Auto-injected variables |
|---|---|---|
postgres |
5432 | DATABASE_URL, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD |
redis |
6379 | REDIS_URL, REDIS_HOST, REDIS_PORT |
rabbitmq |
5672 | AMQP_URL, RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USER, RABBITMQ_PASSWORD |
elasticsearch |
9200 | ELASTICSEARCH_URL |
mongodb |
27017 | MONGODB_URL |
When you link a service to a resource, JuhJuh automatically injects these connection variables. No manual wiring needed.
Resource fields¶
| Field | Description |
|---|---|
type |
Resource type (see table above, or any custom type) |
version |
Version tag (e.g., "16" for database version 16) |
plan |
Resource tier: dev, starter, standard, premium (default: standard) |
database |
Database name to create |
config |
Type-specific tuning (image overrides, connection limits, memory policies) |
persistence |
Volume mapping, data paths, init scripts, config file mounts |
shared |
Whether this resource is shared across deployments (default: false) |
from |
Reference to an external resource. When set, this resource is not provisioned by JuhJuh |
ephemeral |
Whether data persists between restarts (default: false) |
ipAllowList |
IP addresses allowed to connect directly |
firewall |
Firewall rules for this resource (port, protocol, label, source ranges) |
External resources¶
Use the from field to reference a resource managed outside of JuhJuh. External resources are not provisioned or started by JuhJuh, but their connection details can still be wired through links and env groups.
resources:
external-db:
type: postgres
from: managed-rds-instance
Persistence options¶
persistence:
volume: postgres_data
path: /var/lib/postgresql/data
init_script: ./scripts/init.sh
config_files:
- ./my.conf:/etc/my.conf:ro
Firewall rules¶
Each rule requires a port number. Use access to control whether the port is reachable from the public internet or only within your private network. Use label for a short display name shown in the infrastructure diagram.
resources:
postgres:
type: postgres
firewall:
- port: 5432
protocol: tcp
access: internal
label: PostgreSQL
description: Only reachable within the VPC
| Field | Required | Description |
|---|---|---|
port |
Yes | Port number (integer) |
protocol |
No | tcp or udp (default: tcp) |
access |
No | public (open to internet) or internal (VPC only). Default: public |
label |
No | Short display name, e.g. HTTP, HTTPS, PostgreSQL. Shown in diagrams and UI |
allowedSources |
No | Allowed IP ranges (default: ["0.0.0.0/0"]). Automatically cleared when access: internal. Legacy alias: source_ranges |
description |
No | Longer explanation of the rule's purpose |
envGroups¶
Reusable sets of environment variables. Define them once, reference them from multiple services with envFrom.
envGroups:
shared-app:
clear:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
REDIS_URL: redis://redis:6379/0
DEBUG: ${DEBUG:-False}
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost}
secret:
- POSTGRES_PASSWORD
- SECRET_KEY
Variable types¶
Clear variables are plain key-value pairs. They support ${VAR:-default} syntax for runtime defaults (resolved by the container, not by JuhJuh).
Secret variables are listed by name only. Their values come from the Vault at deploy time and are never stored in the JuhJuh File. Mark any variable holding credentials, API keys, or tokens as secret.
Secret entries can also be specified as objects with additional metadata:
secret:
- POSTGRES_PASSWORD
- key: API_SECRET
description: Third-party API key
templates¶
Reusable service definitions. Services reference templates with extends to inherit configuration without duplication.
templates:
app-common:
image: registry.example.com/my-app:${IMAGE_TAG:-latest}
dockerfile: Dockerfile
links: [postgres, redis]
envFrom: [shared-app]
restart: unless-stopped
volumes:
- static_files:/app/staticfiles
- media_files:/app/media
app-privileged:
extends: app-common
privileged: true
envFrom: [shared-app, dind]
Templates can extend other templates. JuhJuh resolves the full chain and merges configurations, with the child's non-null values taking precedence. Volumes are merged: template volumes first, then service-specific volumes.
| Field | Description |
|---|---|
type |
Service type (default: worker for templates) |
extends |
Inherit from another template |
image |
Container image with optional ${IMAGE_TAG} interpolation |
dockerfile |
Path to Dockerfile for build-based deployments |
command |
Override the container entrypoint command |
entrypoint |
Override the container entrypoint |
links |
Resources this service connects to |
envFrom |
Environment groups to include |
env |
Additional environment variables (clear and secret) |
compute |
CPU and memory limits |
depends_on |
Startup dependencies with conditions |
restart |
Restart policy: always, unless-stopped, on-failure, never |
privileged |
Run in privileged mode (required for sandboxed execution) |
volumes |
Volume mounts |
services¶
Running processes that make up your application.
services:
web:
type: web
extends: app-common
port: 8000
command: /app/scripts/start-web.sh
healthCheck:
path: /health/
interval: 30s
timeout: 10s
retries: 3
startPeriod: 30s
deploy:
strategy: rolling
preCommand: /app/scripts/migrate.sh
worker-default:
type: worker
extends: app-common
command: /app/scripts/start-worker.sh
compute:
memory: 384M
cpu: 0.50
worker:
queue: default
concurrency: 2
beat:
type: cron
extends: app-common
command: /app/scripts/start-beat.sh
replicas: 1
migrate:
type: job
extends: app-common
command: /app/scripts/migrate.sh
restart: never
Service types¶
| Type | Purpose | Example |
|---|---|---|
web |
HTTP-serving process | Application server, API gateway |
worker |
Background task processor | Queue consumer, job runner |
cron |
Scheduled task runner | Task scheduler, periodic jobs |
proxy |
Reverse proxy or load balancer | Traffic routing, SSL termination |
daemon |
Long-running background process | Certificate renewal, log shipper |
job |
One-shot task that runs and exits | Database migrations, seed scripts |
Service fields¶
| Field | Description |
|---|---|
type |
One of: web, worker, cron, proxy, daemon, job |
extends |
Template to inherit from |
image |
Container image (overrides template). Use ./ for build mode |
dockerfile |
Dockerfile path for build mode |
port |
Primary listening port |
ports |
Exposed port mappings (e.g., "80:80", "443:443"). Takes precedence over port |
command |
Container command |
entrypoint |
Container entrypoint |
links |
Resource connections |
dependsOn |
Service startup dependencies with conditions |
envFrom |
Environment groups to include |
env |
Inline environment variables |
compute |
CPU and memory limits |
replicas |
Number of instances |
restart |
Restart policy |
deploy |
Deployment strategy and hooks |
scaling |
Auto-scaling policy |
autoStop |
Cost-saving auto-stop after idle timeout |
healthCheck |
Health check configuration |
volumes |
Volume mounts |
profiles |
Conditional inclusion profiles |
group |
Grouping label for the UI |
enabled |
Whether this service runs (default: true) |
worker |
Worker-specific metadata (queue, concurrency, tasks) |
upstream |
Proxy upstream target |
ssl |
SSL/TLS configuration |
watchPatterns |
File patterns that trigger rebuilds during development |
ignorePatterns |
File patterns to exclude from watch |
firewall |
Firewall rules for exposed ports (port, protocol, label) |
Health checks¶
healthCheck:
path: /health/
command: ["CMD", "pg_isready"]
interval: 30s
timeout: 10s
retries: 3
startPeriod: 40s
Use path for HTTP services (generates a curl-based check against localhost:{port}{path}). Use command for everything else. If both are set, command takes precedence.
| Field | Default | Description |
|---|---|---|
path |
/health/ |
HTTP path to check |
command |
none | Shell command or exec form |
interval |
30s |
Time between checks |
timeout |
10s |
Max wait per check |
retries |
3 |
Failures before unhealthy |
startPeriod |
40s |
Grace period after container start |
Compute specs¶
compute:
memory: 384M
cpu: 0.50
memoryReservation: 128M
| Field | Description |
|---|---|
memory |
Memory limit (e.g., 384M, 2G). Default: 384M |
cpu |
CPU cores, fractional allowed (e.g., 0.50, 2.0). Default: 1.0 |
memoryReservation |
Guaranteed minimum memory (soft limit). If not set, estimated at 50% of the memory limit |
Deployment hooks¶
deploy:
strategy: rolling
preCommand: /app/scripts/migrate.sh
postCommand: /app/scripts/collect-assets.sh
healthCheckGracePeriod: 60s
stopGracePeriod: 30s
Pre-commands run before new containers start. Post-commands run after containers are healthy. Use pre-commands for database migrations, post-commands for cache warming or static file collection.
Worker metadata¶
worker:
queue: default
concurrency: 2
tasks:
- app.tasks.send_email
- app.tasks.process_upload
Auto-scaling¶
scaling:
min: 1
max: 4
targetCPU: 70
targetMemory: 80
cooldownScaleUp: 5m
cooldownScaleDown: 10m
Auto-stop¶
autoStop:
enabled: true
idleTimeout: 15m
Useful for preview and development environments to reduce costs.
Dependencies¶
dependsOn:
web:
condition: service_started
postgres:
condition: service_healthy
Available conditions: service_started, service_healthy, service_completed_successfully.
Linking a service to a resource automatically creates a service_healthy dependency. You do not need to add it manually.
Proxy configuration¶
services:
nginx:
type: proxy
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
upstream:
target: web
ssl:
enabled: "true"
dependsOn:
web:
condition: service_started
The upstream field tells the proxy which service to forward traffic to.
Profiles¶
Profiles let you conditionally include services based on the deployment context. A service with profiles set only runs when at least one of its profiles is active.
services:
debug-console:
type: daemon
extends: app-common
profiles: [development, staging]
When resolving an environment, services whose profiles do not intersect with the active profile set are disabled.
environments¶
Per-environment overrides that modify the base configuration. Use environments to change resource sizes, service compute, or settings between production, staging, and development.
environments:
production:
config:
region: your-region
resources:
postgres:
plan: premium
config:
max_connections: 400
services:
worker-default:
compute:
memory: 1024M
cpu: 1.0
replicas: 2
staging:
inherit: production
services:
worker-default:
compute:
memory: 512M
replicas: 1
Environment fields¶
| Field | Description |
|---|---|
inherit |
Parent environment to inherit from (deep-merged before this environment's overrides) |
resources |
Resource-level overrides (merged by resource name) |
services |
Service-level overrides (merged by service name) |
config |
Config section overrides |
envOverrides |
Environment group overrides (add or replace clear/secret vars in named env groups) |
Inheritance¶
Environments can inherit from other environments. Overrides are deep-merged: scalars and lists replace the parent, dicts merge recursively. Circular inheritance is detected and rejected.
Disabling services per environment¶
Set enabled: false in an environment override to exclude a service:
environments:
production:
services:
debug-console:
enabled: false
Environment group overrides¶
Use envOverrides to add or replace variables in an env group for a specific environment without redefining the entire group:
environments:
production:
envOverrides:
shared-app:
clear:
DEBUG: "False"
ALLOWED_HOSTS: "*.example.com"
Clear values are merged (new keys added, existing keys overwritten). New secret keys are appended if not already present.
links¶
Links connect services to resources. When you link a service to a resource, JuhJuh injects the connection details as environment variables automatically.
Simple links¶
links: [postgres, redis]
Renamed links¶
links:
- resource: postgres
as:
DATABASE_URL: PRIMARY_DB_URL
host: PRIMARY_DB_HOST
port: PRIMARY_DB_PORT
Use as to rename injected variables when your application expects specific names, or when a service connects to multiple databases.
vms¶
Define VM provisioning specs directly in the JuhJuh File. When you run juhjuh apply, JuhJuh creates or updates these VMs to match.
vms:
production-app:
name: my-app-prod
env_type: production
cpu: 4
memory_gb: 16
disk_gb: 200
region: ${var.region}
production-db:
name: my-app-db
env_type: production
cpu: 2
memory_gb: 8
disk_gb: 100
region: ${var.region}
VM fields¶
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | VM instance name (lowercase, hyphens, 2-63 chars) | |
env_type |
No | development |
production, staging, development, or testing |
cpu |
No | 2 |
Number of CPU cores |
memory_gb |
No | 8 |
Memory in GB |
disk_gb |
No | 200 |
Total disk in GB |
boot_disk_size_gb |
No | 20 |
Boot disk size in GB (when using two-disk layout) |
app_disk_size_gb |
No | 50 |
Application disk size in GB (when using two-disk layout) |
region |
No | Deployment region | |
deploy_command |
No | Custom deploy command for non-standard setups |
If you specify boot_disk_size_gb and app_disk_size_gb, JuhJuh provisions two separate disks: a smaller boot disk for the OS and a larger application disk for your data and containers. If only disk_gb is set, a single disk is used.
VMs defined here are provisioned the same way as VMs created through the dashboard wizard. They follow the same lifecycle (provisioning, staging, running) and appear on the VM dashboard.
vaults¶
Declare the variables each deployment needs, along with their initial values. Vault declarations let you seed secrets from the JuhJuh File or from your local environment, while actual secret values are stored securely in the Vault.
vaults:
app-vault:
variables:
SECRET_KEY:
value: ${from_env}
is_secret: true
description: Application secret key
DATABASE_PASSWORD:
value: ${from_env}
is_secret: true
description: Database password
APP_ENV:
value: production
description: Application environment
files:
ssl-cert:
path: /etc/ssl/certs/app.crt
description: SSL certificate file
Vault variable fields¶
| Field | Required | Description |
|---|---|---|
value |
Yes | The variable value. Use ${from_env} to read from your local environment at import time |
is_secret |
No | Whether the value is encrypted at rest (default: false) |
description |
No | Human-readable explanation of the variable |
The ${from_env} sentinel¶
When a vault variable has value: ${from_env}, the CLI reads the value from your current shell environment when you run juhjuh vault import or juhjuh apply. If the environment variable is not set, the entry is skipped with a warning.
This lets you keep actual secrets in your local environment or CI pipeline, never in the config file.
Vault files¶
Use files to declare file-type vault entries:
files:
ssl-cert:
path: /etc/ssl/certs/app.crt
description: SSL certificate for the application
When you run juhjuh vault import, JuhJuh reads these declarations and creates or updates vault entries on the server. For more on managing vault entries, see Vault.
deployments¶
Deployments map services and resources to target VMs. Each deployment declares what runs where, which image to use, which vault to attach, and how deployments depend on each other.
deployments:
production-db:
vm: production-db
image: pgvector/pgvector
tag: pg16
vault: db-vault
resources: [postgres]
network: app-network
expose:
- port: 5432
alias: postgres
protocol: tcp
production-app:
vm: production-app
image: ${var.app_image}
tag: latest
vault: app-vault
services: [nginx, web, worker-default, worker-analysis, beat, migrate]
resources: [redis]
envGroups: [shared-app]
registry:
url: registry.example.com
username: ${REGISTRY_USER}
password: ${REGISTRY_PASSWORD}
dependsOn:
- deployment: production-db
condition: healthy
network: app-network
Deployment fields¶
| Field | Required | Description |
|---|---|---|
vm |
Yes | Target VM name (must match a VM in the vms section or an existing VM) |
image |
No | Container image to deploy |
tag |
No | Image tag (default: latest) |
vault |
No | Vault name to attach for secret injection |
services |
No | Service names to include in this deployment |
resources |
No | Resource names to include in this deployment |
envGroups |
No | Environment groups to attach to this deployment |
registry |
No | Private registry credentials (url, username, password) |
dependsOn |
No | Other deployments this one depends on |
expose |
No | Ports exposed on the organization network |
network |
No | Network name for cross-deployment communication |
environment |
No | Logical environment name (staging, production, dev) — must match a key under the top-level environments: block. Do not use a deployment or application name here. |
files |
No | Local files to sync to the VM at deploy time |
Deployment dependencies¶
Deployments can depend on other deployments. JuhJuh waits for the dependency to reach the specified condition before proceeding.
dependsOn:
- deployment: production-db
condition: healthy
Available conditions: healthy (default), started, completed.
JuhJuh validates the dependency graph at parse time. Circular dependencies are rejected. Deployments within the same dependency level run in parallel; levels execute sequentially.
Exposed ports¶
Expose services on the organization network so other deployments can reach them.
expose:
- port: 5432
alias: postgres
protocol: tcp
- port: 6379
alias: redis
Aliases must be unique within a deployment. Other deployments reference exposed ports through deployment references.
Registry authentication¶
For private container registries, provide credentials:
registry:
url: registry.example.com
username: ${REGISTRY_USER}
password: ${REGISTRY_PASSWORD}
Credentials support runtime variable substitution. When using vault-backed credentials, make sure the deployment references a vault that contains those keys.
Scoping services and resources¶
The services and resources lists scope what gets included in the generated orchestration config for this deployment. If you have 10 services defined globally but a VM only runs 3 of them, list those 3 in the deployment.
deployments:
production-app:
vm: production-app
services: [nginx, web, worker-default]
resources: [redis]
production-db:
vm: production-db
resources: [postgres]
When scoping is used, only the listed services and resources are included in the orchestration output, along with their file mappings and firewall rules. If no scoping is set, all services and resources are included.
File sync¶
Sync local files to the target VM at deploy time:
deployments:
production-app:
vm: production-app
files:
- source: ./nginx/nginx.conf
target: /etc/nginx/nginx.conf
readonly: true
description: Nginx configuration
| Field | Required | Description |
|---|---|---|
source |
Yes | Local file path (relative to the config file) |
target |
Yes | Destination path on the VM |
readonly |
No | Mount as read-only (default: true) |
description |
No | Human-readable explanation |
JuhJuh also collects files automatically from resource config_files, init_script paths, and bind-mount volumes in templates and services.
firewall¶
Root-level firewall rules control which ports are opened on the VM. Use access to declare whether a port faces the public internet or stays within your private network.
firewall:
- port: 80
label: HTTP
access: public
description: Web traffic
- port: 443
label: HTTPS
access: public
description: Secure web traffic
- port: 5432
label: PostgreSQL
access: internal
description: Database, VPC only
Public rules (access: public, the default) appear in the Internet zone of the infrastructure diagram with connection lines from the internet to your proxy or service. Internal rules (access: internal) stay within the VPC and do not draw internet connections.
The label field is a short display name (e.g. HTTP, HTTPS, PostgreSQL) shown in diagrams and detail panels. If label is not set, description is used as the display name.
See Firewall rules under resources for the full field reference.
networks¶
Define organization-level networks for cross-deployment communication.
networks:
app-network:
name: my-app-network
scope: organization
driver: bridge
subnet: auto
| Field | Description |
|---|---|
name |
Network name |
scope |
Network scope (default: organization) |
driver |
Network driver (default: bridge) |
subnet |
Subnet allocation (default: auto) |
Deployments on the same network can communicate with each other. Use expose in deployments to make specific ports reachable across the network.
hooks¶
Lifecycle commands that run at specific events.
hooks:
pre_deploy:
- /app/scripts/migrate.sh
- /app/scripts/collect-assets.sh
post_deploy:
- /app/scripts/warm-cache.sh
Deployment references¶
When deployments depend on each other, you can reference runtime state from one deployment inside another. This lets you wire services across VMs without hardcoding IP addresses or ports.
The syntax is ${deployment.<name>.<attribute>}.
envGroups:
shared-app:
clear:
POSTGRES_HOST: ${deployment.production-db.host}
POSTGRES_PORT: ${deployment.production-db.port.postgres}
POSTGRES_ENDPOINT: ${deployment.production-db.endpoint.postgres}
Available attributes¶
| Attribute | Returns | Example |
|---|---|---|
host |
IP address of the deployment's VM | 192.0.2.5 |
status |
Current deployment status | healthy |
network |
Network name assigned to the deployment | app-network |
port.<alias> |
Port number for an exposed service | 5432 |
endpoint.<alias> |
Full host:port string for an exposed service |
192.0.2.5:5432 |
Rules¶
- You can only reference deployments listed in the referencing deployment's
dependsOn. JuhJuh validates this at parse time and rejects references to deployments not declared as dependencies - Port and endpoint references require the target deployment to expose that alias
- Deployment references are resolved at deploy time from live infrastructure state, not at parse time
- If a referenced deployment is not yet provisioned or has no host assigned, the deploy fails with an error
Three variable resolution phases¶
JuhJuh resolves variables in three phases, each with different syntax:
| Phase | Syntax | Resolved when | Example |
|---|---|---|---|
| 1. Config variables | ${var.name} |
After file merge, before validation | ${var.app_image} |
| 2. Deployment references | ${deployment.name.attr} |
At deploy time, from live state | ${deployment.prod-db.host} |
| 3. Runtime variables | ${ENV_VAR:-default} |
At container start, by the runtime | ${DEBUG:-False} |
Phase 1 variables are fully resolved before Phase 2 runs. Phase 3 variables are passed through untouched and resolved by the container runtime.
Multi-file configuration¶
For larger projects, split your JuhJuh File into a .juhjuh/ directory with multiple YAML files. JuhJuh loads all .yaml and .yml files in the directory, sorts them alphabetically, and deep-merges them into a single config.
.juhjuh/
00-variables.yaml
01-base.yaml
02-resources.yaml
03-env-groups.yaml
04-templates.yaml
05-services.yaml
06-environments.yaml
07-vms.yaml
08-deployments.yaml
Why split files?¶
A single juhjuh file works perfectly for small-to-medium projects. Split files help when:
- Your config exceeds a few hundred lines
- Multiple team members edit different parts of the stack
- You want clear separation between services, resources, and deployment targets
- You want to review infrastructure changes in smaller, focused diffs
Naming convention¶
Prefix files with numbers to control merge order. Lower numbers are loaded first. Later files override earlier files for scalar and list values. Dicts merge recursively.
Example: variables file¶
# .juhjuh/00-variables.yaml
variables:
project_name:
type: string
default: my-saas-app
app_image:
type: string
default: registry.example.com/my-app
app_cpu:
type: integer
default: 4
app_memory_gb:
type: integer
default: 16
db_cpu:
type: integer
default: 2
db_memory_gb:
type: integer
default: 8
region:
type: string
default: your-region
Example: base config¶
# .juhjuh/01-base.yaml
name: ${var.project_name}
version: "1"
config:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
postgres_data:
name: ${var.project_name}-postgres-data
redis_data:
name: ${var.project_name}-redis-data
static_files:
name: ${var.project_name}-static
Example: deployments file¶
# .juhjuh/08-deployments.yaml
deployments:
production-db:
vm: production-db
image: pgvector/pgvector
tag: pg16
vault: db-vault
resources: [postgres]
network: app-network
expose:
- port: 5432
alias: postgres
production-app:
vm: production-app
image: ${var.app_image}
tag: latest
vault: app-vault
services: [nginx, web, worker-default, beat, migrate]
resources: [redis]
envGroups: [shared-app]
dependsOn:
- deployment: production-db
condition: healthy
network: app-network
networks:
app-network:
scope: organization
driver: bridge
subnet: auto
Merge rules¶
| Value type | Merge behavior |
|---|---|
| Scalar (string, number, boolean) | Later file wins |
| List | Later file replaces the entire list |
| Dict | Keys merge recursively. Nested scalars/lists follow the rules above |
How JuhJuh uses the file¶
When you deploy through JuhJuh (dashboard or CLI), the platform:
- Discovers the JuhJuh File in your repository
- Merges all files (if using
.juhjuh/directory) - Resolves config variables (
${var.name}) - Validates the merged config against the schema
- Resolves the target environment (applies overrides and inheritance)
- Generates orchestration configuration from the resolved config
- Resolves deployment references (
${deployment.name.attr}) from live state - Provisions resources and syncs files to the target VM
- Injects vault secrets as environment variables
- Deploys the services
graph LR
A[JuhJuh File] --> B[Merge & Resolve Variables]
B --> C[Validate Schema]
C --> D[Resolve Environment]
D --> E[Generate Config]
E --> F[Resolve Deployment Refs]
F --> G[Provision & Deploy]Validation¶
JuhJuh validates your config in 8 phases: YAML syntax, variable definitions, variable completeness, variable resolution, schema validation, cross-reference checks (VM names, resource/service names, image/registry requirements, env group references), optional online vault verification, and local file existence. Run juhjuh validate to check your config without deploying. Use --strict to treat warnings as errors.
Converting from existing orchestration¶
Already have container orchestration configured? JuhJuh can work with raw orchestration files if no JuhJuh File is present. Migration is optional and incremental. You can start by adding a minimal JuhJuh File alongside your existing config and expand it over time.
CLI workflow¶
The JuhJuh CLI reads the same file and drives the full provisioning and deployment cycle from your terminal.
# Validate your config
juhjuh validate
# Preview what would change
juhjuh apply --dry-run
# Provision VMs, create vaults, and deploy
juhjuh apply
# Deploy a specific target
juhjuh deploy create production-app --tag v2.1.0
# Import vault declarations
juhjuh vault import production-app
# Check deployment status
juhjuh deploy list
The CLI auto-discovers the JuhJuh File in the current directory. Pass --file to point to a specific file or directory. See the CLI reference for the full command list.
Complete single-file example¶
name: my-saas-app
version: "1"
variables:
app_image:
type: string
default: registry.example.com/my-app
worker_memory:
type: string
default: 512M
config:
organization: my-team
network:
name: app-network
driver: bridge
volumes:
postgres_data:
name: app-postgres-data
redis_data:
name: app-redis-data
static_files:
name: app-static
resources:
postgres:
type: postgres
version: "16"
database: my_app
persistence:
volume: postgres_data
path: /var/lib/postgresql/data
redis:
type: redis
version: "7"
config:
maxmemory: 128mb
maxmemory_policy: allkeys-lru
envGroups:
shared:
clear:
POSTGRES_HOST: ${deployment.production-db.host}
POSTGRES_PORT: ${deployment.production-db.port.postgres}
REDIS_URL: redis://redis:6379/0
DEBUG: ${DEBUG:-False}
secret:
- POSTGRES_PASSWORD
- SECRET_KEY
- STRIPE_SECRET_KEY
templates:
app:
image: ${var.app_image}:${IMAGE_TAG:-latest}
links: [redis]
envFrom: [shared]
restart: unless-stopped
volumes:
- static_files:/app/staticfiles
services:
nginx:
type: proxy
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
upstream:
target: web
dependsOn:
web:
condition: service_started
web:
type: web
extends: app
port: 8000
command: /app/scripts/start-web.sh
healthCheck:
path: /health/
interval: 30s
timeout: 10s
retries: 3
deploy:
strategy: rolling
preCommand: /app/scripts/migrate.sh
worker-default:
type: worker
extends: app
command: /app/scripts/start-worker.sh
compute:
memory: ${var.worker_memory}
cpu: 0.50
worker:
queue: default
concurrency: 4
beat:
type: cron
extends: app
command: /app/scripts/start-beat.sh
replicas: 1
migrate:
type: job
extends: app
command: /app/scripts/migrate.sh
restart: never
environments:
production:
resources:
postgres:
plan: premium
services:
worker-default:
compute:
memory: 1024M
cpu: 1.0
staging:
inherit: production
services:
worker-default:
compute:
memory: 384M
vms:
production-app:
name: my-app-prod
env_type: production
cpu: 4
memory_gb: 16
disk_gb: 200
production-db:
name: my-app-db
env_type: production
cpu: 2
memory_gb: 8
disk_gb: 100
deployments:
production-db:
vm: production-db
image: pgvector/pgvector
tag: pg16
vault: db-vault
resources: [postgres]
expose:
- port: 5432
alias: postgres
network: app-network
production-app:
vm: production-app
image: ${var.app_image}
tag: latest
vault: app-vault
services: [nginx, web, worker-default, beat, migrate]
resources: [redis]
envGroups: [shared]
dependsOn:
- deployment: production-db
condition: healthy
network: app-network
networks:
app-network:
scope: organization
driver: bridge
subnet: auto
Related¶
- CLI: the command-line tool that reads, validates, and applies your JuhJuh File
- Infrastructure Overview: how the JuhJuh File fits into the broader infrastructure model
- VMs: the compute instances provisioned by your file
- Deployments: what happens when JuhJuh applies your file
- Vault: where secret variables defined in
envGroupsare stored - Resources: external services your file's resources map to
- Billing: understand costs for VMs and infrastructure provisioned by your file