Native Linux Install¶
maintenant can run as a standalone systemd service on any amd64 or arm64 Linux host: no Docker, no container runtime required.
The published binaries are statically linked against musl, so a single file per architecture runs on Debian, Ubuntu, RHEL, Rocky, Alpine or Arch. There is no libc version to match and nothing to install alongside it.
The install script handles everything: binary download, SHA256 verification, cosign signature check, system user creation, and service activation. Running it again upgrades an existing install.
No built-in authentication
maintenant has no login of its own. The default 127.0.0.1:8080 keeps it local. Before binding another interface, put a reverse proxy with authentication in front, see Security.
Prerequisites¶
- Linux (amd64 or arm64), any distribution: the binary carries its own libc
curlorwget(not needed with--binary)installandsha256sum(coreutils) anduseraddsystemctl, unless installing with--no-service- Root access (
sudo) - To monitor containers: Docker Engine 19.03 or later (API 1.40)
What the script does¶
- Checks the platform and the prerequisites, then works out the install, configuration and data paths.
- Resolves the version and downloads the binary with
SHA256SUMS(or takes the local file given with--binary), and verifies them. - Creates the
maintenantsystem user (no login shell, home in the data directory) and adds it to thedockergroup when that group exists, see The docker group. - Creates the data directory (
0750, owned by the service user) and the configuration directory (0750,root:maintenant). - Merges the configuration flags you passed into
/etc/maintenant/maintenant.env. - Replaces the binary atomically (written next to
/usr/local/bin/maintenant, then renamed). - Writes the systemd unit, enables it, then restarts the service if it runs or starts it if not, and waits up to 10 seconds for it to become active.
- Prints a summary: version, binary path and the address it listens on.
Installation paths¶
Minimal: one-liner, all defaults¶
Listens on 127.0.0.1:8080 by default. Use a reverse proxy (nginx, Caddy) to expose it externally.
With custom configuration¶
Pass any configuration flag after --, as --flagName value or --flagName=value. They are written to /etc/maintenant/maintenant.env and loaded by the systemd service.
curl -fsSL https://install.maintenant.dev | sudo bash -s -- \
--addr 0.0.0.0:8080 \
--baseUrl https://monitoring.example.com \
--organisationName "Acme Corp" \
--logLevel info
Boolean flags take no value (--proxyLabels) or an explicit one (--proxyLabels=false); true, false, 1, 0, yes, no, on, off, t, f, y and n are accepted. --proxyLabels true, with a space, is refused: true would be read as a separate, unknown argument. Any other flag needs a non-empty, single-line value. For a value that starts with --, use the --flagName=value form. The script exits with code 2 on an unknown flag, a missing value or a bad boolean.
A flag value shows up in the process list while the script runs, and in your shell history. For a long-lived secret (license key, SMTP password, database URL), add the line to /etc/maintenant/maintenant.env yourself, as described further down.
Without systemd (containers, CI, minimal hosts)¶
Installs the binary, the user, the directories and the env file, and no service. Run it manually: maintenant --addr 0.0.0.0:8080. If a maintenant service already runs, the script warns that it still uses the previous binary, and you restart it yourself.
Pinning a specific version¶
curl -fsSL https://install.maintenant.dev | sudo MAINTENANT_VERSION=v1.11.0 bash -s -- --addr 0.0.0.0:8080
The variable is set on the bash side of the pipe. MAINTENANT_VERSION=v1.11.0 curl … | sudo bash does not pin anything: the assignment applies to curl, and sudo does not pass it on. Without the variable the script installs the latest release.
Standalone binaries exist from v1.6.0 on. If the version does not exist or predates them, the script exits with code 20 and lists the recent releases (among the 10 latest) that ship binaries. The script is also a release asset: to run the exact script of a release, download install.sh from that release's assets.
Air-gapped (no outbound internet)¶
--binary installs a binary you already have, with no network access at all. Download the files on a machine with internet access:
VERSION=v1.11.0
BASE=https://github.com/kOlapsis/maintenant/releases/download/${VERSION}
curl -LO ${BASE}/maintenant-${VERSION}-linux-amd64
curl -LO ${BASE}/SHA256SUMS
curl -LO ${BASE}/install.sh
Transfer the three files to the target host, then:
--sha256sumschecks the binary against aSHA256SUMSfile. The binary can have any file name: it is matched by checksum against alinux-<arch>entry of the file, and the version shown in the summary comes from that entry. A checksum that matches nothing for this architecture exits with code 21.- Without
--sha256sums, the script warns that the integrity of the binary is not checked and installs it, with the versionlocal. - The cosign signature is never verified offline, and the script says so. To check it, verify
SHA256SUMSwithSHA256SUMS.bundleon the connected machine first, see Manual verification. - Neither
curlnorwgetis required. Every other flag works as usual (--no-service, configuration flags). - A missing
--binaryfile exits with code 2.
Configuration reference¶
Every MAINTENANT_* environment variable in the table has a CLI equivalent. Precedence: CLI flag > environment variable > built-in default. The Configuration page describes what each one does.
| CLI flag | Environment variable | Type | Default |
|---|---|---|---|
--addr |
MAINTENANT_ADDR |
string | 127.0.0.1:8080 |
--baseUrl |
MAINTENANT_BASE_URL |
string | http://<addr> |
--corsOrigins |
MAINTENANT_CORS_ORIGINS |
string | (empty = same-origin) |
--trustedProxies |
MAINTENANT_TRUSTED_PROXIES |
string | (empty = none) |
--db |
MAINTENANT_DB |
string | ./maintenant.db (the service runs in its data directory, so /var/lib/maintenant/maintenant.db) |
--organisationName |
MAINTENANT_ORGANISATION_NAME |
string | Maintenant |
--runtime |
MAINTENANT_RUNTIME |
string | (autodetect) |
--proxyLabels |
MAINTENANT_PROXY_LABELS |
bool | false |
--logLevel |
MAINTENANT_LOG_LEVEL |
string | info |
--maxBodySize |
MAINTENANT_MAX_BODY_SIZE |
int | 1048576 |
--updateInterval |
MAINTENANT_UPDATE_INTERVAL |
duration | 24h |
--disableOsEolRefresh |
MAINTENANT_DISABLE_OS_EOL_REFRESH |
bool | false |
--securityScoreThreshold |
MAINTENANT_SECURITY_SCORE_THRESHOLD |
int | (unset) |
--disableTelemetry |
MAINTENANT_DISABLE_TELEMETRY |
bool | false |
--allowPrivateWebhooks |
MAINTENANT_ALLOW_PRIVATE_WEBHOOKS |
bool | false |
--licenseKey |
MAINTENANT_LICENSE_KEY |
string | (unset) |
--smtpHost |
MAINTENANT_SMTP_HOST |
string | (unset) |
--smtpPort |
MAINTENANT_SMTP_PORT |
string | 587 |
--smtpUsername |
MAINTENANT_SMTP_USERNAME |
string | (unset) |
--smtpPassword |
MAINTENANT_SMTP_PASSWORD |
string | (unset) |
--smtpFrom |
MAINTENANT_SMTP_FROM |
string | maintenant@localhost |
--smtpAllowPlaintext |
MAINTENANT_SMTP_ALLOW_PLAINTEXT |
bool | false |
--mcp |
MAINTENANT_MCP |
bool | false |
--mcpClientId |
MAINTENANT_MCP_CLIENT_ID |
string | (unset) |
--mcpClientSecret |
MAINTENANT_MCP_CLIENT_SECRET |
string | (unset) |
--mcpAllowedRedirectUris |
MAINTENANT_MCP_ALLOWED_REDIRECT_URIS |
string | (unset) |
--mcpAllowUnauthenticated |
MAINTENANT_MCP_ALLOW_UNAUTHENTICATED |
bool | false |
--k8sNamespaces |
MAINTENANT_K8S_NAMESPACES |
string | (empty = all) |
--k8sExcludeNamespaces |
MAINTENANT_K8S_EXCLUDE_NAMESPACES |
string | (unset) |
--statusUrl |
MAINTENANT_STATUS_URL |
string | (unset) |
--containerDownAfter |
MAINTENANT_CONTAINER_DOWN_AFTER |
duration | 0 (off) |
--retentionSnapshots |
MAINTENANT_RETENTION_SNAPSHOTS |
duration | 48h |
--retentionInterval |
MAINTENANT_RETENTION_INTERVAL |
duration | 1h |
--retentionBatchSize |
MAINTENANT_RETENTION_BATCH_SIZE |
int | 1000 |
--mode |
MAINTENANT_MODE |
string | embedded |
--server |
MAINTENANT_SERVER |
string | (unset) |
--enrollment-token |
MAINTENANT_ENROLLMENT_TOKEN |
string | (unset) |
--label |
MAINTENANT_LABEL |
string | (unset) |
--nodeName |
MAINTENANT_NODE_NAME |
string | (unset) |
--grpc-listen |
MAINTENANT_GRPC_LISTEN |
string | 127.0.0.1:8443 |
--grpc-url |
MAINTENANT_GRPC_URL |
string | (unset) |
--grpc-tls-cert |
MAINTENANT_GRPC_TLS_CERT |
string | (unset) |
--grpc-tls-key |
MAINTENANT_GRPC_TLS_KEY |
string | (unset) |
--grpc-server-fingerprint |
MAINTENANT_GRPC_SERVER_FINGERPRINT |
string | (unset) |
--grpc-insecure-skip-tls-verify |
MAINTENANT_GRPC_INSECURE_SKIP_TLS_VERIFY |
bool | false |
--embedded-agent |
MAINTENANT_EMBEDDED_AGENT |
bool | false |
--grpc-tls-insecure |
MAINTENANT_GRPC_TLS_INSECURE |
bool | false |
--agentRateLimitPerSecond |
MAINTENANT_AGENT_RATE_LIMIT_PER_SECOND |
int | 1000 |
--agentStaleThresholdSeconds |
MAINTENANT_AGENT_STALE_THRESHOLD_SECONDS |
int | 60 |
--agentSpoolMaxMemoryBytes |
MAINTENANT_AGENT_SPOOL_MAX_MEMORY_BYTES |
int | 16777216 |
--agentSpoolMaxDiskBytes |
MAINTENANT_AGENT_SPOOL_MAX_DISK_BYTES |
int | 134217728 |
--agentSpoolMaxAgeSeconds |
MAINTENANT_AGENT_SPOOL_MAX_AGE_SECONDS |
int | 86400 |
--data-dir |
MAINTENANT_DATA_DIR |
string | /var/lib/maintenant |
--ca-cert |
MAINTENANT_CA_CERT |
string | (unset) |
--database-url |
MAINTENANT_DATABASE_URL |
string | (empty = SQLite) |
The spelling is not uniform: most flags are camelCase, while enrollment-token, grpc-*, embedded-agent, data-dir, ca-cert and database-url are kebab-case. Use the names exactly as written; the script rejects any other spelling.
Two options exist only on the binary, and the script refuses them with exit code 2: --copy-store-to and --yes (copy an install into PostgreSQL, see PostgreSQL storage). MAINTENANT_DEMO_TOKEN has no flag at all.
Run maintenant --help to see this list with descriptions at any time.
Script options and environment¶
| Option | Effect |
|---|---|
--no-service |
Do not install or enable the systemd service. |
--uninstall |
Remove the binary and the service. Keeps the data, the configuration and the user. |
--purge |
With --uninstall: also remove the data, the configuration and the user. |
--skip-cosign |
Skip the cosign signature check. The SHA256 check stays mandatory. |
--binary <path> |
Install this local binary instead of downloading one. |
--sha256sums <path> |
With --binary: check it against this SHA256SUMS file. |
--help, -h |
Print the usage and exit. |
| Variable | Default | Effect |
|---|---|---|
MAINTENANT_VERSION |
latest |
Release to install. |
MAINTENANT_INSTALL_DIR |
/usr/local/bin |
Where the binary goes. |
MAINTENANT_DATA_DIR |
/var/lib/maintenant |
Data directory. |
MAINTENANT_CONFIG_DIR |
/etc/maintenant |
Configuration directory. |
MAINTENANT_SKIP_COSIGN |
unset | Any non-empty value behaves like --skip-cosign. |
NO_COLOR |
unset | Disable ANSI colors. |
Because sudo does not pass the environment through, give these on the sudo command line, as in sudo MAINTENANT_VERSION=v1.11.0 bash.
Installing a native agent¶
A host that only reports to an existing server installs the same binary in agent mode:
curl -fsSL https://install.maintenant.dev | sudo bash -s -- \
--mode agent \
--server grpcs://maintenant.example.com:8443 \
--enrollment-token TOKEN \
--label web-01
This is the command the Standalone tab of the enrollment dialog shows, with --label added. The agent keeps its identity and its spool in /var/lib/maintenant. See Agent Setup for the server side and the enrollment token.
The token stays in /etc/maintenant/maintenant.env. The agent uses it for its first enrolment. If the server later refuses the stored identity (the agent was deleted or revoked), the agent enrols a new one with the token in the file. A token works once, so create a new one and run the script again with --enrollment-token NEWTOKEN: it replaces the value in the file and restarts the service.
The /etc/maintenant/maintenant.env file¶
When you pass configuration flags to the install script, they are written to /etc/maintenant/maintenant.env in KEY=value format, with 0640 permissions, owner root and group maintenant. The systemd service loads this file via EnvironmentFile=. You can edit it directly:
Re-running the script with flags merges them into the existing file: only the keys you pass are replaced, or added when missing, and every other line is kept as it is, comments and extra variables such as DOCKER_HOST included. The script reports N keys replaced, M keys added, other lines kept. The file is untouched when you pass no configuration flag.
The unit applies ProtectSystem=strict, ProtectHome=true and PrivateTmp=true: a file you point maintenant to (a CA bundle, a TLS key) must live outside /home and be readable by the maintenant user.
Custom data and database paths¶
By default the data directory is /var/lib/maintenant and the database is maintenant.db inside it. --data-dir and --db move them:
curl -fsSL https://install.maintenant.dev | sudo bash -s -- \
--data-dir /srv/maintenant \
--db /mnt/volume/maintenant/maintenant.db
- The data directory is the first of:
--data-dir,MAINTENANT_DATA_DIRin the env file, theMAINTENANT_DATA_DIRenvironment variable of the script,/var/lib/maintenant. It must be an absolute path other than/. - The database is the first of:
--db,MAINTENANT_DBin the env file,./maintenant.db. A relative path lands in the data directory; an absolute path can point anywhere. - The script creates both directories, owned by the service user with
0750, and writes a unit with the matchingWorkingDirectory,ReadWritePaths,Environment=MAINTENANT_DATA_DIR,User,ExecStartandEnvironmentFile. - An existing directory that is not empty and belongs to another owner is refused (exit code 30): use a dedicated directory, or
chownit tomaintenantfirst. - The install, configuration, data and database directories accept only the characters
A-Z a-z 0-9 . _ / @ + -; any other character exits with code 2. --uninstall --purgeremoves the real data directory. A database directory outside it is kept, with a warning.
The docker group¶
If a docker group exists when the script runs, the maintenant user is added to it, so the service can read the Docker socket. Membership of the docker group is equivalent to root access on the host. If Docker is installed after maintenant, the user is not added: run the script again, or add it yourself with usermod -aG docker maintenant and restart the service. --uninstall --purge removes the user from the group before deleting it.
To avoid that access, remove the user from the group (gpasswd -d maintenant docker) and point maintenant at a socket proxy with DOCKER_HOST=tcp://… in the env file, see Security. The merge keeps such a line. The next run of the script adds the user to the group again, so repeat the gpasswd command after an upgrade.
Upgrade¶
Re-run the install script. It replaces the binary atomically, rewrites the unit file, reloads systemd, enables the service and restarts it if it is running (it starts the service otherwise). Your data and your env file stay in place.
To upgrade to a specific version:
With --no-service the script replaces the binary and warns that a running service still uses the previous one: restart it with systemctl restart maintenant.
Uninstall¶
# Remove binary and service, keep data and config
curl -fsSL https://install.maintenant.dev | sudo bash -s -- --uninstall
# Remove everything including /var/lib/maintenant and /etc/maintenant
curl -fsSL https://install.maintenant.dev | sudo bash -s -- --uninstall --purge
--purge deletes the data directory, the configuration directory and the maintenant user. It prompts for confirmation when the script reads from a terminal. In a non-interactive pipe (curl | bash), it executes directly: only pass --purge when you mean it. If you installed with --data-dir or --db, pass the same flags (or keep them in the env file) so the script finds what to remove.
Supply-chain verification¶
Every release includes:
| Asset | Purpose |
|---|---|
maintenant-vX.Y.Z-linux-amd64 / -arm64 |
Binary for each architecture |
install.sh |
The install script itself, stamped with the release tag |
SHA256SUMS |
Checksums for the binaries and the script |
SHA256SUMS.bundle |
Sigstore bundle: the cosign signature and its certificate |
provenance.intoto.jsonl |
SLSA build provenance of the container image |
sbom.cdx.json |
CycloneDX SBOM of the container image |
The binaries have their own SLSA build provenance attestation, stored on GitHub: it is checked with gh attestation verify below, and it is not a release asset.
The script published with a release carries that release's tag in its header, and
writes it into /etc/maintenant/maintenant.env (# Script version:), so a file on
disk always names the script that produced it.
What the script does automatically¶
- Downloads the binary and
SHA256SUMSinto a temp directory. - Verifies the binary against its SHA256 checksum: mandatory, exits code 21 on mismatch.
- If
cosign3 or later is in$PATHand--skip-cosignis not set, verifies theSHA256SUMSsignature from the bundle against the Sigstore transparency log, asserting that the signature was produced by the officialrelease.ymlworkflow on the correct tag.
cosign 3 is required. sign-blob dropped --output-signature and --output-certificate, so a release carries a bundle and nothing else, and reading that bundle needs a 3.x binary. An older cosign is treated like an absent one: the script says so and continues, rather than reporting a signature failure that would say nothing about the signature.
The cosign check is best-effort: if cosign is absent, the script warns and continues. Install it for full supply-chain protection:
# Install cosign (see https://docs.sigstore.dev/cosign/system_config/installation/)
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
sudo install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
Manual verification¶
VERSION=v1.11.0
ARCH=amd64
BASE=https://github.com/kOlapsis/maintenant/releases/download/${VERSION}
curl -LO ${BASE}/maintenant-${VERSION}-linux-${ARCH}
curl -LO ${BASE}/SHA256SUMS
curl -LO ${BASE}/SHA256SUMS.bundle
# SHA256
sha256sum -c SHA256SUMS --ignore-missing
# cosign (3.x)
cosign verify-blob \
--bundle SHA256SUMS.bundle \
--certificate-identity-regexp \
"^https://github\.com/kOlapsis/maintenant/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
# SLSA provenance
gh attestation verify --owner kOlapsis maintenant-${VERSION}-linux-${ARCH}
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Success, or --help |
1 |
Unexpected failure, for example useradd failing |
2 |
Invalid argument: unknown flag, missing or empty value, bad boolean, unsupported character in a path, --sha256sums without --binary, or a --binary or --sha256sums file that does not exist |
10 |
Unsupported OS or architecture |
11 |
Not running as root |
12 |
Missing required tool: curl or wget (unless --binary), install, useradd, or systemctl (unless --no-service) |
20 |
Release not found, release without standalone binaries, or download failure |
21 |
SHA256 checksum mismatch, or (offline) no matching linux-<arch> entry in SHA256SUMS |
22 |
cosign signature invalid |
30 |
Filesystem error, including a non-empty directory owned by someone else |
31 |
systemd failure: a systemctl command failed, or the service was not active after 10 seconds |