Meridian handbook
Getting started

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

ComponentRequiredNotes
The engine binary, engine-dmo or engine-cmsYesOne self-contained executable. No runtime, interpreter or system library is needed for the pure Go build
PostgreSQLYesVersion 14 or later; 15 or later is recommended. The database must be created with UTF8 encoding
A search serviceNoMeilisearch, Elasticsearch or OpenSearch. Without one, the search route serves browse mode only
A mail serviceNoSMTP, Postmark, Amazon SES or Resend. Without one, messages are written to the log
Node.jsOnly for a frontend buildThe 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:

AvailableNot available
Two Linux container images of engine-dmo (below), for linux/amd64 and, for the pure Go image, linux/arm64A 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:8080 to the docker run for serve or dev up. Inside the container localhost is the container itself: point DATABASE_URL at host.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:

LineTells 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 1The 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:

ImagePlatformsContents
ghcr.io/cherwell-studio/meridian:<version>linux/amd64, linux/arm64The pure Go build on a distroless static base. Encodes JPEG and PNG
ghcr.io/cherwell-studio/meridian:<version>-vipslinux/amd64Linked 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

PortUsed forSet with
8080The public sites, the admin at /studio, the APIsserver.listen; a new project reads ${LISTEN_ADDR:-127.0.0.1:8080}
9090 on 127.0.0.1Prometheus metrics at /_metricstelemetry.metrics.bind
6061 on 127.0.0.1engine-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.