Installation¶
maintenant ships as a single binary with the frontend embedded. No external dependencies required — just deploy and go.
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). 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.
Production deployment
For production, place maintenant behind a reverse proxy with authentication. See the Configuration page for a Traefik + Authelia example.
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 (1Gi) 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.25 |
| 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 install
npm run build-only
cd ..
# Copy frontend assets into the embed directory
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: