Skip to the content.

← Back to README · Documentation index

Configuration

Configuration (.orobox.yaml)

Orobox uses a configuration file called .orobox.yaml in the root of your bundle or project. If the file does not exist, the init command will guide you through its interactive creation.

This page is the reference for every key. For a task-oriented walkthrough of adding packages, PHP extensions, PHP settings, ports and services — with worked examples and troubleshooting — see Customizing the environment.

Example .orobox.yaml file:

type: bundle
class: MyBundle
namespace: MyVendor\Bundle\MyBundle
oro_version: "6.1"
domains:
  - host: oro.demo
    root: public
    ssl: false
services:
  redis: true
  redisinsight: true
  mailpit: true
  rabbitmq: true
  elasticsearch: false
  kibana: false
  adminer: true
test:
  use_tmpfs: true
  tmpfs_size: 1g
  qa:
    eslint: true
    stylelint: false
commands:
  - name: "otr"
    command: "php bin/console oro:test:run"
    description: "Runs the Shippy Pro tests suite"
    service: "application"
image:
  dockerfile: docker/image/Dockerfile
  apk: [imagemagick]
  php_extensions: [redis]
php_ini:
  memory_limit: 4G
  xdebug.log_level: 0
ports:
  http: 8090
  db: 5434
composer:
  # Tokens for private repositories. Mirrors Composer's COMPOSER_AUTH schema and is
  # injected only into the containers that run composer (never committed or baked
  # into long-running services).
  auth:
    github-oauth:
      github.com: ghp_yourtokenhere
    http-basic:
      repo.packagist.com:
        username: token
        password: yourtokenhere
  # Forward the host SSH agent into the containers. Omit to auto-detect (any SSH-transport
  # URL in the repositories below, or in the project's own composer.json); true forwards
  # unconditionally, false never forwards.
  ssh_agent: true
  repositories:
    - type: vcs
      url: https://github.com/my-org/private-repo.git
    - type: composer
      url: https://repo.packagist.com/my-org/
    # SSH-format URLs are supported too; orobox forwards your host SSH agent into
    # the containers automatically (start ssh-agent and `ssh-add` your key first).
    - type: vcs
      url: git@github.com:my-org/ssh-repo.git

Installation types (type)

Orobox supports three installation types, selected via the type field (or the init prompt / --type flag):

Example project config:

type: project
oro_version: "6.1"
domains:
  - host: oro.demo
    root: public
    ssl: false

Example demo config:

type: demo
oro_version: "6.1"
domains:
  - host: demo.local
    root: public
    ssl: false

Environment files per install type

Overriding the generated env files

A .env (or .env.test) in the directory that holds the config file (.orobox.yaml, or the file given with --config) is merged key by key over the file Orobox generates; it no longer replaces it. List only the keys you change or add:

ORO_MAILER_DSN=smtp://mail:1025
MY_API_KEY=xyz

The merged file is read two ways: Symfony Dotenv reads it in order, resolving ${...} against what it has read so far, while compose resolves the env_file against the project environment, which is this same file read whole. In both, the last assignment of a key wins. The merged file is laid out so that both readings give the values you meant:

In your file:

A project that already keeps a complete copy of .env keeps working: every key it defines still overrides the generated one. The difference is that keys it does not define, for example ones added by a later Orobox release, now come from the generated file instead of being missing.

For project and demo installs the merged file is what the first orobox init copies to .env-app.local / .env-app.test, as described above. After that, the application and php-fpm-app containers read your checkout’s own .env-app.local: a later change to .env reaches only the services that load Orobox’s .env directly (ws, consumer, cron, and the databases’ credentials). Edit .env-app.local for the application itself.

Image customization (image)

Orobox runs a published image, so what it ships is the same for everyone. A project that depends on something more — a system library, pdftotext, a PECL extension, a CLI tool it shells out to — adds it under image:. Most needs fit four declarative keys and no Dockerfile at all:

