Target: Local, ephemeral, development, and staging environments
Not for: Production deployments at scale
Orchfile is a declarative specification for service orchestration. It describes what services exist, their relationships, and resource requirements. Platform-specific tooling implements how to run them.
Orchfile is inspired by Containerfile's declarative simplicity, not docker-compose's configuration verbosity.
- Platform-agnostic specification - Orchfile defines intent, not implementation
- Explicit over implicit - No magic defaults that vary by platform
- Container XOR Host - A service is either containerized or host-native, never both
- Dependency clarity - Distinguish between ordering and requirements
- Resource-aware - Development environments need resource optimization too
- Plain text, line-oriented
- UTF-8 encoding
- Comments start with
# - Blank lines ignored
- Case-sensitive directives (uppercase)
- No continuation lines (each directive is one line)
The formal grammar is defined in W3C EBNF in grammar.ebnf. A typeset version with railroad diagrams is available in docs/grammar.pdf.
Notes: var_ref (${name}) may appear within any value, path, image_ref, or command_string. Expansion is performed at parse time using resolved ARG values. Unresolved references to built-in variables (e.g. ${ORCH_PROJECT}) are preserved for runtime resolution.
A service MUST specify exactly one of FROM (container mode) or RUN (host mode). Specifying both is a parse error. Specifying neither is a parse error.
SERVICE valid-container
FROM postgres:15
CMD -c config_file=/etc/postgres.conf
SERVICE valid-host
RUN python manage.py runserver
SERVICE invalid
FROM postgres:15
RUN postgres # ERROR: Cannot specify both FROM and RUN
The following directives are only valid in container mode (with FROM):
ENTRYPOINTCMDPUBLISHVOLUME
Using these with RUN is a parse error.
The following directives are only valid in host mode (with RUN):
USERSTOPRELOAD
Using these with FROM is a parse error.
Service dependencies defined by REQUIRES and AFTER MUST form a directed acyclic graph (DAG). Cycles are a parse error.
Defines a variable with default value, overridable at parse time.
ARG postgres_port=5433
ARG memory=4G
Override mechanisms (in priority order):
- CLI:
--arg name=value - Environment:
ORCH_ARG_name=value - Orchfile default
Variable expansion: Use ${name} syntax in any directive value.
Begins a new service block. All subsequent directives until the next SERVICE apply to this service.
SERVICE postgres
SERVICE my-app
SERVICE socat-proxy
Naming rules:
- Lowercase alphanumeric and hyphens only
- Must start with letter
- Maximum 63 characters
Declares a container-based service using the specified image.
FROM postgres:15
FROM docker.io/library/nginx:alpine
FROM public.ecr.aws/localstack/localstack:4.2
Image resolution: Platform tooling handles registry authentication and pulling.
Declares a host-based service with the specified command.
RUN python manage.py runserver 0.0.0.0:9090
RUN uvicorn --host 0.0.0.0 --port 8000 app:main
RUN /usr/bin/redis-server /etc/redis.conf
Command execution: Run via platform's process supervisor (launchd, systemd).
Override the container's entrypoint.
ENTRYPOINT /usr/sbin/nginx
Override the container's default command/arguments.
CMD postgres -c config_file=/etc/postgresql.conf
CMD -g 'daemon off;'
Map host port to container port.
PUBLISH 5433:5432
PUBLISH 8080:80
Format: host_port:container_port
Multiple allowed: Specify multiple PUBLISH directives for multiple port mappings.
Mount host path or named volume into container.
VOLUME /host/path:/container/path
VOLUME my-named-volume:/var/lib/data
VOLUME ${ORCH_DATA}/postgres:/var/lib/postgresql/data
Format: source:destination
Named volumes: If source doesn't start with /, it's treated as a named volume (created if missing).
Multiple allowed: Specify multiple VOLUME directives.
Run the service as specified user.
USER postgres
USER www-data
Default: Current user
Platform mapping:
- launchd:
UserNamekey in plist - systemd:
User=directive
Custom command to stop the service gracefully.
STOP kill -SIGTERM $(cat /var/run/myapp.pid)
STOP /usr/local/bin/myapp --shutdown
Default: Send SIGTERM to process group
Platform mapping:
- launchd: Not directly supported (uses process termination)
- systemd:
ExecStop=
Command to reload service configuration without restart.
RELOAD kill -SIGHUP $MAINPID
RELOAD nginx -s reload
Platform mapping:
- launchd: Not directly supported
- systemd:
ExecReload=
Working directory for service execution.
WORKDIR backend/canary
WORKDIR /app
Relative paths: Resolved against ${ORCH_PROJECT}
Container mode: Sets container working directory
Host mode: Sets process working directory
Set environment variable.
ENV DJANGO_SETTINGS_MODULE=canary.settings.dev
ENV DEBUG=1
ENV DATABASE_URL=postgres://localhost:5433/canary
Format: KEY=value
Multiple allowed: Specify multiple ENV directives.
Variable expansion: Values can use ${ARG_NAME} syntax.
Load environment variables from file.
ENV_FILE ${ORCH_PROJECT}/.env.local
ENV_FILE ${ORCH_DATA}/secrets.env
Format: One KEY=value per line, # comments supported.
Multiple allowed: Files loaded in order, later values override earlier.
Platform mapping:
- launchd: Parsed and inlined into plist
- systemd:
EnvironmentFile=
Hard dependency - service fails to start if required services are unavailable.
REQUIRES postgres redis
REQUIRES refresh-backend-deps
Behavior:
- Required services are started first
- If required service fails to become healthy, dependent service does not start
- Creates both ordering AND requirement relationship
Ordering dependency - start after specified services, but don't require them.
AFTER localstack
AFTER nginx
Behavior:
- Specified services are started first if they exist
- If specified service is disabled or fails, dependent still starts
- Creates ordering only, NOT requirement
Migration from DEPENDS: The legacy DEPENDS directive was ambiguous and has been removed. Convert as follows:
DEPENDS foowhere foo MUST succeed →REQUIRES fooDEPENDS foowhere foo is optional →AFTER foo
Command or URL to verify service health.
HEALTHCHECK pg_isready -h localhost -p 5433
HEALTHCHECK http://localhost:8000/health
HEALTHCHECK redis-cli -h localhost ping
Type detection:
- Starts with
http://orhttps://→ HTTP check (expect 2xx) - Otherwise → Execute command (expect exit code 0)
How long to wait for HEALTHCHECK to pass during startup.
READINESS_TIMEOUT 120s
READINESS_TIMEOUT 30s
Default: 90s
Format: Duration with unit suffix (s, m)
Service runs once and exits (not a daemon).
ONESHOT true
Default: false
Behavior:
- Service runs to completion
- Success determined by exit code
- Creates ready marker file on success
- Dependent services wait for completion
Service is defined but not started by default.
DISABLED true
Default: false
Behavior:
- Parsed and validated
- Skipped during
orch upunless explicitly named - Can be started explicitly:
orch up disabled-service
Container recreation policy.
RECREATE always
RECREATE never
Default: never
Values:
always: Destroy and recreate container on everyorch createnever: Keep existing container if present
Note: Only applies to container-mode services.
Automatic restart behavior on service failure.
RESTART no
RESTART always
RESTART on-failure
Default: no
Values:
no: Never restart automaticallyalways: Always restart when process exitson-failure: Restart only on non-zero exit code
Platform mapping:
- launchd:
KeepAlive/SuccessfulExit - systemd:
Restart=
Time to wait before restarting failed service.
RESTART_DELAY 5s
RESTART_DELAY 1m
Default: 1s
Platform mapping:
- launchd:
ThrottleInterval - systemd:
RestartSec=
Maximum restart attempts within interval before giving up.
START_LIMIT_BURST 5
Default: 5
Time window for counting restart attempts.
START_LIMIT_INTERVAL 10s
Default: 10s
Behavior: If service fails START_LIMIT_BURST times within START_LIMIT_INTERVAL, stop attempting restarts.
Maximum time to wait for service to start/become ready.
TIMEOUT_START 30s
TIMEOUT_START 5m
Default: 90s
Maximum time to wait for service to stop gracefully before force killing.
TIMEOUT_STOP 10s
TIMEOUT_STOP 30s
Default: 10s
Platform mapping:
- launchd:
ExitTimeOut - systemd:
TimeoutStopSec=
Resource limits apply to BOTH container and host services.
Maximum memory allocation.
MEMORY 4G
MEMORY 512M
Format: Number with unit suffix (K, M, G)
Platform mapping:
- Container:
--memoryflag - launchd host: Not enforced (advisory)
- systemd host:
MemoryMax=
CPU core allocation.
CPUS 2
CPUS 0.5
Format: Number (can be fractional)
Platform mapping:
- Container:
--cpusflag - launchd host: Not enforced (advisory)
- systemd host:
CPUQuota=(converted to percentage)
CPU percentage limit (more precise than CPUS for host services).
CPU_QUOTA 50%
CPU_QUOTA 200%
Format: Percentage (>100% allowed for multi-core)
Platform mapping:
- launchd: Not enforced
- systemd:
CPUQuota=
Maximum open file descriptors.
LIMIT_NOFILE 65536
Default: System default
Platform mapping:
- launchd:
SoftResourceLimits/HardResourceLimits→NumberOfFiles - systemd:
LimitNOFILE=
Maximum number of processes/threads.
LIMIT_NPROC 4096
Default: System default
Platform mapping:
- launchd:
SoftResourceLimits/HardResourceLimits→NumberOfProcesses - systemd:
LimitNPROC=
Maximum number of tasks (threads + processes).
TASKS_MAX 4096
Platform mapping:
- launchd: Not enforced
- systemd:
TasksMax=
IO scheduling weight relative to other services.
IO_WEIGHT 100
IO_WEIGHT 500
Format: 10-1000 (default 100)
Platform mapping:
- launchd: Not enforced
- systemd:
IOWeight=
Destination for standard output.
STDOUT ${ORCH_STATE_DIR}/logs/myapp.log
STDOUT /var/log/myapp.log
Default: Platform-specific
- launchd:
${ORCH_STATE_DIR}/logs/${SERVICE_NAME}.log - systemd: journal
Destination for standard error.
STDERR ${ORCH_STATE_DIR}/logs/myapp.err
STDERR /var/log/myapp.err
Default: Platform-specific
- launchd:
${ORCH_STATE_DIR}/logs/${SERVICE_NAME}.err - systemd: journal
Orchfile supports multi-file composition using a systemd drop-in inspired overlay model. A base Orchfile can be overlaid with environment-specific files (staging, CI, personal) that tune resources, ports, env vars, and dependencies without forking the entire file.
orch parse base.orch staging.orch personal.orch [--arg ...]
orch validate base.orch staging.orch personal.orch [--arg ...]
Files are merged left-to-right. The leftmost file is the base; each subsequent file is an overlay.
Any scalar directive (FROM, RUN, WORKDIR, MEMORY, CPUS, etc.) in an overlay replaces the base value. Omitting a scalar in an overlay preserves the base value.
# base.orch
SERVICE web
FROM nginx:1.24
MEMORY 1G
# staging.orch — overrides image, preserves MEMORY
SERVICE web
FROM nginx:1.25
- ENV: Merged by variable name (overlay wins on conflict)
- PUBLISH: Merged by container port (overlay replaces host port for same container port)
- VOLUME: Merged by destination path (overlay replaces source for same destination)
# base.orch
SERVICE web
FROM nginx
ENV MODE=production
PUBLISH 8080:80
# staging.orch — MODE overridden, DEBUG added, port 80 remapped
SERVICE web
ENV MODE=staging
ENV DEBUG=true
PUBLISH 9090:80
- REQUIRES: Appended, duplicates removed
- AFTER: Appended, duplicates removed
- ENV_FILE: Appended, duplicates removed
# base.orch
SERVICE web
FROM nginx
REQUIRES db
# staging.orch — adds cache dependency
SERVICE web
REQUIRES cache
# Result: REQUIRES db cache
Services in overlays that don't exist in the base are appended.
Setting FROM in an overlay on a service that had RUN clears all host-only directives (USER, STOP, RELOAD) and switches to container mode. Setting RUN clears all container-only directives (ENTRYPOINT, CMD, PUBLISH, VOLUME) and switches to host mode.
The CLEAR directive resets a list-type field before applying overlay values. Without CLEAR, overlay values merge with (append to) base values.
CLEAR ENV
CLEAR ENV_FILE
CLEAR PUBLISH
CLEAR VOLUME
CLEAR REQUIRES
CLEAR AFTER
Valid targets: Only list-type directives can be cleared. Using CLEAR on scalar directives (e.g., CLEAR FROM) is a parse error — scalars override by last-wins naturally.
# base.orch
SERVICE web
FROM nginx
ENV OLD_KEY=old_value
ENV KEEP=this
# overlay.orch — wipe all env, start fresh
SERVICE web
CLEAR ENV
ENV FRESH=new_value
# Result: only FRESH=new_value (OLD_KEY and KEEP are gone)
ARGs are merged left-to-right (last wins). Variable expansion is deferred until all files are merged, so overlays can redefine ARG defaults that affect the base file's ${var} references.
# base.orch
ARG port=8080
SERVICE web
FROM nginx
PUBLISH ${port}:80
# staging.orch
ARG port=9090
# Result: port=9090, so PUBLISH becomes 9090:80
CLI --arg overrides take highest priority, above all file defaults.
Constraints (C1-C4) are validated on the merged result, not on individual files. An overlay file that only sets ENV without FROM or RUN is valid — it inherits the mode from the base file.
Available for expansion in directive values:
| Variable | Description |
|---|---|
${ORCH_PROJECT} |
Project root directory |
${ORCH_DATA} |
Data directory for persistent storage |
${ORCH_STATE_DIR} |
Orchestrator state directory |
${ORCH_CONTAINERS_DIR} |
Containers scripts directory |
${SERVICE_NAME} |
Current service name (in STDOUT/STDERR) |
${PORT_OFFSET} |
Port offset for parallel environments |
${CONTAINER_PREFIX} |
Container name prefix |
ARG postgres_port=5433
ARG postgres_memory=4G
ARG django_port=9090
SERVICE postgres
FROM pgvector/pgvector:pg15
MEMORY ${postgres_memory}
CPUS 2
PUBLISH ${postgres_port}:5432
VOLUME postgres-data:/var/lib/postgresql/data
ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=canary
HEALTHCHECK pg_isready -h localhost -p ${postgres_port}
RESTART on-failure
RESTART_DELAY 5s
SERVICE redis
FROM redis:6.2.0-alpine
MEMORY 1G
CPUS 1
PUBLISH 6380:6379
RECREATE always
HEALTHCHECK redis-cli -h localhost -p 6380 ping
RESTART always
SERVICE django
RUN python manage.py runserver 0.0.0.0:${django_port}
WORKDIR backend/canary
ENV DJANGO_SETTINGS_MODULE=canary.settings.dev
ENV_FILE ${ORCH_PROJECT}/.env.local
REQUIRES postgres redis
AFTER localstack
HEALTHCHECK http://localhost:${django_port}/health
RESTART on-failure
RESTART_DELAY 2s
MEMORY 2G
LIMIT_NOFILE 65536
TIMEOUT_START 60s
SERVICE db-migrate
FROM flyway/flyway:latest
CMD -url=jdbc:postgresql://postgres/canary migrate
REQUIRES postgres
ONESHOT true
- Container services: Managed via
/usr/local/bin/container - Host services: Managed via launchd plists in
~/Library/LaunchAgents - Resource limits: Limited enforcement for host services (MEMORY/CPUS advisory only)
- Logging: File-based in
${ORCH_STATE_DIR}/logs/
- Container services: Managed via podman with systemd integration
- Host services: Managed via systemd user units in
~/.config/systemd/user/ - Resource limits: Full cgroup v2 enforcement
- Logging: journald by default, file override supported