Skip to content

Installation

flowwler is available two ways: as a Debian/Ubuntu .deb package (APT repository) or as container images. Both install the same binary and require a purchased license — see Pricing & Licensing, which also covers the free time-limited demo build (a different, credential-free distribution method, not covered here). Pick whichever of the two below fits your environment.


APT package (Debian/Ubuntu)

Add the APT repository

sudo mkdir -p /etc/apt/keyrings /etc/apt/auth.conf.d

curl -fsSL https://mirror.level66.services/apt/flowwler.gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/flowwler.gpg

sudo tee /etc/apt/sources.list.d/flowwler.sources >/dev/null <<'EOF'
Types: deb
URIs: https://mirror.level66.services/apt/flowwler
Suites: stable
Components: main
Architectures: amd64 arm64
Signed-By: /etc/apt/keyrings/flowwler.gpg
EOF

sudo chmod 0644 /etc/apt/keyrings/flowwler.gpg

Configure repository credentials

Credentials are issued per customer. Replace youruser and yourpassword with the values provided to you:

sudo tee /etc/apt/auth.conf.d/flowwler.conf >/dev/null <<'EOF'
machine mirror.level66.services
login youruser
password yourpassword
EOF

sudo chmod 0600 /etc/apt/auth.conf.d/flowwler.conf

Install

sudo apt update
sudo apt install flowwler

The package installs the following:

Path Description
/usr/bin/flowwler Daemon binary
/lib/systemd/system/flowwler.service systemd unit
/usr/share/flowwler/config.example.yaml Annotated example configuration
/etc/flowwler/config.yaml Active configuration (written from example on first install only)

The flowwler system user, /etc/flowwler, and /var/lib/flowwler are created automatically, and the flowwler systemd service is enabled and started automatically on first install.


Container images

flowwler (the daemon) and flowwler-web (the browser UI — see Web Interface) are both available as container images. Contact info@level66.network to receive a registry deploy token.

Registry

Image Registry path
flowwler registry.git.level66.network/flowwler/flowwler
flowwler-web registry.git.level66.network/flowwler/flowwler-web

Images are tagged with the release version they correspond to (e.g. v26.08.17). Both images additionally publish a floating latest tag pointing at the most recent release, for convenience — pin to a specific version tag in production so upgrades stay an explicit, deliberate choice rather than something that happens underneath you on a re-pull.

Authenticate

A deploy token is issued per customer. Replace <username> and <token> with the values provided to you:

docker login registry.git.level66.network -u <username> -p <token>

Docker Compose

The simplest way to run both images together:

# docker-compose.yaml
services:
  flowwler:
    image: registry.git.level66.network/flowwler/flowwler:v26.08.17
    command: ["/etc/flowwler/config.yaml"]
    ports:
      - "2055:2055/udp"   # NetFlow/IPFIX
      - "6343:6343/udp"   # sFlow
      - "179:179/tcp"     # BGP
    volumes:
      - ./config.yaml:/etc/flowwler/config.yaml:ro
      - flowwler-data:/var/lib/flowwler
    restart: unless-stopped

  flowwler-web:
    image: registry.git.level66.network/flowwler/flowwler-web:v26.08.17
    environment:
      FLOWWLER_API_URL: "http://flowwler:9731"
    ports:
      - "8080:8080"
    restart: unless-stopped

volumes:
  flowwler-data:

flowwler-web reaches flowwler by its Compose service name over the default bridge network — no need to publish port 9731 to the host at all unless you want it directly reachable (e.g. for Prometheus scraping outside this host).

Write your config.yaml next to the compose file — see Configuration Reference for the full field list, or copy config/example.yaml as a starting point.

docker compose up -d