image:
  apk: [imagemagick, poppler-utils, ghostscript]   # apk add --no-cache
  php_extensions: [redis, imagick, xsl]            # install-php-extensions
  npm: ["@playwright/test"]                        # npm install -g
  run:                                             # extra RUN lines, in order
    - curl -sSL https://example.com/tool -o /usr/local/bin/tool && chmod +x /usr/local/bin/tool

Orobox renders these keys into a layer on top of the published image, one RUN per key in a fixed order (apk, php_extensions, npm, run; each run entry is its own RUN, and php_extensions is preceded by a RUN that fetches install-php-extensions only when the base image lacks it), so a change to one key reuses the cached layers before it. Empty keys are skipped. Orobox does not check that a package exists — the build reports that with its own log.

Validation:

The layer is built locally, tagged orobox-custom/<project>:<oro_version>-<type>, and exists only on your machine. See Custom Dockerfile below for what it costs and when it rebuilds; the same rules apply whichever keys you use.

Custom Dockerfile (image.dockerfile)

For anything the declarative keys cannot express — multi-stage builds, COPYing files in — point image.dockerfile at a Dockerfile of your own (orobox extend image creates one with the required header and sets the key for you, see extend):

image:
  dockerfile: docker/image/Dockerfile

Deprecated: the top-level dockerfile key is the old spelling of image.dockerfile. It still works and is read as image.dockerfile, but every command prints a warning asking you to move it. Setting both is an error. orobox deploy-init rewrites the file with the new key.

The path is relative to the directory holding .orobox.yaml. The file belongs to your repository: commit it, review it, change it like any other source file.

Its final stage must build on the image Orobox publishes, which arrives in the OROBOX_BASE_IMAGE build argument:

ARG OROBOX_BASE_IMAGE
FROM ${OROBOX_BASE_IMAGE}

RUN apk add --no-cache imagemagick poppler-utils
RUN install-php-extensions redis

The published image already ships install-php-extensions (at a pinned release), so adding a PHP extension is a single RUN line as above — no need to download the tool yourself.

ARG before FROM is required by Docker itself — that is the only way a FROM can read a build argument. Orobox refuses a Dockerfile whose last FROM names anything else, because a hardcoded tag would make oro_version decide nothing and break the stack somewhere far from this config key. Bumping oro_version in .orobox.yaml therefore moves your layer with it, and the Dockerfile never needs editing for an Oro upgrade.

Earlier stages are unconstrained, so compiling something and copying the result over works as usual:

ARG OROBOX_BASE_IMAGE

FROM golang:1.24-alpine AS tool
RUN go install example.com/some/tool@latest

FROM ${OROBOX_BASE_IMAGE}
COPY --from=tool /go/bin/tool /usr/local/bin/tool

How it works, and what it costs:

Build the image, not the code. Your sources are bind-mounted into the container at run time, so COPYing them into the image gains nothing and makes every edit a rebuild. Use this for tools and libraries.

PHP settings (php_ini)

To change a PHP setting — a memory limit, a timezone, Xdebug options — for every PHP service, set php_ini to a map of directives. No image build is involved:

php_ini:
  memory_limit: 4G
  max_execution_time: 0
  xdebug.log_level: 0

Orobox renders the map into zz-project.ini in its internal directory and bind-mounts it, read-only, at /usr/local/etc/php/conf.d/zz-project.ini in every PHP service (application, php-fpm-app, ws, consumer, cron) and in the install service of the setup run, so orobox init installs Oro with the same settings the stack runs with. The zz- prefix makes PHP read it after the image’s own oro-custom.ini, so your values win.

A change needs the containers recreated, not rebuilt. The bind-mount path never changes, which Docker Compose does not treat as a reason to recreate anything, so Orobox also stamps every PHP service with a dev.orobox.php-ini-hash label holding a short hash of the mounted ini. When the ini changes, the label changes, and the next orobox up recreates the containers. In the file form the hash covers the content of your own file, so editing it is picked up the same way.

Rendering rules:

If you already keep an ini file in the repository, give its path instead of a map. It is mounted as-is in place of the generated one:

php_ini: docker/php.ini

