Marea is an extensible CLI for the lifecycle of Elixir applications —
building, packaging, running, deploying and operating them. It is
plugin-based end to end: every command, every config field, every
workflow comes from a Malla
plugin, so the same binary handles local-dev, container builds,
Helm + Kubernetes deploys, AWS plumbing or whatever you wire in.
Marea is also a thin, transparent wrapper around the tools it drives —
mix, docker, helm, kubectl, aws — never a replacement.
Every shell command is printed on screen before it runs, so what
Marea does is always visible: no hidden orchestrator, no cluster-side
controller, no daemon, no lock-in.
For the current set of bundled plugins (and what each one adds), see
What's in the box in the Overview.
Adding your own — a different registry, a custom deploy target, an
extra build step, an organisation-specific notification hook — is
typically a few-dozen-line module, listed in plugins: next to the
bundled ones. See 06-plugin-development.md.
A typical Helm + Kubernetes flow
When you combine the Docker, Helm, K8s and Aws plugins, you
get the standard Elixir-on-Kubernetes pipeline:
flowchart LR
A[mix release] --> B[docker build]
B --> C[docker push]
A --> A1[_build/prod/...]
B --> B1[company-registry/app:vsn]
C --> C1[image stored in ECR]
C1 --> D[helm upgrade]
D --> E[pods rolled out]Marea wraps every step behind short, declarative commands so that the
underlying tools are invoked consistently across environments and
with the right flags. Once a project's marea.yaml is in place,
going from a code change to a rolled-out pod is typically a couple of
commands and a couple of minutes.
This is the most heavily exercised combination — the one running in
production today — but it is one configuration among many: drop
Helm and K8s and Marea is just a build/run CLI; swap Aws for a
custom registry plugin and the rest of the pipeline is untouched.
What Marea is not
- It is not a Kubernetes operator and does not run inside the cluster.
- It is not a replacement for
mix release,helm, oraws— it shells out to those tools, prints the command it is about to run, and benefits from anything you have configured in them. - It is not tied to AWS or to Helm. Both are opt-in plugins; either can be left out, swapped or extended.
- It is not limited to single-app Elixir projects (see Project shapes below).
Project shapes
Marea follows the umbrella pattern but does not force a particular
release layout. The same marea build / marea helm commands work
across the common shapes:
- Single BEAM with everything. One release that bundles every app in the umbrella. Simplest to operate; useful for small services or for "deploy" environments where you intentionally want a single node type.
- A few releases grouping related apps. Two or three releases
under the same umbrella, each starting a different subset of apps
(e.g.
api+worker+migrations). Marea treats each as a separate release inreleases:. - One release per application. Maximum isolation, where every
umbrella app is its own release and its own pod. The
releases:block scales linearly with the number of apps.
You can mix these in a single project, and you can also include Docker
releases (type: dockerfile_dir, contributed by the Docker plugin)
for non-Elixir companions — sidecars, migration jobs, auth proxies —
that ride alongside the Elixir apps in the same chart.
A particularly useful combination is "one release per application"
for the Kubernetes deploy, "single BEAM with everything" for local
development. In Kubernetes each release becomes its own
Deployment / ReplicaSet (or StatefulSet, where appropriate),
scaled and rolled independently, isolated in CPU/memory, and able to
fail without taking the rest of the system down. On your laptop you
don't want any of that ceremony: marea run local starts every app
in the umbrella inside a single BEAM node, so you get one process tree,
one IEx, one set of logs, and instant code reload across the whole
system. Same marea.yaml, same code — only the release: you
target changes between the two modes.
The convention payoff
Marea is at its most powerful when the project it ships follows Malla's conventions. Those conventions are not idiosyncratic — they are the industry-standard patterns for distributed Elixir on the BEAM: clustering, service discovery, request handling, status reporting, tracing, life-cycle management. Malla packages them as plugins that compose, and Marea is built on the same plumbing.
If your app is built on top of those conventions, Marea takes you
from marea setup init-umbrella to a complex distributed system
running on Kubernetes in minutes: a Helm chart is generated for
you, ConfigMaps and Secrets are populated from local files, the
release exposes a console entry point for kubectl exec debugging,
the AWS registry/DNS plumbing is wired in. The whole supporting
toolkit — build, package, deploy, debug, hot-fix — is included.
You can deviate, of course. Every template is overridable, every
plugin is replaceable, every command is interceptable, and there are
escape hatches (helm.chart_dir, type: dockerfile_dir) for the
parts of the system you'd rather hand-roll. But the further you stray
from the conventions, the more of the deploy stack you take ownership
of; the closer you stay, the more Marea does for you.
In production today. Marea, together with Malla, currently runs the deploy and operational workflow of a complex telemedicine project, with dozens of releases shipping every day across several Kubernetes environments. The patterns documented in these guides — multi-deploy values files, plugin-driven extensions, EEx templates, the debug/hot-fix flow — are the ones in daily use on real clusters.
Design principles
Transparent, never magical
Every shell command Marea is about to run is printed in bright text
before execution (CMD: docker build …). Destructive or
slow-to-roll-back operations — helm upgrade, helm delete,
helm rollback, Route53 changes — also pause for an enter / Ctrl-C
confirmation, unless you pass --nopause (e.g. in CI). If something
goes wrong, you can copy the printed command and run it directly to
debug.
LLM-ready, MCP-native
The transparency above is not only for humans. Because Marea's whole
surface is a set of short, standard subcommands and every underlying
invocation is printed with its output, an LLM coding agent can drive
Marea with no special integration at all: it learns the commands
from a guide or a --help page, reads the captured output to see
exactly what happened, and debugs from the real docker / helm /
kubectl error text — the same signals a human operator works from.
There is no bespoke DSL to teach it and no hidden framework state it
has to infer.
marea mcp serve takes that same command tree and exposes each leaf
subcommand as a typed Model Context
Protocol tool. The JSON Schema for
every tool is derived from the same Optimus metadata that defines the
CLI, so the tool surface tracks your plugins automatically — load a
plugin, get its tools. This is where driving Marea comes under
control: destructive operations (helm delete, helm rollback, AWS
deletes) carry destructiveHint: true and read-only ones
readOnlyHint: true, so an MCP client can allowlist the safe tools,
gate the dangerous ones behind confirmation, and pass typed arguments
instead of free-form shell strings. And because Marea runs outside the
agent's sandbox, the agent never has to break out of its own
restrictions to reach AWS, Kubernetes or Docker — it asks Marea, one
typed call at a time. See the MCP Plugin guide.
One config file per project
Everything Marea needs lives in marea.yaml and the marea.d/
directory. Marea looks for the config in this order, taking the first
match: marea.yaml, config/marea.yaml, marea.d/marea.yaml.
The schema is validated with Zoi and
plugins enrich it incrementally — the AWS plugin adds the aws: field,
your custom plugin can add its own fields.
Plugins all the way down
Marea is built on Malla. The Marea
binary is just a Malla service that loads a fixed set of base plugins
(Setup, Build,
Run, Mcp,
Base) plus whatever you
list under plugins: in marea.yaml. Every CLI subcommand is contributed by a plugin via the
Marea.Plugins.Base.marea_config_args/1 callback; every command
handler runs through the Marea.Plugins.Base.marea_cmd/2 callback
chain.
Container builds, Helm chart generation, AWS, Kubernetes operations,
and the docker/python build-image tweak are not base — they are
opt-in plugins shipped with Marea (Docker,
Helm, Aws,
K8s, docker.python).
Listing them
in your project's plugins: list is the same kind of operation as
adding your own MyOrg.Plugins.Sentry — both are entries in the same
list. plugin_deps: between plugins means listing Helm is enough
to pull in Docker (and Build) transitively, so the typical
project ends up with [Docker, Helm, Aws] or just [Helm, Aws].
Because every callback is a Malla chain, plugins are not limited to adding entirely new commands — they can also modify the existing ones. Listing your plugin before another one means it sees the same callback first and can:
- Match a command meant for another plugin
(e.g.
[:build, :docker | rest]), do something extra (run linters, push a Slack notification, mutate the config), and return:contso the original handler still runs — or short-circuit it entirely. - Inject new fields into another plugin's
marea.yamlschema viaMarea.Config.Schema.add_field_at_path/4, with no fork required. - Add an extra flag or option to an existing subcommand by enriching
the same
argsmap underMarea.Plugins.Base.marea_config_args/1. - Override the release-version computation (
Marea.Plugins.Base.marea_release_vsn/0) or the contents of generated release files (Marea.Plugins.Base.marea_init_release_file/1).
The result is that customising Marea is usually a small plugin, not a
fork: dozens of lines, one entry in plugins:, no patches to core.
EEx-driven templates
The two kinds of templates Marea ships — Dockerfile templates for
marea build docker and Kubernetes manifest templates for
marea helm chart — are both rendered through EEx, Elixir's standard
embedded-template engine. Inside a template you have plain Elixir at
expansion time: <%= @release %>, <%= for {k, v} <- @values do … end %>,
<%= String.upcase(name) %>, plus a small set of helpers in
Marea.Templates (to_dashes/1, indent/2, yaml!/1). There is no
bespoke templating language to learn: if you can read Elixir, you can
read and write Marea templates.
The values that drive the expansion come from your marea.yaml and
the files under <marea_dir>/configs/ and <marea_dir>/secrets/,
exposed to templates as @values, @config_files, @secret_files,
@name, @deploy, @namespace, etc.
For Kubernetes manifests, EEx coexists with Helm's own
{{ .Values… }} templating. The convention is to name these files
<name>.yaml.eex (so editors treat them as templated files instead
of flagging the EEx markers as YAML syntax errors) and to put the
EEx setup — imports, value lookups, helper calls — in a leading
<%!-- … --%> comment block at the top, followed by a plain <% … %>
setup block. EEx runs at marea helm chart time, driven by your
marea.yaml and local files; Helm runs later at helm upgrade time,
driven by values.yaml on the cluster. Both phases are useful, and
neither replaces the other.
Deferred shell execution
Some commands (notably marea k8s console, which drops you into an
IEx session inside a running pod via kubectl exec) need to replace
the current shell rather than run as a child of the escript. Marea
handles this by writing the final command to .marea/next_cmd. The
shell wrapper around the escript runs that file after the BEAM exits,
so the command takes over the parent shell.
In dev (mix marea …) the same mechanism is used, but the deferred
command is run via a Port for interactive stdio.
Last-values memory
The most recently used --deploy and --release (along others) are stored in
.marea/last_values and become the defaults for subsequent commands.
After your first marea build docker --deploy staging --release api,
you can run marea build helm --upgrade and Marea will reuse staging
and api.
Per-deploy directory layout
marea.d/ (or whatever you set marea_dir to) follows a fixed layout:
marea.d/
├── marea.yaml # main config (or one of the other paths)
├── configs/ # configmap source files (any format)
├── secrets/ # secret source files (yaml or raw)
├── deploys/<name>/ # generated values.yaml per deploy
├── charts/<name>/ # generated helm chart per deploy
├── helms/ # optional: pre-existing helm charts
└── templates/ # optional: per-project helm/dockerfile overridesThe Helm plugin builds charts/<deploy>/ from releases: definitions;
the build/run plugins read from templates/ and configs/.
Operate, debug, hot-fix
Deploying is half the job. Marea also helps you get into a running
release once it is on the cluster: attaching to a pod's IEx session
through kubectl exec, opening a kuttle-based tunnel for
network-level debugging, and pushing hot-fixes (mix release →
marea build helm --upgrade) without leaving the same marea.yaml
that built the running image. The operational commands use the same
shown-on-screen rule: every kubectl / helm invocation is printed
before it runs.
Where to go next
- New to Marea? Start with 02-installation.md and then 03-quick-start.md for a full walkthrough.
- Already up and running? The 04-marea-yaml.md reference and the 08-commands.md command tree are the two most useful pages once you know the basics.
- Want to extend Marea? Read 05-architecture.md and then 06-plugin-development.md.
- Operating a running deploy? Head to 07-operations.md — remote IEx console, hot-fixes, rollback.