Install
What Meridian needs to run (the engine binary or container image, PostgreSQL 14 or later, and optional search and mail services) and how to check an installation.
Meridian is one binary. It needs a PostgreSQL database and nothing else to serve a site; a search service and a mail service are optional and add site search and real email.
What you need
| Component | Required | Notes |
|---|---|---|
The engine binary, engine-dmo or engine-cms | Yes | One self-contained executable. No runtime, interpreter or system library is needed for the pure Go build |
| PostgreSQL | Yes | Version 14 or later; 15 or later is recommended. The database must be created with UTF8 encoding |
| A search service | No | Meilisearch, Elasticsearch or OpenSearch. Without one, the search route serves browse mode only |
| A mail service | No | SMTP, Postmark, Amazon SES or Resend. Without one, messages are written to the log |
| Node.js | Only for a frontend build | The engine never needs Node at runtime. A project with its own CSS and JavaScript build uses it to run that build |
Nothing else runs beside the engine: there is no separate worker process, cache server or queue. Every engine-dmo serve process serves requests and runs the background jobs. serve accepts --role web|worker|all, but this version ignores it: a process given --role web still runs jobs, so you cannot split web and worker processes.
Getting the engine
What a release publishes today:
| Available | Not available |
|---|---|
Two Linux container images of engine-dmo (below), for linux/amd64 and, for the pure Go image, linux/arm64 | A standalone download of either binary |
An engine-cms image | |
| A macOS or Windows build | |
| The example projects: they are in the Meridian source repository, not in the binary or the images |
So on your own machine you have three ways to run the engine:
-
A binary you have been given. Put it on your
PATH. Everything in this handbook assumes this. -
The image, as a command. Every command works through the image with the project mounted as the working directory. A shell alias makes it read like a local binary:
alias engine-dmo='docker run --rm -i -v "$PWD":/srv/project -w /srv/project \ -e DATABASE_URL -e SIGNING_KEY ghcr.io/cherwell-studio/meridian:0.8.0' engine-dmo check --no-db To serve this way, add
-p 8080:8080 -e LISTEN_ADDR=0.0.0.0:8080to thedocker runforserveordev up. Inside the containerlocalhostis the container itself: pointDATABASE_URLathost.docker.internal(Docker Desktop) or run with--network host(Linux) to reach a database on your machine. -
The binary copied out of the image, on Linux only, since the image holds a Linux executable:
id=$(docker create ghcr.io/cherwell-studio/meridian:0.8.0) docker cp "$id":/app/engine-dmo ./engine-dmo docker rm "$id"
The binary
Put the binary for your bundle on your PATH and check it:
engine-dmo version meridian v0.8.0 (pure-go) go1.27.3
bundle dmo: modules ai directory forms crm affiliates memberships newsletter portal claims memberreport promotions paidlistings translation
...
image pipeline Go image, golang.org/x/image: encodes jpeg png; heic false, icc false
template contract 1 The lines that matter:
| Line | Tells you |
|---|---|
meridian v0.8.0 (pure-go) | The engine version and the build variant: pure-go, or vips for a build linked against libvips |
bundle dmo: modules ... | The bundle and every module compiled in |
image pipeline ... | The image formats this build can encode, and whether it reads HEIC and ICC colour profiles |
template contract 1 | The template contract version this engine implements (see Upgrading the engine) |
engine-dmo version --json prints the same as JSON. engine-dmo version --require webp,avif exits 1 unless the build has every capability named, which makes a quick smoke test after an install. The capabilities are webp, avif, heic, icc, pdf (a browser that prints a tagged PDF) and extension:<name> (a first-party extension that opens).
engine-cms and engine-dmo take the same commands. A project written for engine-cms also runs on engine-dmo, since every module of the CMS bundle is in the DMO bundle too.
The container images
Each release is published as two Linux container images of engine-dmo:
| Image | Platforms | Contents |
|---|---|---|
ghcr.io/cherwell-studio/meridian:<version> | linux/amd64, linux/arm64 | The pure Go build on a distroless static base. Encodes JPEG and PNG |
ghcr.io/cherwell-studio/meridian:<version>-vips | linux/amd64 | Linked against libvips: encodes JPEG, PNG, WebP and AVIF, reads HEIC and ICC profiles, prints the member report as a tagged PDF through a bundled headless Chromium, and carries the first-party extensions in /app/extensions/. The recommended image for production |
Tags: every release is tagged with its full version (0.8.0) and its minor version (0.8), each with the -vips suffix for the libvips image. latest points at the newest pure Go release that is not a release candidate; there is no latest-vips, so the floating libvips tag is the minor one (0.8-vips). On an arm64 host the -vips image does not run natively, so an arm64 server gets the pure Go image: JPEG and PNG only, no member report PDF and no extensions.
Both images run as a non-root user with no shell, have the binary at /app/engine-dmo as their entrypoint, run serve by default, work in /app, and expose port 8080. Mount your project into the container and name it with --project:
docker run --rm \
-v "$PWD":/srv/project:ro \
-e DATABASE_URL='postgres://meridian:secret@db:5432/meridian?sslmode=disable' \
-e LISTEN_ADDR=0.0.0.0:8080 \
ghcr.io/cherwell-studio/meridian:0.8.0-vips \
check --no-db --project /srv/project Run the image with a read-only root filesystem, a writable /tmp, and the project’s storage directory on a volume of its own. The container images and Single-node deployment cover production use.
engine-cms in a container
The published images contain engine-dmo. Because a CMS project runs on engine-dmo unchanged, you can run an engine-cms project in them.
PostgreSQL
Any PostgreSQL 14 or later server works, managed or self-hosted. The engine refuses to start against an older server and names the version it found, and engine-dmo migrate up refuses a database whose encoding is not UTF8.
For development, a container is the quickest route:
docker run -d --name meridian-db \
-e POSTGRES_USER=meridian -e POSTGRES_PASSWORD=meridian -e POSTGRES_DB=meridian \
-e POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=C.UTF-8" \
-p 5432:5432 postgres:16 The engine reads the connection string from database.url in config/engine.yaml, which a new project sets to ${DATABASE_URL}:
export DATABASE_URL='postgres://meridian:meridian@localhost:5432/meridian?sslmode=disable' The engine does not read .env files, although the starter’s .gitignore lists .env: that is for a file your shell, direnv, Docker Compose or your process manager loads. Export variables in your shell, or have your process manager or container runtime set them.
Sorting text in a locale’s own order uses the server’s ICU collations. On a server built without ICU the engine still runs and logs a warning; text then sorts in code point order. Run engine-dmo doctor against a database to see its version, encoding, collation and whether ICU is present.
Use one database per project. Projects with different schemas must not share a database.
Variables you must set, even in development
A ${VAR} without a default must be set for any command that loads the whole configuration, which includes check (without --no-db), config validate, seed verify, new template, dev route add --scaffold, describe template, render, migrate and serve. A placeholder is enough for the commands that never connect. check --no-db, describe project, describe type, template api, guide and the dev verbs other than --scaffold run without them.
The new project uses only ${DATABASE_URL}. When you add secrets for production, such as signing_key: ${SIGNING_KEY}, they must be set on your machine too, and a secret cannot take a default (${SIGNING_KEY:-} is refused). Keep development values in a file you load into your shell:
# .env, loaded with: set -a; . ./.env; set +a
DATABASE_URL='postgres://meridian:meridian@localhost:5432/meridian?sslmode=disable'
SIGNING_KEY='development-only-0123456789abcdef' # at least 32 characters, even here (meridian:allow-secret, a local placeholder) server.security.signing_key is checked for length whenever it is set, by check --no-db too. Left out of engine.yaml altogether, the engine derives a development key from the database URL and logs a warning; check refuses a missing key when engine.environment is production.
Search (optional)
Without a search backend, search.driver is none and a site’s search route serves browse mode. To search, run Meilisearch (or Elasticsearch or OpenSearch) and point engine.yaml at it:
search:
driver: meilisearch
meilisearch:
url: ${MEILISEARCH_URL:-http://localhost:7700}
api_key: ${MEILISEARCH_API_KEY} docker run -d --name meridian-search \
-e MEILI_MASTER_KEY=dev-key -e MEILI_ENV=development \
-p 7700:7700 getmeili/meilisearch:v1.15
export MEILISEARCH_API_KEY=dev-key api_key is a secret, so it must be a bare ${VAR} and the variable must be set, even to an empty value for a server with no key. Search covers the backends and their options.
Mail (optional)
mail.driver defaults to log: every message, including staff enrolment and password reset mail, is written to the engine’s log instead of being sent. That is enough for development. To catch real messages locally, run an SMTP catcher such as Mailpit and use the smtp driver:
mail:
driver: smtp
from: "Visit Wyndmouth <hello@wyndmouth.example>"
smtp:
host: ${SMTP_HOST:-localhost}
port: 1025 The drivers are log, smtp, postmark, ses and resend. Each provider’s keys are secrets and come from environment variables. engine-dmo mail status and engine-dmo mail test check a configuration against a running database.
Ports
| Port | Used for | Set with |
|---|---|---|
8080 | The public sites, the admin at /studio, the APIs | server.listen; a new project reads ${LISTEN_ADDR:-127.0.0.1:8080} |
9090 on 127.0.0.1 | Prometheus metrics at /_metrics | telemetry.metrics.bind |
6061 on 127.0.0.1 | engine-dmo docs serve | --listen |
engine-dmo serve --listen and engine-dmo dev up --listen override server.listen for one run.
Checking an installation
engine-dmo version prints the version, bundle and modules.
engine-dmo check --no-db --project <dir> validates a project’s configuration and templates with no database and no secrets set (a secret that is set is still checked, so a short SIGNING_KEY fails).
engine-dmo migrate status lists applied and pending migrations against the database DATABASE_URL names, and exits 2 if any are pending.
engine-dmo doctor writes a support bundle in one text file: the build, whether the configuration is valid, the database server and more, with every secret redacted.
Next: Your first project.