---
title: "Project file"
summary: "Overview of the onebox.run/v1alpha2 Application resource, its envelope, spec blocks, and environment-value precedence rules."
description: "The shape of ob.yml: required keys, shorthand forms, and where each block lives."
status: shipped
read_when:
  - "Starting a new project file"
  - "Looking up which scalar shorthand forms are accepted"
  - "Working out which block a field belongs to"
---
`onebox.run/v1alpha2` is the contract for one application on one host. It goes in
`ob.yml` at the root of your repository. `ob.yaml` is accepted automatically
when `ob.yml` is absent, and `-c` accepts either spelling or any explicit path.

The resource requires `apiVersion`, `kind`, `metadata.name`, and `spec`.
Inside `spec`, both `environments` and `workloads` are required. There are no
top-level workload shorthands.

Start with the schema reference so your editor can help while you type:

```yaml
# yaml-language-server: $schema=https://onebox.run/schemas/application/v1alpha2/application.schema.json
apiVersion: onebox.run/v1alpha2
```

Use the published schema at
`https://onebox.run/schemas/application/v1alpha2/application.schema.json` for
editor completion and validation.

`ob schema --out onebox.schema.json` writes a local copy, and `ob init` puts the
published reference on the first line of a scaffolded project.

## Route declarations

Every ingress rule is explicit and uses `hostname` for both exact hosts and
wildcard hosts. Routes live under a workload in `spec.workloads`:

```yaml
spec:
  workloads:
    web:
      image: nginx
      routes:
        - {hostname: shop.example.com, port: 3000}
        - {hostname: "*.preview.example.com", port: 3000}
```

The loader rejects retired route fields instead of silently changing their
meaning. The workload-level `port` field remains available as the default
container port for HTTP health checks; it no longer creates an ingress route.

## The block map

Configuration blocks live under `spec`; the application name lives in
`metadata.name`.

| Block | What it says | Fields |
| --- | --- | --- |
| `metadata.name` | The application's name. Every derived name carries it. | [Top level](/docs/reference/fields/top-level) |
| `environments` | Where it runs, and the policy that governs deploying there. | [environments](/docs/reference/fields/environments) |
| `workloads` | The containers that are yours. | [workloads](/docs/reference/fields/workloads) |
| `services` | The supporting services Onebox runs for you. | [services](/docs/reference/fields/services) |
| `deployment` | Release order, retention, migration policy. | [deployment](/docs/reference/fields/deployment) |
| `proxy` | Who runs the proxy and what routes. | [proxy](/docs/reference/fields/proxy) |
| `runtime` | Environment files and local environment-file checks. | [runtime](/docs/reference/fields/runtime) |
| `hooks` | Commands at lifecycle seams. | [hooks](/docs/reference/fields/hooks) |
| `checks` | What must be true for a release to activate, grouped by kind. | [checks](/docs/reference/fields/checks) |
| `externalServices` | Dependencies operated outside Onebox, and how a workload reaches them. | [externalServices](/docs/reference/fields/external-services) |
| `backupTargets` | Off-host repositories a protected service writes its backups to. | [backupTargets](/docs/reference/fields/backup-targets) |
| `registries` `notifications` | Named maps. | [registries](/docs/reference/fields/registries) · [notifications](/docs/reference/fields/notifications) |

`externalServices` declares dependencies operated outside Onebox;
`backupTargets` declares off-host backup destinations. Declaring a destination
does not enable protection by itself. Executable support depends on the service
and the operation; see [Shipped vs proposed](/docs/reference/capabilities).

Unknown fields, including behavior-bearing `x-*` fields, are rejected. Store
opaque notes in `metadata.annotations`; annotations do not change the runtime.

## Shorthand

A scalar form accepted by a contract version remains accepted for that version.

| Written as | Means |
| --- | --- |
| `image: nginx` | `image: {reference: nginx}` |
| `health: /healthz` | `health: {http: /healthz}` |
| `server: root@203.0.113.10` | `server: {user: root, host: 203.0.113.10}` |
| `jump: deploy@bastion.example.com:2222` | `jump: {user: deploy, host: bastion.example.com, port: 2222}` |
| `needs: [postgres]` | `needs: [{name: postgres}]` |
| `services: {postgres: 17}` | `services: {postgres: {version: 17}}` |
| `envFiles: [.env]` | `envFiles: [{file: .env}]` |
| `hooks: {PostDeploy: "echo hi"}` | `hooks: {PostDeploy: {run: "echo hi"}}` |
| `build: .` | `build: {context: .}` |

## Environment values: two rules

**Which list a workload resolves.** Exactly one, from the most specific
declaration present: override, then workload, then environment, then project.
Lists replace; they do not extend.

