Skip to content

Add opt-in flag to skip the crowdsec container prerequisite check #128

Description

@aficiomaquinas

Repo: https://github.com/hhftechnology/crowdsec_manager
Suggested labels: enhancement, good first issue, area: independent-image
Suggested area: cmd/server/main.go (prereq check) + internal/config/config.go (new env var)


Summary

The independent image's startup prerequisite check (checkPrerequisites in cmd/server/main.go:60-63 + :191-214) fatally aborts the manager when no Docker container with the name set in CROWDSEC_CONTAINER_NAME (default crowdsec) exists. The check is non-configurable, which forces every operator deploying crowdsec-manager:independent to also deploy a sibling crowdsec Docker container — even when their real CrowdSec deployment is a non-containerized one (apt-installed systemd unit, k8s DaemonSet, remote LAPI instance, etc.).

Please add an opt-in environment variable (e.g. SKIP_CROWDSEC_PREREQ=true) that, when set, downgrades the prereq check from Fatal to a Warn, so operators using the independent image without a co-located crowdsec container can still run the manager.


Our setup (why this hurts us)

We run SporeHarbor (a spore-based deployment platform for ~12 production hosts across WordPress, FreePBX, Zammad, ERPNext, etc.). On every host we install CrowdSec via the Debian/Ubuntu apt package, which gives us a systemd crowdsec.service running both the log-processing agent AND the Local API (LAPI) on 127.0.0.1:8080. This is intentional — the systemd-managed crowdsec is the single source of truth for local log processing, bouncer registration, and metrics on 127.0.0.1:6060.

We opted into the crowdsec-manager:independent image (love the no-Traefik / no-Pangolin / no-Gerbil footprint — exactly what we want) to get a web UI for managing decisions, allowlists, scenarios, and hub collections. The spec we wrote is at SporeHarbor/docs/specs/2026-07-08_crowdsec-manager-opt-in-docker.md.

Our exact deploy (one host, one systemd unit, no sibling container):

# /etc/systemd/system/crowdsec-manager.service
[Service]
ExecStart=/usr/bin/docker run --rm \
  --name crowdsec-manager \
  --network host \
  -e PORT=8081 \
  -e ENVIRONMENT=production \
  -e INCLUDE_CROWDSEC=false \
  -e CONFIG_DIR=/app/config \
  -e DATABASE_PATH=/app/data/settings.db \
  -v /var/lib/crowdsec-manager/config:/app/config \
  -v /var/lib/crowdsec-manager/data:/app/data \
  -v /var/lib/crowdsec-manager/logs:/app/logs \
  -v /var/lib/crowdsec-manager/backups:/app/backups \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  hhftechnology/crowdsec-manager:independent
Restart=always

On startup, the manager's own log shows:

{"level":"info","time":"...","msg":"Database initialized","path":"/app/data/settings.db"}
{"level":"info","time":"...","msg":"History database initialized","path":"./data/history.db"}
{"level":"info","time":"...","msg":"Multi-host Docker client initialized with single default host"}
{"level":"info","time":"...","msg":"Checking prerequisites..."}
{"level":"info","time":"...","msg":"  Docker daemon is running"}
{"level":"fatal","time":"...","msg":"Prerequisite check failed","error":"required container not found: crowdsec"}

The systemd unit then enters a crashloop. The crashloop is silent (no stdout/stderr in journalctl beyond the unit log line), which is itself a UX issue but separate from the prereq problem.


Why we'd benefit from a skip flag

We (and, we suspect, many other operators using independent against a non-containerized CrowdSec) currently have three options, all bad:

  1. Run a no-op sibling container named crowdsec (e.g. docker run --name crowdsec busybox sleep infinity). This is what we ended up doing as a workaround. Cost: ~5 MB RAM for a container whose only purpose is to satisfy the prereq check. Risk: if the container dies, the manager crashloops again. Risk: it pollutes docker ps output with a container that has no real purpose, confusing future operators.

  2. Run a real crowdsecurity/crowdsec container as a sidecar, in addition to the systemd crowdsec. Cost: duplicate log processing, duplicate metric endpoints, two CrowdSec processes competing for the same /var/log files. Risk: double-reporting to the Central API, double-bouncer-registration attempts, configuration drift between the two.

  3. Replace our systemd crowdsec with the container entirely. Cost: we lose the apt-managed upgrade path, lose the integration with our existing crowdsec-firewall-bouncer (apt), and have to maintain a Dockerfile / compose for a service that the distro already packages. Risk: when we add a new spore with a different log source, we have to remember to mount it into the container.

A SKIP_CROWDSEC_PREREQ=true env var would let us go with option 0: zero changes to our existing crowdsec deployment, no sidecar, no duplicate processes. The manager would still validate Docker daemon reachability (which is the only check that actually matters for its own operation — it needs the socket to query CrowdSec via ExecCommand / WriteFileToContainer / GetContainerLogs), and it would gracefully warn-and-continue when the optional crowdsec container isn't there.


