Skip to content

Make your own workbench template

By the third project you notice you are typing the same thing again. Your stack, your database, the agent, and the plugin your team keeps its conventions in. You know the command by heart, which is exactly the problem: it lives in your head, and everyone else on the team has their own version of it in theirs.

A template is that setup as a file. monoceros init can start from one instead of from nothing, and because a template is an ordinary workbench yml, making your own means copying a config that already works and changing two lines.

Four templates ship with the CLI. Two are a stack to edit down, web-app and go-service, and two carry a way of working and no stack, discovery-atlassian and agent-github. Start with whichever is closest; the init reference lists what each one holds.

web-app is a Node front end, a Java back end, a Postgres, a Keycloak for the logins, and Claude Code.

Terminal window
monoceros init acme --template=web-app

Init asks for the credentials those components need, writes the config and stops. Nothing is built yet. Press Tab after --template= to see which templates you have.

You are not stuck with what the template holds. The --with-* flags still work and are applied on top of it, so you take the ready-made setup and put this project’s own pieces next to it:

Terminal window
monoceros init acme --template=web-app --with-repos=https://gitlab.com/acme/api.git

Init then asks for the values it cannot know, one per question, with the component’s own explanation underneath. It skips a value you already keep in monoceros-config.env. Here, the Claude key and a GitLab token:

A template plus a repo, and the questions init asks (not a video, so you can select and copy any line)

Your answers go straight into acme.env, and the list at the end says where each value came from. Press Enter on one and the key stays empty in acme.env. If you keep that key in your global monoceros-config.env, the question says so, and Enter means that value is used. Without one, the Claude key stays empty, which is what you want when you sign in with a subscription. Nothing else is asked: Postgres and Keycloak bring working dev credentials, so there is nothing to invent there.

What you end up with is the template plus the repo, and the repo brought its own support along: Monoceros saw a GitLab URL and added the glab CLI, so pushing from inside the container works with the token you just typed. The two files, with the comments and the options left at their defaults dropped for brevity:

languages:
- node:22
- java:
version: latest
installMaven: true
installGradle: true
services:
- name: postgres
image: postgres:18
port: 5432
env:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
- name: keycloak
image: quay.io/keycloak/keycloak:26.7
port: 8080
command: start-dev --import-realm --proxy-headers=xforwarded
features:
- ref: ghcr.io/getmonoceros/monoceros-features/claude-code:1
options:
permissionMode: auto
apiKey: ${CLAUDE_CODE_API_KEY}
- ref: ghcr.io/getmonoceros/monoceros-features/gitlab-cli:1
options:
apiToken: ${GITLAB_API_TOKEN}
repos:
- url: https://gitlab.com/acme/api.git

A component the template already carries stays as the template set it. One that clashes with it, the same feature with different options, is the same error it would be on the command line, and init writes nothing at all in that case.

The Claude key is the same in every workbench you build, and so is your Atlassian token. Typing it again for each one is not the idea.

Put such a value in monoceros-config.env in your Monoceros home, and every workbench on the machine can draw on it:

~/.monoceros/monoceros-config.env
CLAUDE_CODE_API_KEY=sk-ant-api03-7Rk2mQ

Init still asks, because it cannot tell which of its questions you have already answered globally. Press Enter and the key is written empty into acme.env, which is exactly right: at apply the two files are merged, and an empty entry falls through to the global one. An empty key does not blank out what you have in the pool.

The other direction works too, and that is the reason the question is asked at all. Type a value and it lands in acme.env, where it wins over the global one for this workbench only. That is how one project gets the token of a different customer without touching anything else.

Secrets and the .env file has the whole model, including how an acme.env entry can point at a named value in the pool rather than copying it.

None of the shipped four is your setup, and they are not supposed to be. Yours has the services you actually run, the agent configured the way your team likes it, and the plugin marketplace your conventions live in. So build it once, as an ordinary workbench:

Terminal window
monoceros init proj-one --with-languages=go --with-services=postgres,mailpit --with-features=claude,github

That is Go, a Postgres, a Mailpit to catch the mail the service sends, and Claude Code with the GitHub CLI.

Then open proj-one.yml and put in what no flag can express, the plugin block on Claude Code being the usual one:

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

Apply it, work in it, and change whatever turns out to be wrong. Do that before you make a template out of it. A setup that has not survived one real project will be wrong in ways you only find out by using it, and a template makes those mistakes easier to repeat.

Copy the config into your template directory:

Terminal window
cp ~/.monoceros/container-configs/proj-one.yml ~/.monoceros/templates/workbenches/standard.yml

Then open it and change two lines. Where the workbench name is, put __MONOCEROS_NAME__, and where the runtime version is, put __MONOCEROS_RUNTIME_VERSION__:

schemaVersion: 1
name: __MONOCEROS_NAME__
runtimeVersion: __MONOCEROS_RUNTIME_VERSION__
languages:
- go:latest

That is the whole format. Everything else stays exactly as it was, comments included, and the rest of the file, the services with their credentials and the features with their options, comes along untouched.

Terminal window
monoceros init proj-two --template=standard --with-repos=https://github.com/acme/web.git

Tab completion lists standard next to the four shipped ones. A service brings its working dev credentials along rather than empty keys, so Postgres comes up the way it did in the project you built the template from:

Terminal window
GIT_USER_NAME=
GIT_USER_EMAIL=
CLAUDE_CODE_API_KEY=
GITHUB_API_TOKEN=
POSTGRES_USER=monoceros
POSTGRES_PASSWORD=monoceros
POSTGRES_DB=monoceros

Your own templates are searched before the ones that ship, so naming yours web-app replaces ours for you. That is deliberate: what we ship is where you start, and you are meant to outgrow it.

What a template carries that a command cannot

Section titled “What a template carries that a command cannot”

Look at the plugins: block on Claude Code above, or the opus and sonnet on the roles. Neither has a flag, and neither is getting one: a plugin is a nested block with a marketplace and an enable list, and those model names are options that live in the yml rather than in the env file. A setup that needs them can only be handed over as a file.

So whatever you can configure in a workbench yml, you can put in a template: a private plugin marketplace your team uses, a permission mode, the exact Postgres image you have standardised on, your ports.