The path follows the same rules as image.dockerfile: relative to the directory holding .orobox.yaml, inside the project, and free of :, ", \, $ and control characters (they would break the volume definition). It must also exist and be a file: a missing file is a configuration error reported when the config loads (and by orobox init), rather than a mount Docker would fill with an empty directory. orobox down and orobox clear are the exception — they only stop the environment, so they print a warning and go on.

php_ini stays outside image: because it is not part of the image.

Host ports (ports)

Every port the stack publishes on your machine can be moved with a ports map, for the day 8080 or 5432 is already taken by something else:

ports:
  http: 8090
  https: 8453
  db: 5434
  redis: 6380

Keys you leave out keep their default. A value of 0 means “do not publish this port on the host” (except for http and https, see below): the service stays reachable by the other containers on the compose network, it just has no host port. A key that is not in the table, or a number outside 0-65535, is a configuration error, so a typo does not silently keep the default.

Key Service Default Container port
http web 8080 80
https web (only with an SSL domain) 8443 443
db db 5432 5432
db_test db-test (orobox test-init) 5433 5432
redis redis 6379 6379
redisinsight redisinsight 8001 5540
mail_ui mail (Mailpit web UI) 8025 8025
mail_smtp mail (Mailpit SMTP) 2025 1025
rabbitmq rabbitmq (AMQP) 5672 5672
rabbitmq_ui rabbitmq (management UI) 15672 15672
elasticsearch elasticsearch 9200 9200
kibana kibana 5601 5601
adminer adminer 8081 8080
gotenberg gotenberg 3000 3000

Only the host side moves. Containers keep talking to each other on the container ports, so ORO_DB_PORT, the Redis and RabbitMQ DSNs and the like need no change. orobox up prints the URLs and the database port of the services that are enabled using these values, and leaves out a service whose port is 0.

http and https take precedence over the deprecated nginx_http_port / nginx_https_port keys of .orobox.yaml and the ORO_NGINX_HTTP_PORT / ORO_NGINX_HTTPS_PORT environment variables, which keep working when ports does not set them (the keys must be between 1 and 65535). Between those two, the environment variable wins when both are set, as an ORO_* variable does for any key. A port left empty (db: ~) is a configuration error: write a number, or 0 to not publish it. http and https are the exception to the 0 rule: they must be between 1 and 65535, because the web port is the application’s entry point (the application URLs and the websocket frontend are built from it) and cannot be left unpublished.

Changing a port needs the containers recreated: run orobox up again.

Extending the stack (.orobox.compose.yaml)

To add a service of your own (an object store, a mock API), or to tweak one of Orobox’s, write a standard Docker Compose file next to .orobox.yaml. There is no configuration key: the files are found by name.

File Committed Purpose
.orobox.compose.yaml yes The team’s additions to the stack
.orobox.compose.local.yaml no (git-ignored) One developer’s tweaks: ports, personal mounts

Both are optional and are appended in that order after Orobox’s own compose files, so the local file wins over the team file, and both win over the generated stack. Docker Compose’s usual merge rules apply, so there is nothing new to learn:

# .orobox.compose.yaml
services:
  minio:
    image: minio/minio
    command: server /data --console-address :9001
    ports: ["9001:9001"]
    volumes: [minio_data:/data]

  consumer:
    environment:
      ORO_MQ_PREFETCH: "5"

  php-fpm-app:
    volumes:
      - ./docker/fixtures:/var/www/oro/fixtures:ro

volumes:
  minio_data:

orobox extend compose writes the team file with commented examples, and orobox extend compose --local writes the personal one and adds it to your .gitignore when the project has one (see extend). orobox create also adds .orobox.compose.local.yaml to the .gitignore of a standalone bundle it generates.

Relative paths. Write paths relative to the file, as in any compose project. Orobox runs compose from its own internal directory (~/.config/orobox/<project>/ on Linux, the platform’s user config directory elsewhere, .orobox/ in CI or with OROBOX_LOCAL_CONFIG set), so it rewrites the relative host paths of a copy of your file (./docker/fixtures becomes an absolute path under the directory that holds .orobox.yaml) and passes that copy to compose as compose.project.resolved.yaml / compose.local.resolved.yaml. Your file is never modified. The rewritten keys are:

