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) — ifgeoip.enabled: trueon a brand-new volume beforegeoipupdate's first download completes, flowwler's config validation (which requires the configured.mmdbpaths to already exist and be readable) fails startup. Either pre-seed the volume with.mmdbfiles before first rollout, or leavegeoip.enabled: falseuntil 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