Installation¶
maintenant ships as a single binary with the frontend embedded. No external dependencies required — just deploy and go.
Two ways in: a container, covered below, or the binary itself as a systemd service on a host with no container runtime — see Native Linux Install.
Docker Compose (Recommended)¶
The fastest way to get started. Create a docker-compose.yml:
services:
maintenant:
image: ghcr.io/kolapsis/maintenant:latest
ports:
- "8080:8080"
read_only: true
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:noexec,nosuid,size=64m
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /proc:/host/proc:ro
- maintenant-data:/data
environment:
MAINTENANT_ADDR: "0.0.0.0:8080"
MAINTENANT_DB: "/data/maintenant.db"
restart: unless-stopped
volumes:
maintenant-data:
Open http://localhost:8080. maintenant auto-discovers all your containers immediately.
Docker socket access
The entrypoint reads the mounted socket's group and grants the unprivileged user access
automatically — no group_add required, on Compose and Swarm alike. Set DOCKER_GID only
to pin a specific GID (non-standard socket path or socket proxy), or to grant gid 0 on a host
whose socket is owned by root:root with no docker group (Synology DSM). See
Troubleshooting if access fails.
Why 0.0.0.0? And the security finding it triggers
MAINTENANT_ADDR: "0.0.0.0:8080" is the bind address inside the container. It must be
0.0.0.0 there — Docker's port mapping reaches the process through the container's bridge
interface, so binding 127.0.0.1 inside the container would make the UI unreachable. This
setting alone exposes nothing to your network.
What decides the actual exposure is the ports: mapping. "8080:8080" publishes the port on
all host interfaces — and maintenant's own security scanner, which does not exempt
maintenant's container, will report a critical "Port exposed on all interfaces" finding
for it. That finding is expected, not a bug. To avoid it, publish the port on a specific
interface instead:
ports:
- "127.0.0.1:8080:8080" # local only — pair with a reverse proxy
# or, for direct access from your LAN on a headless server:
# - "192.168.1.50:8080:8080" # the server's LAN IP
See Configuration → Choosing a Bind Address for the full decision guide, including how to acknowledge the finding when the exposure is intentional.
Run without mounting the Docker socket
Mounting docker.sock — even :ro — hands the container root-equivalent access to the
host; the :ro flag does not block Docker API writes. maintenant's API usage is entirely
read-only, and its client honours DOCKER_HOST, so it runs cleanly behind a
docker-socket-proxy that rejects every
write with 403. Recommended for production.
Deploying on a cloud provider
A ready-to-use cloud-init file provisions any Ubuntu cloud server with Docker and
maintenant on first boot, with the dashboard bound to loopback. Firewall rules, block volumes,
private-network agents, load balancer checks and managed Kubernetes are per-provider:
Production deployment
For production, place maintenant behind a reverse proxy with authentication. See the Configuration page for a Traefik + Authelia example.
Watching a fleet?
The server holds what the agents cannot rebuild: their identity and their enrolment. On the default local file, losing that machine means re-enrolling every host by hand. If you already operate a PostgreSQL, point the server at it and the instance becomes replaceable — see PostgreSQL storage. An existing install moves over with a one-shot service you add next to the instance:
Kubernetes¶
Helm (recommended)¶
Raw manifests¶
Both approaches deploy:
- A ServiceAccount with read-only RBAC (pods, logs, services, namespaces, events, deployments, statefulsets, daemonsets, replicasets, pod metrics)
- A Deployment with security hardening (non-root, read-only filesystem, all capabilities dropped)
- A PersistentVolumeClaim (10Gi) for the SQLite database
- A ClusterIP Service on port 80
maintenant auto-detects the in-cluster Kubernetes API. Namespace filtering and workload-level monitoring work out of the box.
Note
The deployment uses strategy: Recreate because SQLite requires a single writer.
Do not scale beyond 1 replica.
For detailed Kubernetes configuration and all Helm values, see the Kubernetes Guide.
Building from Source¶
Requirements¶
| Tool | Minimum Version |
|---|---|
| Go | >= 1.26.6 |
| Node.js | >= 20 |
| CGO | Enabled |
| Docker | For testing |
Build Steps¶
# Clone the repository
git clone https://github.com/kolapsis/maintenant.git
cd maintenant
# Build the frontend
cd frontend
npm ci
npm run build-only
cd ..
# Copy frontend assets into the embed directory
rm -rf cmd/maintenant/web/dist/*
cp -r frontend/dist/. cmd/maintenant/web/dist/
# Build the Go binary
CGO_ENABLED=1 go build -o maintenant ./cmd/maintenant
# Run
./maintenant
The resulting maintenant binary includes the entire frontend (embedded via Go's embed.FS). There is nothing else to deploy.
Build with Version Info¶
CGO_ENABLED=1 go build \
-ldflags="-s -w \
-X main.version=$(git describe --tags --always) \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o maintenant ./cmd/maintenant
Docker Image¶
The official Docker image uses a multi-stage build:
- Stage 1 — Node.js 22 builds the Vue 3 SPA
- Stage 2 — Go 1.25 compiles the binary with the frontend embedded
- Stage 3 — Alpine 3.21 minimal runtime (runs as
nobody:nobodyuid 65534, read-only binary, health check included)
The image is available at ghcr.io/kolapsis/maintenant.
Verifying the Installation¶
Once maintenant is running, verify it with the health endpoint:
Expected response: