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.
Where it lives
Section titled “Where it lives”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.
A complete example
Section titled “A complete example”schemaVersion: 1name: acmeruntimeVersion: 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]POSTGRES_USER=monocerosPOSTGRES_PASSWORD=monocerosPOSTGRES_DB=monoceros
CLAUDE_CODE_API_KEY=sk-ant-...CONTEXT7_API_KEY=ctx7sk-...
GIT_USER_NAME=Jane DoeGIT_USER_EMAIL=jane@example.comQuick index of the top-level fields:
| Field | What it is |
|---|---|
schemaVersion | Config format version (1 today). |
name | The container name (matches the file name). |
runtimeVersion | Pinned base-image version - reproducible. |
languages | Toolchains installed in the container. |
aptPackages | Extra Debian/Ubuntu packages. |
services | Backing containers (DBs, caches, …). |
features | Devcontainer features - AI tools, CLIs. |
repos | Git repos cloned into projects/. |
git | Committer identity. |
routing | Container ports exposed to the host. |
installUrls | https:// scripts run on build. |
schemaVersion
Section titled “schemaVersion”The config format version. Always 1 today; required. It only changes if the
yml shape ever changes in a breaking way.
schemaVersion: 1The 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: acmeruntimeVersion
Section titled “runtimeVersion”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.0See Reproducibility and upgrades.
languages
Section titled “languages”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: trueFull details, the curated set and per-language options: Languages.
aptPackages
Section titled “aptPackages”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]services
Section titled “services”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: 5POSTGRES_USER=monocerosPOSTGRES_PASSWORD=monocerosOnly name and image are required; the rest are optional:
port- the in-container port, used bymonoceros tunnel; not a host mapping.httpPort- the one HTTP port that may leave the container. The proxy routesacme-<service>.localhostto it andmonoceros shareoffers 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.
features
Section titled “features”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}CLAUDE_CODE_API_KEY=sk-ant-...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.
plugins
Section titled “plugins”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-conventionsurl 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.
mcpServers
Section titled “mcpServers”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}CONTEXT7_API_KEY=ctx7sk-...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}GIT_USER_NAME=Jane DoeGIT_USER_EMAIL=jane@example.compath 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}GIT_USER_NAME=Jane DoeGIT_USER_EMAIL=jane@example.comrouting
Section titled “routing”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: falsevscodeAutoForward (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.
installUrls
Section titled “installUrls”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.shSecrets and the .env file
Section titled “Secrets and the .env file”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:
features: - ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1 options: apiKey: ${CLAUDE_CODE_API_KEY}…and the value next to it:
CLAUDE_CODE_API_KEY=sk-ant-...Two env layers
Section titled “Two env layers”${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:
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/.
Seeding, dev-defaults and resolution
Section titled “Seeding, dev-defaults and resolution”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.
Editing & applying
Section titled “Editing & applying”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.