- File discovery
- Start simple
- Root fields
- variables
- config
- resources
- envGroups
- templates
- services
- environments
- links
- vms
- vaults
- deployments
- firewall
- networks
- hooks
- Deployment references
- Multi-file configuration
- .juhjuh/00-variables.yaml
- .juhjuh/01-base.yaml
- .juhjuh/08-deployments.yaml
- Validate your config
- Preview what would change
- Provision VMs, create vaults, and deploy
- Deploy a specific target
- Import vault declarations
- Check deployment status
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:
```yaml 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.
```yaml 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¶
yaml
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.
yaml
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.
```yaml 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.
yaml
resources:
external-db:
type: postgres
from: managed-rds-instance
Persistence options¶
yaml
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.
yaml
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.
yaml
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:
yaml
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.
```yaml 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.
```yaml 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¶
yaml
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¶
yaml
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¶
yaml
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¶
yaml
worker:
queue: default
concurrency: 2
tasks:
- app.tasks.send_email
- app.tasks.process_upload
Auto-scaling¶
yaml
scaling:
min: 1
max: 4
targetCPU: 70
targetMemory: 80
cooldownScaleUp: 5m
cooldownScaleDown: 10m
Auto-stop¶
yaml
autoStop:
enabled: true
idleTimeout: 15m
Useful for preview and development environments to reduce costs.
Dependencies¶
yaml
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¶
yaml
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.
yaml
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.
```yaml 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:
yaml
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:
yaml
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¶
yaml
links: [postgres, redis]
Renamed links¶
yaml
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.
```yaml 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.
yaml
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:
yaml
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.
```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 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.
yaml
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.
yaml
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:
yaml
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.
```yaml 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:
yaml
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.
yaml
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.
yaml
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.
yaml
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>}.
yaml
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¶
```yaml
.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¶
```yaml
.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¶
```yaml
.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.
```bash
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¶
```yaml 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