**What wins inside the container.** Lowest first: a `compose:` workload's own
`env_file`; the resolved `envFiles` entries in order; managed-service connection
files; the service's `environment`.

Full explanation, including why level four outranks the rest:
[Handle secrets](/docs/guides/secrets).

## What Onebox generates

**Names**: workload containers are uniformly numbered: `shop-web-1` and
`shop-web-2`. Containers Onebox runs from its own images are unnumbered:
`onebox-postgres` for a managed service and `onebox-proxy` for the proxy. Persistent and provider names include `onebox_postgres`,
`onebox_postgres_data`, `shop_default` (the external application network),
`onebox_services` (the external service network), and `onebox-ingress`. These are contract:
once a persistent resource exists its name cannot change without migration. A
foreign resource already holding a derived name is refused, not adopted.

**Layout** (`/var/lib/onebox/app/releases/<id>`, plus `current`, `journal`, and
`services`. `basePath` moves this per environment. Host state) the owner
record, host lock and proxy: stays in `/var/lib/onebox/_host` whatever
`basePath` says, so that one host has one owner.

### Bind-mount lifetimes

A workload bind source has one of two lifetimes:

```yaml
workloads:
  web:
    image: ghcr.io/acme/shop:1.4.0
    volumes:
      # Versioned configuration shipped inside this release.
      - {source: ./config, path: /etc/shop, mode: ro}
      # Writable state managed outside the release store.
      - {source: /srv/shop/uploads, path: /var/lib/shop/uploads}
```

A relative source begins with `./` (or is `.`) and resolves inside the release
directory. It must be read-only because that directory belongs to the
release: the next deploy ships a new one, and `deployment.retainReleases`
cleanup removes an expired release once no container still mounts it, so
anything written there is lost. Use it for versioned configuration, never
changing data.

An absolute source is external host state. Onebox mounts it but neither creates,
backs up nor removes it; provision and protect that path separately. For data
Onebox should own, prefer a managed named volume instead.

**The proxy**: if anything is routed, Onebox runs Traefik and writes its static
configuration. When Onebox owns that static configuration, HTTPS routes receive
managed response compression by default; Traefik skips bodies below its
threshold, responses already encoded, and `text/event-stream` so streaming is
not buffered. Declare `proxy.config` with dynamic YAML or TOML files to extend
that managed configuration. Include `traefik.yml` or `traefik.yaml` in the same
directory only when you need to own the static configuration too. The external
`proxy.network` may be changed, but `default` is reserved for the application's
own Compose network. The derived `<app>_default` and `onebox_services` names are
reserved too; routed projects must use a distinct ingress network.

Managed Traefik runs as a non-root user and never receives the Docker socket.
An isolated Onebox discovery controller observes only this application's
container lifecycle, routing labels and Docker health. It runs as root only to
open the host's root-owned socket, but has no network or listener; it atomically
publishes a sanitized dynamic configuration that
Traefik mounts read-only. This preserves health-aware rolling and draining
without placing host control in the internet-facing process.

Deploy converges this managed proxy after its read-only preconditions and before
application mutation. Updating the Onebox runner therefore upgrades the default
proxy boundary automatically; an operator does not need to edit the project or
run a separate migration command.

`proxy.config` keeps this boundary and has two levels. With dynamic YAML or TOML
alone, Onebox continues to write the static configuration and stages those files
into its watched directory. This is the normal way to add middleware, TLS
options, transports, or other routing policy without duplicating the managed
entrypoints, health check, provider, and ACME configuration.

If the directory includes `traefik.yml` or `traefik.yaml`, that file becomes the
project-owned static configuration. It must not enable `providers.docker` and
must configure `providers.file.directory: /etc/traefik/dynamic`. Watching must
remain enabled, and `providers.file.filename` cannot be combined with that
directory. A proxy `.env` is accepted with project-owned static configuration
or with `proxy.dnsChallenge`; it may carry ordinary DNS-provider credentials,
but not `TRAEFIK_*` static overrides.

In both modes, the filenames `onebox.yml`, `onebox.yaml`, and
`onebox-managed.yml`, the `dynamic/` mountpoint, the `onebox-compress`
middleware, and HTTP or TCP router or service names beginning with the derived
`<app>_` prefix are reserved for generated output. `ob deploy`, `ob bootstrap`,
and `ob proxy apply` refuse an incompatible configuration with migration
guidance, preventing a proxy from starting with missing or stale routes.

### Wildcard host routes

Use a wildcard `hostname` when one HTTP workload should receive every immediate
subdomain below a suffix. The complete left-most label is `*`, matching the
convention used by TLS certificates and common ingress APIs:

