Skip to content

Installation

maintenant ships as a single binary with the frontend embedded. No external dependencies required — just deploy and go.


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:
docker compose up -d

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 install maintenant ./deploy/helm/maintenant \
  -n maintenant --create-namespace

Raw manifests

kubectl create namespace maintenant
kubectl apply -f deploy/kubernetes/

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:

  1. Stage 1 — Node.js 22 builds the Vue 3 SPA
  2. Stage 2 — Go 1.25 compiles the binary with the frontend embedded
  3. Stage 3 — Alpine 3.21 minimal runtime (runs as nobody:nobody uid 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:

curl http://localhost:8080/api/v1/health

Expected response:

{"status": "ok", "version": "v0.1.0"}