flowwler's default BGP listen port is the standard 179. Binding it doesn't require any special flags here — Docker containers get the NET_BIND_SERVICE capability by default (unlike Kubernetes' restricted Pod Security Standard, which drops it unless explicitly added back; see the Kubernetes example below).

NetFlow/sFlow/BGP need to reach flowwler at a real, stable address your routers can be configured to point at — the host's own IP, reached via the published ports above, works for a single-host deployment. If you're running flowwler behind additional NAT, make sure your routers' configured exporter destination/BGP neighbor is the address that's actually reachable, not the container's internal one.

Docker (single container)

If you only need the daemon:

docker run -d --name flowwler \
  -p 2055:2055/udp -p 6343:6343/udp -p 179:179/tcp -p 9731:9731/tcp \
  -v ./config.yaml:/etc/flowwler/config.yaml:ro \
  -v flowwler-data:/var/lib/flowwler \
  registry.git.level66.network/flowwler/flowwler:v26.08.17 \
  /etc/flowwler/config.yaml

Kubernetes

The manifests below mirror deploy/k8s/ in the flowwler repository, reproduced here in full since that repository isn't customer-browsable. This is a starting point, not a drop-in — adjust it for your cluster and router topology before applying.

Registry pull secret

kubectl create secret docker-registry flowwler-registry \
  --docker-server=registry.git.level66.network \
  --docker-username=<username> \
  --docker-password=<token> \
  -n <your-namespace>

Config

# flowwler-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: flowwler-config
data:
  config.yaml: |
    netflow:
      listen_addr: "[::]:2055"
      aggregation_window: 10s

    sflow:
      listen_addr: "[::]:6343"
      enabled: false

    gobgp:
      local_asn: 65001            # CHANGE ME
      router_id: "10.0.0.1"       # CHANGE ME
      peers: []                   # CHANGE ME — add your BGP peers

    http:
      listen_addr: "[::]:9731"

    storage:
      path: "/var/lib/flowwler/flowwler.db"

    defaults:
      hold_time: 5m

    groups: []                    # CHANGE ME — add your groups/rules

See Configuration Reference for the full field list. If you enable http.auth (API key or Basic Auth password hash), move those values into a Kubernetes Secret instead of putting them in this ConfigMap in plaintext — not shown here, since the right approach depends on how you manage secrets elsewhere in your cluster.

Storage

# flowwler-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: flowwler-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

Deployment

flowwler needs a stable, externally routable IP for two things: receiving NetFlow/sFlow UDP exports from routers, and BGP peering (TCP) with them — both are configured on the router side with flowwler's IP hardcoded as a known exporter destination/BGP neighbor. A default pod IP is ephemeral (changes on every reschedule) and typically not externally routable (it's on the cluster's internal CNI overlay network). hostNetwork: true gives the pod the node's real, stable, already-routable IP instead — this is also why the Deployment below uses strategy.type: Recreate (avoids two hostNetwork pods briefly binding the same host ports during a rollout) and effectively runs as a single instance pinned to whichever node it's scheduled on. If your cluster can give a Service a stable, externally routable IP some other way (e.g. a LoadBalancer with a pinned address), you can drop hostNetwork and expose the relevant ports via a Service instead.

Binding BGP's standard port 179 from a non-root container needs the NET_BIND_SERVICE capability added back explicitly, since Kubernetes' restricted Pod Security Standard (used below) drops all capabilities by default — this is the one capability that profile still permits adding back, and it avoids running the container as root.

# flowwler-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flowwler
  labels:
    app: flowwler
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: flowwler
  template:
    metadata:
      labels:
        app: flowwler
    spec:
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      imagePullSecrets:
        - name: flowwler-registry
      containers:
        - name: flowwler
          image: registry.git.level66.network/flowwler/flowwler:v26.08.17
          args: ["/etc/flowwler/config.yaml"]
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
              add: ["NET_BIND_SERVICE"]
          ports:
            - name: http
              containerPort: 9731
            - name: netflow
              containerPort: 2055
              protocol: UDP
            - name: sflow
              containerPort: 6343
              protocol: UDP
            - name: bgp
              containerPort: 179
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 10
            failureThreshold: 30   # ~5 min for IRR/NetBox resolution
          volumeMounts:
            - name: config
              mountPath: /etc/flowwler
              readOnly: true
            - name: data
              mountPath: /var/lib/flowwler
        - name: flowwler-web
          image: registry.git.level66.network/flowwler/flowwler-web:v26.08.17
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          env:
            - name: FLOWWLER_LISTEN
              value: ":8080"
            - name: FLOWWLER_API_URL
              value: "http://localhost:9731"
          ports:
            - name: web
              containerPort: 8080
          livenessProbe:
            httpGet:
              path: /health
              port: web
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: web
            initialDelaySeconds: 5
            periodSeconds: 10
      volumes:
        - name: config
          configMap:
            name: flowwler-config
        - name: data
          persistentVolumeClaim:
            claimName: flowwler-data

livenessProbe/readinessProbe hit GET /health, which is intentionally unauthenticated (see REST API - Authentication) and only confirms the HTTP server is up, not that BGP peers or flow listeners are healthy — check GET /api/v1/status and GET /api/v1/bgp/peers for deeper health.

flowwler-web reaches flowwler over localhost:9731 because both containers share the same pod's (and, with hostNetwork: true, the node's) network namespace. It has no built-in authentication of its own on its browser-facing routes — --api-key/--username/--password only authenticate its outbound calls to flowwler's API — so put your own reverse proxy or Ingress with authentication in front of it before exposing it beyond your cluster.

