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, or aws — 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 in releases:.
  • 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:

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 overrides

The 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 releasemarea 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