```yaml
proxy:
  config: traefik
  dnsChallenge:
    provider: cloudflare
    resolvers: ["1.1.1.1:53"]

workloads:
  preview:
    image: ghcr.io/acme/preview:1.0.0
    routes:
      - {hostname: "*.preview.example.com", port: 3000}
```

This matches `branch.preview.example.com`, but not the suffix itself or
`a.branch.preview.example.com`. Declare the apex as a separate exact `hostname`
route when it should be served too. Onebox accepts only exact hostnames or a
complete left-most wildcard label; it does not accept authored regular
expressions, partial wildcards, or a bare HTTP catch-all. It renders the
wildcard hostname as the anchored Traefik rule
``HostRegexp(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.preview\.example\.com$`)``.
Exact and wildcard claims
that overlap on the same entrypoint,
protocol, and path are refused instead of relying on implicit proxy priority.
The existing `hostname: "*"` form remains available only for a plaintext or TLS
passthrough TCP route (`protocol: tcp` with `tls: none` or `tls: passthrough`),
where Traefik requires ``HostSNI(`*`)``;
it overlaps every TCP host claim on the same entrypoint and path.

Terminating TLS for a wildcard requires ACME DNS-01. `proxy.dnsChallenge`
keeps the static Traefik configuration managed by Onebox while selecting the
DNS provider. Put the provider's required environment variables in
`proxy.config/.env`; the directory may contain only that file. Onebox stages it
with mode `0600` and mounts it only into Traefik. Do not commit provider tokens.
Exact routes continue to use the separate `letsencrypt` HTTP-01 resolver, so
adding a wildcard does not change issuance or renewal for existing domains.
The provider names and credential variables are defined by
[Traefik's DNS challenge provider documentation](https://doc.traefik.io/traefik/reference/install-configuration/tls/certificate-resolvers/acme/#providers).
If `proxy.config` contains a project-owned
`traefik.yml` or `traefik.yaml` instead, configure DNS-01 under
`certificatesResolvers.onebox-wildcard` there and omit
`proxy.dnsChallenge`.

### Additional proxy entrypoints

Declare an entrypoint when clients must reach the managed proxy on a port other
than HTTP `80` or HTTPS `443`. Name the same entrypoint on the route that should
receive that traffic:

```yaml
proxy:
  entrypoints:
    otlp-grpc: {port: 4317}
    otlp-http: {port: 4318}

workloads:
  telemetry:
    image: ghcr.io/acme/telemetry-gateway:1.0.0
    routes:
      - hostname: telemetry.example.com
        entrypoint: otlp-grpc
        port: 4317
        scheme: h2c
      - hostname: telemetry.example.com
        entrypoint: otlp-http
        port: 4318
```

Each declared port is published on the host by the managed proxy. Use this for
remote clients; workloads on the same host can communicate over Onebox's
internal networks without opening another host port. The route's `port` is the
workload's listening port, while `entrypoint` selects the proxy listener.

When Onebox writes the static proxy configuration, including when
`proxy.config` contains only dynamic extensions, it also writes these named
entrypoints. If `proxy.config` supplies your own `traefik.yml` or
`traefik.yaml`, Onebox still publishes the ports, but that configuration must
define matching entrypoint names and addresses.

### Route middleware

Managed HTTPS routes already use Onebox's streaming-safe response compression.
No route declaration or `proxy.config` is needed for that default.

Attach dynamic proxy middleware to the exact route that needs it with an
ordered, provider-qualified reference:

```yaml
proxy: {config: traefik}

workloads:
  web:
    image: ghcr.io/acme/shop:1.4.0
    routes:
      - hostname: shop.example.com
        path: /admin
        port: 8080
        middlewares:
          - admin-auth@file
          - secure-headers@file
```

The definitions belong to the provider named by the suffix. For example,
`admin-auth@file` names `http.middlewares.admin-auth` in a dynamic Traefik file
under `proxy.config`. Onebox supplies the watched file provider when the
directory contains only dynamic extensions; the project must supply it when the
directory also contains custom static configuration. A Onebox-managed proxy
therefore requires `proxy.config` when a route names middleware. With
`proxy.managed: false`, the operator-owned proxy provides the referenced
resources instead. Onebox preserves list order when it attaches the chain to
the generated router. HTTP and TCP routes both support middleware references;
the referenced middleware must match the route's protocol.

> **Compose is an output, not an input**
>
> A Compose file you wrote cannot be the contract: the declaration is, and Onebox
> generates the runtime from it. Individual services can still be adopted with
> `compose: docker-compose.yml#service`, and Onebox generates around them. See
> [Why Compose is generated](/docs/concepts/generated-compose) and
> [Adopt an existing Compose file](/docs/guides/compose).

The JSON Schema published by `ob schema` is generated from the same declarations
the loader enforces and is checked against the conformance corpus.