Compose-Format Files and Labels Reference
EasyRunner reads Compose-format files and converts them into Podman/systemd configuration for the web host. Compose means the Docker Compose file format in this reference, not the Docker Compose CLI tool.
Required Network for Public Services
- EasyRunner creates this network during web-host initialization. Public services attach to it so Caddy can reach them.
Attach public web services to this network so Caddy can route traffic to them.
Service Labels
EasyRunner reads labels on each service entry to decide how to route and run it. The canonical labels use the xyz.easyrunner.service.* prefix.
| Label | Applied to | Meaning |
|---|---|---|
xyz.easyrunner.service.type |
Service entry | Service role. Values: web, internal, worker. |
xyz.easyrunner.service.domain |
web services |
Public domain Caddy routes to this service. Required on every web service; must be unique within the app. |
xyz.easyrunner.service.framework |
web services |
Routing/framework hint such as standardbackend or nextjs. |
xyz.easyrunner.service.port |
web services |
Port the service listens on inside the container network, quoted as a string (default 3000). |
xyz.easyrunner.service.build-context |
Service entry | Optional Flow A build context relative to the repo root. |
Label values
service.type—web(publicly routable through Caddy),internal(reachable only inside the app stack), orworker(background process with no inbound port).service.domain— the public domain for awebservice, for exampleshop.example.com. The compose file is the source of truth for routing — there is no app-level--custom-domainflag. Required on everywebservice and must be unique within the app. DNS A-records for every declared domain are provisioned at deploy time.service.framework—standardbackend(generic HTTP backend) ornextjs(Next.js-aware Caddy configuration).service.port— the container's internal listen port, quoted as a string (for example"3000"). Defaults to3000when omitted.service.build-context— a path relative to the repo root, used by Flow A to build a service from a subdirectory.
Renamed from the app* labels
Earlier releases used appNodeType, appFramework, appContainerInternalPort, and appBuildContext. Those names still work — EasyRunner falls back to them when the matching service.* label is absent — but the service.* names are canonical and recommended for new files. When both names are set on a service, the service.* value wins.
| Deprecated alias | Canonical label |
|---|---|
xyz.easyrunner.appNodeType |
xyz.easyrunner.service.type |
xyz.easyrunner.appFramework |
xyz.easyrunner.service.framework |
xyz.easyrunner.appContainerInternalPort |
xyz.easyrunner.service.port |
xyz.easyrunner.appBuildContext |
xyz.easyrunner.service.build-context |
The older xyz.easyrunner.appIsPublic: true flag is also still honored but deprecated. Mark public services with xyz.easyrunner.service.type: web instead.
Service Examples
services:
web:
image: localhost/my-app:latest
networks:
- easyrunner_proxy_network
labels:
xyz.easyrunner.service.type: web # (1)!
xyz.easyrunner.service.domain: app.example.com # (2)!
xyz.easyrunner.service.framework: standardbackend
xyz.easyrunner.service.port: "3000" # (3)!
- Marks this service as publicly routable.
- The public domain Caddy routes to this service.
- Tells Caddy which internal container port to use.
Multiple public services
An app can declare several web services, each with its own service.domain — for example a storefront, an admin, and an API on three different domains. They are all routed, and a DNS record is provisioned for each domain at deploy time.
name: medusa
services:
storefront:
image: ghcr.io/acme/storefront:latest
networks: [easyrunner_proxy_network]
labels:
xyz.easyrunner.service.type: web
xyz.easyrunner.service.framework: nextjs
xyz.easyrunner.service.port: "8000"
xyz.easyrunner.service.domain: shop.example.com
admin:
image: ghcr.io/acme/admin:latest
networks: [easyrunner_proxy_network]
labels:
xyz.easyrunner.service.type: web
xyz.easyrunner.service.framework: standardbackend
xyz.easyrunner.service.port: "7001"
xyz.easyrunner.service.domain: admin.example.com
backend:
image: ghcr.io/acme/backend:latest
networks: [easyrunner_proxy_network, medusa_internal] # (1)!
environment:
- REDIS_URL=redis://redis:6379 # (2)!
labels:
xyz.easyrunner.service.type: web
xyz.easyrunner.service.framework: standardbackend
xyz.easyrunner.service.port: "9000"
xyz.easyrunner.service.domain: api.example.com
redis:
image: docker.io/library/redis:7
networks: [medusa_internal] # (3)!
labels:
xyz.easyrunner.service.type: internal
networks:
easyrunner_proxy_network:
external: true
medusa_internal:
driver: bridge
- A
webservice that also needs a private sibling joins both the public proxy network and a private network you declare yourself. - On a private network EasyRunner registers each service under its bare compose name, so
redisresolves the normal docker-compose way. internal/workerservices stay offeasyrunner_proxy_network— they get no domain and are not publicly reachable.
The storefront can reach the backend's public API via EASYRUNNER_SERVICE_BACKEND_URL (see below), and the backend reaches redis internally by the bare name redis.
Bare-name resolution is private only
EasyRunner registers bare compose-name DNS only on private networks you
declare, never on the shared easyrunner_proxy_network, so service names
cannot collide across apps.
Build Arguments
Flow A passes standard Compose build.args through to podman build. Use this for build-time values such as NEXT_PUBLIC_* variables in Next.js apps.
Build args are not secrets
Build arguments can be visible in image history and build logs. Do not use them for passwords, tokens, or private keys. Store sensitive runtime values with App Secrets.
Auto-Injected Environment Variables
When EasyRunner deploys an app it injects a set of read-only metadata environment variables into every service container — and, for Flow A, as build args too. You can reference them from your application or from the environment: block of your Compose-format file. They are available in both Flow A and Flow B.
| Variable | Value |
|---|---|
EASYRUNNER_APP_NAME |
The EasyRunner app name. |
EASYRUNNER_APP_DOMAIN |
This service's own domain without a scheme, e.g. app.example.com (from its service.domain label). |
EASYRUNNER_APP_URL |
This service's own full HTTPS URL, e.g. https://app.example.com. |
EASYRUNNER_SERVICE_<NAME>_DOMAIN |
The bare domain of each public web service in the app, so one service can address another. |
EASYRUNNER_SERVICE_<NAME>_URL |
The full HTTPS URL of each public web service in the app. |
EASYRUNNER_SERVER_NAME |
The EasyRunner web-host name. |
EASYRUNNER_SERVER_IP |
The web host's IP address or hostname. |
<NAME> is the compose service name upper-cased with non-alphanumerics replaced by _ — e.g. store-front becomes EASYRUNNER_SERVICE_STORE_FRONT_URL.
These are plain environment variables, not secrets. EasyRunner injects them before your service's own environment: entries, so a service can override one by setting the same name. Use them to build absolute URLs, OAuth/OIDC callback URLs, and CORS allowlists without hardcoding your domain.
services:
web:
image: localhost/my-app:latest
environment:
- PUBLIC_URL=${EASYRUNNER_APP_URL} # (1)!
- Resolves to
https://app.example.comat deploy time.
EASYRUNNER_* secrets are different
App secrets you set with names beginning EASYRUNNER_ (such as EASYRUNNER_GHCR_USERNAME) are reserved for EasyRunner's own use and are not injected into your container. The metadata variables above are provided by EasyRunner and are injected. See App Secrets.
Naming Guidance
- Set
name:to an app-specific value. Avoid generic names reused across multiple apps. - Use service names that describe the process, such as
web,api,worker, orredis. - Keep secrets out of Compose-format files; use App Secrets.