Source locations in this repo

For reference, the relevant source as of the :independent image tag (org.opencontainers.image.revision = be96391de9a7d685bdb96c426cf801fcb55b0ba0):

// cmd/server/main.go:54-63
multiHost, err := docker.NewMultiHostClient(cfg.DockerHosts)
if err != nil {
    logger.Fatal("Failed to initialize Docker client", "error", err)
}
defer multiHost.Close()

dockerClient := multiHost.DefaultClient()
if err := checkPrerequisites(dockerClient, cfg); err != nil {
    logger.Fatal("Prerequisite check failed", "error", err)
}

// cmd/server/main.go:191-214
func checkPrerequisites(client *docker.Client, cfg *config.Config) error {
    logger.Info("Checking prerequisites...")

    if err := client.Ping(); err != nil {
        return fmt.Errorf("docker daemon not running: %w", err)
    }
    logger.Info("  Docker daemon is running")

    containers := []string{cfg.CrowdsecContainerName}
    for _, name := range containers {
        exists, err := client.ContainerExists(name)
        if err != nil {
            return fmt.Errorf("failed to check container %s: %w", name, err)
        }
        if exists {
            logger.Info("  Container exists", "name", name)
        } else {
            return fmt.Errorf("required container not found: %s", name)
        }
    }
    return nil
}

A minimal patch (illustrative, not a PR):

// internal/config/config.go (add to Config struct + Load())
SkipCrowdsecPrereq bool   // env: SKIP_CROWDSEC_PREREQ, default false

// cmd/server/main.go:checkPrerequisites
for _, name := range containers {
    exists, err := client.ContainerExists(name)
    if err != nil {
        if cfg.SkipCrowdsecPrereq {
            logger.Warn("Failed to check container (SKIP_CROWDSEC_PREREQ=true)", "name", name, "error", err)
            continue
        }
        return fmt.Errorf("failed to check container %s: %w", name, err)
    }
    if exists {
        logger.Info("  Container exists", "name", name)
    } else {
        if cfg.SkipCrowdsecPrereq {
            logger.Warn("  Container not found (SKIP_CROWDSEC_PREREQ=true)", "name", name)
            continue
        }
        return fmt.Errorf("required container not found: %s", name)
    }
}

(Keeping the Ping() check fatal — that's the one the manager truly depends on at runtime.)


Suggested env var naming

We'd be happy with any of these (in rough order of preference):

  1. SKIP_CROWDSEC_PREREQ=true — explicit, scoped to the one check
  2. REQUIRE_CROWDSEC_CONTAINER=false — explicit boolean inversion, follows INCLUDE_CROWDSEC style
  3. CROWDSEC_CONTAINER_REQUIRED=false — names the specific variable being affected

Option 1 is our top pick because it composes well with future prereq relaxations (e.g. if you ever add a traefik or pangolin container check, the same flag could be reused).


Related discussion

There's already an unanswered question on this exact topic: #116 — the asker is in the same boat (apt-installed crowdsec, wants to use the manager, gets the same required container not found: crowdsec error). Upvoting + linking this issue from there would help consolidate the signal.


Our workaround (for context, not asking upstream to adopt it)

For now, on each affected host we deploy a sidecar container:

docker run -d --name crowdsec --restart=always \
  docker.io/library/busybox:latest \
  /bin/sh -c "while true; do sleep 3600; done"

The manager's CROWDSEC_METRICS_URL still points at our systemd crowdsec's metrics endpoint (http://127.0.0.1:6060/metrics), so the dashboard works against real data — the busybox container is purely a presence-satisfier for the prereq check.

This works but feels wasteful, and we worry about it being cargo-culted by future operators who don't understand why there's a busybox container named crowdsec on every host. A native opt-out would let us delete the workaround and ship a cleaner deploy.


Reproduction steps (operator-facing)

# On any host with Docker but no `crowdsec` container running
docker run --rm --name crowdsec-manager-test \
  --network host \
  -e PORT=8080 \
  -e ENVIRONMENT=production \
  -e INCLUDE_CROWDSEC=false \
  -e CONFIG_DIR=/app/config \
  -e DATABASE_PATH=/app/data/settings.db \
  hhftechnology/crowdsec-manager:independent

# Expected (with proposed SKIP_CROWDSEC_PREREQ=true): web UI starts on :8080
# Actual (current): fatal "required container not found: crowdsec", container exits

# Workaround
docker run -d --name crowdsec docker.io/library/busybox:latest sleep infinity
docker run --rm --name crowdsec-manager-test \
  --network host \
  -e PORT=8080 \
  -e ENVIRONMENT=production \
  -e INCLUDE_CROWDSEC=false \
  -e CONFIG_DIR=/app/config \
  -e DATABASE_PATH=/app/data/settings.db \
  hhftechnology/crowdsec-manager:independent
# Now starts cleanly

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions