Install
Meerkat is one Go binary with no CGO and no external dependency. Its embedded storage is a file in a directory; an external database is an option, not a prerequisite.
Docker image
Three images, built from the same commit and carrying the same name: the registry and the tag are what say the edition.
| Image | Edition |
|---|---|
docker.io/softwarity/meerkat:latest |
community, public |
docker.io/softwarity/meerkat:eval |
evaluation, public: everything Enterprise does, with an evaluation notice - not for production |
ghcr.io/softwarity/meerkat:latest |
Enterprise, private registry |
services:
meerkat:
image: docker.io/softwarity/meerkat:latest
ports:
- "8080:8080" # data plane
- "9090:9090" # control plane - keep this one internal
environment:
MEERKAT_ADMIN_PASSWORD: ${MEERKAT_ADMIN_PASSWORD:?set a password for the first admin}
volumes:
- meerkat-data:/data
restart: unless-stopped
volumes:
meerkat-data:
The image sets MEERKAT_DATA=/data and declares that volume. It runs as a non-root user (uid 65532), and the entry point is the binary itself, so anything after the image name is a flag. It is distroless: no shell, no wget, so docker exec ... sh has nothing to open, and a healthcheck runs /meerkat health (Health).
Verify the signature
All three images are signed when they are built, with cosign and no key. There is no public key to go and fetch: the signature carries the identity of the GitHub Actions workflow that produced the image, and that identity is what you check.
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/softwarity/meerkat-ce/\.github/workflows/(ci-ee|_docker)\.yml@refs/heads/main$' \
docker.io/softwarity/meerkat:latest
The same command verifies the evaluation image, docker.io/softwarity/meerkat:eval, and the Enterprise image at ghcr.io/softwarity/meerkat once you are logged in to that registry. All three are built by workflows living in the public mirror, softwarity/meerkat-ce, which is the repository the identity names - the Enterprise sources are private, the pipeline that builds them is not. The identity names two files of that repository, on main and nowhere else: _docker.yml builds the community latest, ci-ee.yml the Enterprise and evaluation images and every release tag. A workflow on another branch, or another file, does not pass. Versions up to 1.6.0 carry no signature: only latest and eval were signed then.
What is signed is the digest, and recursively: the manifest list plus each per-architecture image under it. Two consequences worth knowing. A client that pulled the arm64 image verifies what it is actually running, not its sibling. And a release, which puts its version on an already tested image without rebuilding it - an environment variable and a label over the very same layers - makes a new manifest, so it signs that one in turn, and fails if any tag it pushed does not verify.
To refuse an unsigned image across a cluster rather than check one by hand, the same two values go into a Kyverno policy:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: meerkat-signed
spec:
validationFailureAction: Enforce
rules:
- name: verify-meerkat
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "docker.io/softwarity/meerkat*"
- "ghcr.io/softwarity/meerkat*"
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: '^https://github\.com/softwarity/meerkat-ce/\.github/workflows/(ci-ee|_docker)\.yml@refs/heads/main$'
Check validationFailureAction against your own Kyverno version: recent ones moved that switch into the rule, and a policy that lands in audit mode reports instead of refusing.
Build it yourself
What is published is the images; a binary is built from the source.
git clone https://github.com/softwarity/meerkat-ce.git
cd meerkat-ce
make ui # builds the console and stages it for embedding (needs Node)
make build # -> bin/meerkat
./bin/meerkat --help
make ui is what puts the console inside the binary. Skip it and the gateway still routes, but the control plane answers a JSON status page instead of a console.
Go comes from go.mod, Node from .node-version. That repository is the community tree: the Enterprise sources are not in it, so what it builds is the community edition. See Editions.
What it reads at startup
Every flag has a MEERKAT_* equivalent, and the flag wins. ./bin/meerkat --help prints the list; this is it.
Ports
| Flag | Variable | Default | What |
|---|---|---|---|
-addr |
MEERKAT_ADDR |
:8080 |
data plane, plain HTTP |
-admin-addr |
MEERKAT_ADMIN_ADDR |
:9090 |
control plane, plain HTTP |
-internal-addr |
MEERKAT_INTERNAL_ADDR |
:7070 |
internal plane, plain HTTP: what the services behind the gateway call, never published |
-internal-tls-addr |
MEERKAT_INTERNAL_TLS_ADDR |
:7443 |
internal plane HTTPS, opened when a certificate is placed on it |
-tls-addr |
MEERKAT_TLS_ADDR |
:8443 |
data plane HTTPS, opened when TLS is switched on |
-admin-tls-addr |
MEERKAT_ADMIN_TLS_ADDR |
:9443 |
control plane HTTPS, same |
The HTTPS ports are opened and closed while the gateway runs, from the console. A failure there is logged and does not stop the plain ports - a typo in an address costs one door, not the installation.
Storage
| Flag | Variable | Default | What |
|---|---|---|---|
-data |
MEERKAT_DATA |
data |
directory holding the embedded storage |
-database-url |
MEERKAT_DATABASE_URL |
empty | PostgreSQL URL for several gateways sharing one installation |
-data is relative. Under Docker the image already sets it to /data; anywhere else, give it an absolute path, or the database lands wherever the process happened to start.MEERKAT_DATABASE_URL there answers with a sentence saying what is available rather than a driver error.First start only
| Variable | What |
|---|---|
MEERKAT_ADMIN_PASSWORD |
the first administrator's password, read only while there is no account at all |
MEERKAT_CONFIG_FILE (-config) |
a YAML or JSON configuration seeding an empty gateway. A configured one does not apply it: when the file differs from what runs, it is shelved as a saved configuration (named after the file), to compare and set as current from the Configuration screen |
MEERKAT_VAULT_FILE (-vault) |
an encrypted vault file, ingested once |
MEERKAT_VAULT_PASSPHRASE, MEERKAT_VAULT_PASSPHRASE_FILE |
the passphrase for that file |
MEERKAT_TENANCY (-tenancy) |
single or multi, settled on the first start; afterwards the console owns it |
Without a configuration file the gateway starts empty - no route - and the administrator's account is the only thing it creates.
-tenancy is for bootstrap - a first boot, a seeded install, GitOps. Once the mode has been chosen the flag is ignored, with a line in the log saying so rather than a silent override. Asking for multi on a community image starts single-tenant, and says that too.
Operating switches
| Flag | Variable | What |
|---|---|---|
-plug-addr |
MEERKAT_PLUG_ADDR |
developer tunnel listen address, default :22222, Enterprise only |
-console-url |
MEERKAT_CONSOLE_URL |
development override: proxy the console to a front dev server |
-version |
- | print version and exit |
The developer surface - the tooling, the API docs, the UI test mode, the tunnel - is switched in the console (Application, General and Infra, Plug), on a production gateway as anywhere else. MEERKAT_PRODUCTION, which used to close it from the environment, is no longer read: set, it only logs a warning.
Logs and traces
| Flag | Variable | Default | What |
|---|---|---|---|
-log-level |
MEERKAT_LOG_LEVEL |
info |
debug, info, warn or error; a typo falls back to info rather than stopping the start |
-log-format |
MEERKAT_LOG_FORMAT |
text | json or text: what is written before the gateway has read its settings. JSON for an agent is the console's switch, Infra, OpenTelemetry, logs written for an agent |
The level is the one a start comes back to; the console's Logs screen changes it live, for a while, on every node.
The access log, the export of traces, metrics and logs to a collector, and the logs' format for an agent are set in the console, Infra, OpenTelemetry: stored, applied at every start and live on every node. MEERKAT_ACCESS_LOG, MEERKAT_OTLP_ENDPOINT and MEERKAT_OTLP_SAMPLE are no longer read: set, each only logs a warning. See Traces and Logs.
Probes
The data and control planes answer both probes; the internal plane answers /healthz.
| Path | Answers |
|---|---|
/healthz |
liveness - UP unconditionally, because liveness decides whether to kill the process |
/readyz |
readiness - 503 with a reason when the store is not answering, or when the routing table has not been compiled yet |
/readyz, never /healthz. A liveness probe that failed on an unreachable database would turn a database blip into a restart of every node at once.Signals
SIGHUPreloads the routes.SIGINTandSIGTERMstop the gateway, draining for up to ten seconds.