Key Forms
services.*.volumes short syntax SRC:DST[:MODE] when SRC starts with . or ~; long syntax type: bind with its source
services.*.build the string form, or build.context (build.dockerfile is relative to the context and left alone; remote contexts such as https://…, git@… or github.com/… are left alone). A build: mapping without context gets the project directory: compose’s default . would be Orobox’s internal directory
services.*.build.additional_contexts the mapping form and the name=path list form; docker-image://, service:, oci-layout://, target: and URL values are left alone
services.*.build.ssh the path of id=path entries (a bare default names the agent and has no path)
services.*.develop.watch each entry’s path
services.*.env_file a string, a list of strings, or a list of {path: …}
services.*.extends.file string
services.*.label_file string or list
configs.*.file, secrets.*.file string
include a string entry, or the path / env_file / project_directory of the long form

Values reached through YAML anchors and << merge keys are rewritten too, so a x-mounts: &mounts [...] fragment shared by several services works as written. A leading ~ expands to your home directory, as compose does (~bob/x included). Absolute paths, values starting with $ (interpolated by compose itself) and short-syntax sources without a path — named volumes — are left as they are. A $ in the project directory’s own path is escaped ($$) so compose does not interpolate it, and when that path contains a : short-syntax volumes are written in long syntax. A file may hold several YAML documents (separated by ---); a file that is empty or holds only comments counts as absent.

Services the stack does not define. Compose rejects the whole project over a service that has neither an image nor a build context. An entry that only tweaks a service the generated stack does not define for the command at hand — db-test, which exists only for orobox test and test-init, or an optional service the project disabled — is therefore left out of the copy compose reads for that command, and kept where the stack defines it. A service that the stack never defines (a disabled service, a typo) produces a warning; to add a new service, give it an image or a build. The personal file counts the team file’s services as part of the stack, so it can tweak a service the team file (or a recipe) adds. An entry that reaches services: only through a YAML merge key (<<: *extra) cannot be left out of the copy, so if it would have to be, the command stops and names it.

Build contexts across files. A build: mapping without context gets the project directory (compose’s default . would be Orobox’s internal directory) — unless an earlier document of the same file, or the team file for the personal one, already gives that service a build: compose merges build mappings key by key, so a later build: {args: …} keeps the earlier context.

Merge caveats.

Project service URLs. Give a service the label dev.orobox.url and orobox up lists it under “Project services” after the built-in URLs. The value is printed as written (compose interpolation is not applied to it), and a service with profiles: is not listed, since orobox up does not start it:

services:
  minio:
    labels:
      dev.orobox.url: http://localhost:9001

Every service of the override can also be named in orobox logs, orobox shell and in the service of a custom command.

Errors and warnings. A file that is not valid YAML, or whose top level is not a mapping, stops every command that runs compose, naming the file; Orobox never runs the stack with the override silently dropped. A bind-mount source that does not exist on the host produces a warning naming the path and what Docker will do — in short syntax it creates an empty directory there, in long syntax it refuses to start the container — because either way the problem would show up far from its cause. Sources compose still has to interpolate (${VAR}) and long-syntax binds with bind.create_host_path: true are not checked. Each warning is printed once per command.

The deploy pipeline does not use these files. orobox deploy and the generated CI build and test the application image, not your development stack; services added here exist only in local orobox commands.

Configuration Fields

Every command except init, create, extend, self-update, help, completion and version checks the configuration first and stops on an error, naming the file and the key: an unknown key, a value of the wrong type, a YAML syntax error, a path outside the project.

Note: Versions of PHP, PostgreSQL, Node.js, and other components are automatically determined by the oro_version setting and cannot be changed manually.

Global Flags

These options can be used with any command:

Agent mode

--agent reduces a command’s output to what an automated caller — an LLM agent, a script, a CI job that parses results — actually needs. Everything Orobox says about itself is dropped: banners, spinners, progress lines, per-tool headers and success confirmations.

What is left:

A command that succeeds with nothing to report prints nothing at all:

orobox qa --agent

On a clean tree that writes zero bytes and exits 0.

Details worth knowing: