Skip to content

Workbench configuration

A Monoceros workbench is defined by two files side by side: a YAML config and an .env. The YAML describes everything about the container - languages, services, AI tools, repos - and is safe to share. The .env holds the secrets the YAML refers to, and stays private. This page is the full reference: every top-level field, plus how the two files work together - enough to write one by hand.

New to Monoceros? Start with Installation.

Everything lives under your Monoceros home, ~/.monoceros:

  • Directory~/.monoceros
    • monoceros-config.yml
    • monoceros-config.env
    • Directorycontainer-configs
      • acme.yml
      • acme.env
    • Directorycontainer
      • acme

At the top are two optional, machine-wide files: monoceros-config.yml holds global defaults, and monoceros-config.env is a shared secret and token pool every container can draw on.

container-configs/ holds one pair of files per container. acme.yml is the source of truth - it describes the container and is safe to share. acme.env holds the secrets behind that yml’s ${VAR} references, so you keep it private.

container/ holds the materialized containers. container/acme/ is generated from acme.yml on every monoceros apply (devcontainer.json, the compose file, home/, projects/, data/) - you edit the yml, never this directory.

schemaVersion: 1
name: acme
runtimeVersion: 1.8.0
languages:
- node:22
- java:
version: 21
installMaven: true
installGradle: true
aptPackages: [jq, make]
services:
- name: postgres
image: postgres:18
port: 5432
env:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- data:/var/lib/postgresql
restart: unless-stopped
healthcheck:
test:
['CMD', 'pg_isready', '-U', '${POSTGRES_USER}', '-d', '${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
features:
- ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1
options:
permissionMode: auto
apiKey: ${CLAUDE_CODE_API_KEY}
mcpServers:
- name: context7
options:
apiKey: ${CONTEXT7_API_KEY}
repos:
- url: https://github.com/acme/app.git
path: app
git:
user:
name: ${GIT_USER_NAME}
email: ${GIT_USER_EMAIL}
routing:
ports: [3000]

Quick index of the top-level fields:

FieldWhat it is
schemaVersionConfig format version (1 today).
nameThe container name (matches the file name).
runtimeVersionPinned base-image version - reproducible.
languagesToolchains installed in the container.
aptPackagesExtra Debian/Ubuntu packages.
servicesBacking containers (DBs, caches, …).
featuresDevcontainer features - AI tools, CLIs.
reposGit repos cloned into projects/.
gitCommitter identity.
routingContainer ports exposed to the host.
installUrlshttps:// scripts run on build.

The config format version. Always 1 today; required. It only changes if the yml shape ever changes in a breaking way.

schemaVersion: 1

The container’s name. It must match the file stem - acme.yml carries name: acme - and is what you pass to every command (monoceros apply acme, monoceros shell acme), the host route (acme.localhost), and the materialized container/acme/ directory. Letters, digits, ., _, -.

name: acme

The pinned base-image version (exact major.minor.patch). monoceros init writes it and every monoceros apply reuses it verbatim - it is never auto-bumped, only monoceros upgrade moves it. This is what makes a container reproducible across machines and across time.

runtimeVersion: 1.8.0

See Reproducibility and upgrades.

The toolchains installed into the container. Each entry is either a bare string (node, java:17) or the object form when the language carries options Monoceros surfaces (Java’s installMaven / installGradle). The version is shown inline.

languages:
- node:22
- java:
version: 21
installMaven: true
installGradle: true

Full details, the curated set and per-language options: Languages.

Extra Debian/Ubuntu packages installed at build time, on top of what the base image and language features bring. A flat list of package names.

aptPackages: [jq, make, openssh-client]

Backing containers that run alongside the workspace - databases, caches, queues, object stores. Each entry is an object; only name and image are required. The service is reachable from inside the container by its name as hostname (e.g. postgres:5432).

services:
- name: postgres
image: postgres:18
port: 5432
env:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- data:/var/lib/postgresql
restart: unless-stopped
healthcheck:
test: ['CMD', 'pg_isready', '-U', '${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5

Only name and image are required; the rest are optional:

  • port - the in-container port, used by monoceros tunnel; not a host mapping.
  • httpPort - the one HTTP port that may leave the container. The proxy routes acme-<service>.localhost to it and monoceros share offers it to other devices. Curated web services bring it along (Keycloak 8080, the Mailpit inbox 8025, the RustFS console 9001, Caddy 81); databases have none. Delete the line to keep a service to yourself. The workspace also gets that address as <SERVICE>_PUBLIC_URL, so an agent writing a redirect URI into a realm does not have to assemble it from the workbench name.
  • volumes - data: is the service’s own Docker volume, monoceros-acme-data-<service>; it survives an apply. A relative host path mounts a file or directory from the workbench (a realm export, a theme). Nothing else is supported.
  • restart, healthcheck, command - standard compose semantics.

monoceros add-service acme <name> / --with-services expands a curated name into a full block; any other image works too. Full reference: Services.

Devcontainer features - AI coding tools, CLIs, SDKs - dropped into the workspace. Each entry is a ref plus its options.

features:
- ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1
options:
permissionMode: auto
apiKey: ${CLAUDE_CODE_API_KEY}

Credential options like apiKey are ${VAR} references whose values live in your env files (see Secrets below). monoceros add-feature acme <ref> accepts a full OCI ref or a catalog short name (claude, github, atlassian/twg) and fills in the block. Full reference: Features.

A feature whose agent reads plugins takes a plugins: block beside its options:, and the container comes up with those plugins installed:

features:
- ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1
options:
permissionMode: auto
plugins:
- url: https://github.com/acme/claude-plugins.git
enable:
- acme-conventions

url is the full HTTPS address of the repository holding the marketplace, the same form repos: uses, with provider: for a host that is not github.com, gitlab.com or bitbucket.org. For a marketplace in your workspace, use path: projects/<folder> instead. enable names the plugins to install and is required, because one marketplace often holds several.

Today only claude hosts plugins. The block on any other feature is an error rather than something that quietly does nothing. Details: Claude Code.

MCP servers the AI agents in the container can reach. A curated connector is its name plus whatever options it has, which for most of the catalog is nothing at all; anything else carries its own definition, in the shape its provider publishes.

mcpServers:
- name: context7
options:
apiKey: ${CONTEXT7_API_KEY}
- name: linear
- name: acme-crm
transport: stdio
command: npx
args: ['-y', '@acme/mcp-server']
env:
ACME_API_TOKEN: ${ACME_API_TOKEN}

What tells the two forms apart is not the name but whether the entry carries a definition. A bare name (with or without options) is looked up in the catalog; an entry with transport and the fields that go with it stands for itself and is never looked up, even if a connector of that name ships later.

transport is stdio for a server that runs inside the container, or http / sse for a remote endpoint. Apply writes every entry into the config of every agent in the container, each in its own format. Full reference: MCP servers.

Git repositories cloned into projects/<path>/ when the container starts. HTTPS URLs only.

repos:
- url: https://github.com/acme/app.git
path: app
provider: github
git:
user:
name: ${GIT_USER_NAME}
email: ${GIT_USER_EMAIL}

path overrides the default folder (the repo name). provider (github / gitlab / bitbucket) is only needed for non-canonical hosts, e.g. a self-managed GitLab - github.com / gitlab.com / bitbucket.org are auto-detected. git.user overrides the committer identity for that one repo. Full reference: Git and repositories.

The committer identity used inside the container, usually via ${VAR}. Falls back to the host’s git config --global at apply time when unset, and is overridden per repo by repos[].git.user.

git:
user:
name: ${GIT_USER_NAME}
email: ${GIT_USER_EMAIL}

Container ports exposed to the host through the shared Traefik proxy. ports is a list of in-container port numbers (3000 or the long form { port: 3000 }). Each becomes reachable as acme-<port>.localhost; the first entry is also the bare acme.localhost.

routing:
ports: [3000, 5173]
vscodeAutoForward: false

vscodeAutoForward (default false) lets VS Code also auto-forward ports on top of the proxy routes. The Traefik host port itself is machine-global (in monoceros-config.yml), not per container. Full reference: Proxy and tunnels.

https:// scripts piped to sh on every container build. A flat list. Use sparingly - this is remote code execution by design, and monoceros warns loudly before persisting one. Prefer a feature or aptPackages where possible.

installUrls:
- https://example.com/install.sh

The YAML only ever holds ${VAR} references. The real values - API keys, tokens, passwords - live in env files beside the config: a per-container acme.env, and an optional global monoceros-config.env shared across every container.

A reference in the YAML:

acme.yml
features:
- ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1
options:
apiKey: ${CLAUDE_CODE_API_KEY}

…and the value next to it:

acme.env
CLAUDE_CODE_API_KEY=sk-ant-...

${VAR} values come from two env files, merged at apply:

  • acme.env - per-container, next to the yml. The container’s own private values.
  • monoceros-config.env - an optional global pool in your Monoceros home, shared across every container.

The per-container file is layered on top, so a value in acme.env always wins over the global pool. An acme.env value can even reference the pool, so a container points at a shared secret without copying it:

acme.env
GITHUB_API_TOKEN=${GIT_TOKEN__GITHUB_ACME}

Git and repositories builds on this for repo access tokens. Keep monoceros-config.env private - unlike acme.env it sits in your home, outside the auto-gitignored container-configs/.

You rarely create acme.env by hand. init, add-service and add-feature seed the keys their ${VAR} references need:

  • Credentials (a feature apiKey, tokens, …) are seeded blank - fill them in, or leave empty for interactive auth (an empty Claude key means OAuth login on first run instead of API-key mode).
  • Curated services are seeded with working dev-defaults (e.g. POSTGRES_PASSWORD=monoceros). These are not secrets and are not masked; the container works out of the box, and you change them only for a real one.

At monoceros apply, every ${VAR} is resolved against the merged env - the global monoceros-config.env with acme.env layered on top:

  • A missing/empty variable in a service field is a hard error - a silently empty DB password would fail far more opaquely later.
  • A missing/empty feature option falls back to that option’s default (or interactive auth), so an unset key is fine.

Edit the YAML by hand any time, or with the add-* / remove-* commands, then re-run monoceros apply. The build is deterministic: the same YAML yields the same container on any machine. Never edit container/acme/ directly - it is regenerated on every apply.