GeoIP databases (optional)

If you enable geoip:, flowwler reads the .mmdb files from local disk and reloads them every 12 hours — it never downloads them itself (see Configuration - geoip). In Kubernetes, run MaxMind's own geoipupdate as a long-lived sidecar container in the same pod (not an initContainer — GEOIPUPDATE_FREQUENCY makes the binary loop and keep re-checking rather than run once and exit), sharing a volume with the flowwler container:

# flowwler-geoip-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: flowwler-geoip
type: Opaque
stringData:
  account-id: "<your MaxMind account ID>"
  license-key: "<your MaxMind license key>"

Add the sidecar to the Deployment above, mounting the existing data PVC with subPath: geoip so downloaded databases persist across pod restarts instead of re-downloading every time:

        - name: geoipupdate
          image: ghcr.io/maxmind/geoipupdate:v8.0.0
          securityContext:
            allowPrivilegeEscalation: false
            runAsUser: 1000    # the upstream image has no non-root default
            runAsGroup: 1000
            capabilities:
              drop: ["ALL"]
          env:
            - name: GEOIPUPDATE_ACCOUNT_ID
              valueFrom: {secretKeyRef: {name: flowwler-geoip, key: account-id}}
            - name: GEOIPUPDATE_LICENSE_KEY
              valueFrom: {secretKeyRef: {name: flowwler-geoip, key: license-key}}
            - name: GEOIPUPDATE_EDITION_IDS
              value: "GeoLite2-Country GeoLite2-ASN"
            - name: GEOIPUPDATE_DB_DIR
              value: /var/lib/flowwler/geoip
            - name: GEOIPUPDATE_FREQUENCY
              value: "72"   # hours - GeoLite2 itself only updates roughly weekly
          volumeMounts:
            - name: data
              mountPath: /var/lib/flowwler/geoip
              subPath: geoip

Point geoip.country_db_path/geoip.asn_db_path in flowwler-config.yaml at /var/lib/flowwler/geoip/GeoLite2-Country.mmdb and /var/lib/flowwler/geoip/GeoLite2-ASN.mmdb — the same path the flowwler container already sees via its own data volume mount (no subPath, so the sidecar's geoip/ subdirectory appears underneath it automatically).

Startup-ordering caveat: flowwler has no optional-style flag for GeoIP (unlike some other flow tools) — if geoip.enabled: true on a brand-new volume before geoipupdate's first download completes, flowwler's config validation (which requires the configured .mmdb paths to already exist and be readable) fails startup. Either pre-seed the volume with .mmdb files before first rollout, or leave geoip.enabled: false until you've confirmed the sidecar has completed its first sync.

Service

# flowwler-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: flowwler
spec:
  selector:
    app: flowwler
  ports:
    - name: http
      port: 9731
      targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: flowwler-web
spec:
  selector:
    app: flowwler
  ports:
    - name: web
      port: 8080
      targetPort: web

Both are ClusterIP — in-cluster access only (e.g. Prometheus scraping flowwler:9731, or your own Ingress fronting flowwler-web:8080). NetFlow/sFlow/BGP don't need a Service since hostNetwork: true already exposes them on the node.

Applying

kubectl apply -n <your-namespace> -f flowwler-config.yaml -f flowwler-pvc.yaml -f flowwler-deployment.yaml -f flowwler-service.yaml