Skip to content

Project file

onebox.run/v1alpha1 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 or legacy aliases.

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

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

Use the published schema at https://onebox.run/schemas/application/v1alpha1/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.

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

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.

BlockWhat it saysFields
appThe application’s name. Every derived name carries it.Top level
environmentsWhere it runs, and the policy that governs deploying there.environments
workloadsThe containers that are yours.workloads
servicesThe supporting services Onebox runs for you.services
deploymentRelease order, retention, migration policy.deployment
proxyWho runs the proxy and what routes.proxy
runtimeEnvironment files and local environment-file checks.runtime
hooksCommands at lifecycle seams.hooks
checksWhat must be true for a release to activate, grouped by kind.checks
externalServicesDependencies operated outside Onebox, and how a workload reaches them.externalServices
backupTargetsOff-host repositories a protected service writes its backups to.backupTargets
registries notificationsNamed maps.registries · notifications

externalServices and backupTargets are published in the JSON Schema and accepted by ob validate, but the lifecycle behind them is not shipped: declaring a backup target creates no repository and no schedule, and an external service’s health probe never runs. See Shipped vs proposed.

Any mapping also accepts x- keys. They are carried nowhere and never change the generated runtime.

A scalar form accepted by a contract version remains accepted for that version. These are contract, not convenience that changes within a version.

Written asMeans
image: nginximage: {reference: nginx}
health: /healthzhealth: {http: /healthz}
server: [email protected]server: {user: root, host: 203.0.113.10}
jump: [email protected]:2222jump: {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: .}

Which list a workload resolves. Exactly one, from the most specific declaration present — override, then workload, then environment, then project. Lists replace rather than 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.

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.

A workload bind source has one of two lifetimes:

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.dns_challenge; 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 rather than silently starting a proxy with missing or stale 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:

proxy:
config: traefik
dns_challenge:
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.dns_challenge 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. If proxy.config contains a project-owned traefik.yml or traefik.yaml instead, configure DNS-01 under certificatesResolvers.onebox-wildcard there and omit proxy.dns_challenge.

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:

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.

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:

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.

apiVersion: onebox.run/v1alpha1 is the pre-baseline contract. Before the baseline is locked, normalization may be breaking and will invalidate config digests and any plans or staged artifacts bound to the old bytes. Regenerate those artifacts after changing the declaration. Within the locked baseline:

  • A field is added, never repurposed.
  • Only the documented resource shape is accepted; aliases and fallbacks are not part of the contract.
  • A default may be added; an existing default’s value does not change.
  • A constraint is not tightened against a project that already loads unless accepting it can silently lose data or produce an ambiguous runtime. A safety refusal names the migration required to make the project valid again.

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