Configuration¶
Everything is configured through a single .env file at the repository root,
plus the files Cowrie reads directly. This page lists every variable, what reads
it, and what it falls back to.
The .env File¶
Docker Compose reads .env from the directory it runs in, and the justfile
loads it too, so both see the same values. Four variables must be set before
the stack runs: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB and
FLASK_SECRET_KEY. The example ships changeme for the two secrets.
A variable only reaches a container if the compose file passes it
Each service in docker-compose.yml lists what it receives under
environment:. Adding a name to .env that is not in that list does
nothing. The tables below say which variables are already wired in.
Changes take effect when the container is recreated with docker compose up -d.
Volumes and the schema survive that.
Database¶
| Variable | Read by | Default | Notes |
|---|---|---|---|
POSTGRES_USER |
postgres, api, ingestor | honeywatch |
Required. Becomes the bootstrap superuser on first boot. In docker-compose.yml both the api and the ingestor connect as this role. |
POSTGRES_PASSWORD |
postgres, api, ingestor | none | Required. The api refuses to boot without it; the ingestor exits with KeyError. |
POSTGRES_DB |
postgres, api, ingestor | honeywatch |
Required. |
POSTGRES_HOST |
api, ingestor | postgres |
Set by the compose file. |
POSTGRES_PORT |
api, ingestor | 5432 |
The port inside the Docker network, not the host port. |
POSTGRES_SSLMODE |
api | disable |
Postgres is only reachable on the private bridge, so there is no certificate step. The ingestor has no knob for this. |
POSTGRES_HOST_PORT |
docker-compose.yml |
5433 |
Changes the host-side publish only, never the bind address; for that see Pin the Ports. |
Postgres reads POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB only when
it initialises an empty data directory. Once postgres-data exists, editing them
in .env changes nothing on the server and breaks every service that reconnects
with the new values. Pick the names before the first docker compose up. To
change them later, alter the role and database by hand (just db shell), or
start over with docker compose down -v, which destroys your data.
The pg_hba.conf warning at the top of .env.example applies to
docker-compose.prod.yml, which mounts that file. docker-compose.yml does
not, so on the self-hosting path the names are yours to choose.
API¶
| Variable | Read by | Default | Notes |
|---|---|---|---|
ENVIRONMENT |
api | production |
Only development and production are accepted. docker-compose.yml does not set it, so it falls back to production. |
FLASK_SECRET_KEY |
api | none | Required. ENVIRONMENT=development enables an insecure fallback. |
LOG_LEVEL |
api | INFO |
Not passed by the compose file. |
LOG_FORMAT |
api | json in production, text otherwise |
Not passed by the compose file. |
GUNICORN_WORKERS |
api | 2 |
Not passed by the compose file. |
GUNICORN_THREADS |
api | 4 |
|
GUNICORN_TIMEOUT |
api | 30 |
Seconds. |
GUNICORN_GRACEFUL_TIMEOUT |
api | 30 |
Seconds. |
The API refuses to start without FLASK_SECRET_KEY
An empty secret makes the app factory raise at worker boot, which looks
like gunicorn workers dying in a loop. Check docker compose logs api.
The secret is checked before the database password, so fixing one can
surface the other.
Two things are not configurable. Gunicorn binds 0.0.0.0:5000 inside the
container; the published host port is a compose setting. And the SQLAlchemy
pool is fixed at 5 connections plus 5 overflow, so keep
GUNICORN_WORKERS * GUNICORN_THREADS at or below 10.
Geolocation¶
Optional. Without it the stack runs, but sessions have no country, city or ASN and the world map and Origins page stay empty.
| Variable | Read by | Default | Notes |
|---|---|---|---|
MAXMIND_ACCOUNT_ID |
scripts/fetch-mmdb.sh |
none | Only needed to download the databases. |
MAXMIND_LICENSE_KEY |
scripts/fetch-mmdb.sh |
none | Same. |
GEOIP_DATA_DIR |
ingestor | /data/geoip |
Not passed by the compose file, which bind-mounts ./ingestor/data there. |
The ingestor expects GeoLite2-City.mmdb and GeoLite2-ASN.mmdb in that
directory. The Quick Start covers
fetching them and backfilling old sessions.
just reclassify-geoip runs on the host
It needs uv, the Postgres host port, and GEOIP_DATA_DIR pointing at the
real directory, otherwise it looks for /data/geoip on your host, finds
nothing, and reports every address as unenrichable. Prefer
docker compose exec ingestor python -m src.reclassify_geoip.
Dashboard¶
| Variable | Read by | Default | Notes |
|---|---|---|---|
VITE_API_BASE |
dashboard, at build time | / (same origin) |
Lives in dashboard/.env.example, not the root .env. Must be /, empty, or an absolute http(s):// URL. |
The Dockerfile declares no build argument for it, so the image compose builds
is always same-origin. It exists for pnpm dev against a separately running
API. Leave it empty everywhere else: the API sends no CORS headers and the
dashboard's CSP sets connect-src 'self', so a cross-origin value is blocked
by the browser.
Ingestor Tuning¶
| Variable | Read by | Default | Notes |
|---|---|---|---|
LOG_PATH |
ingestor | /logs/cowrie.json |
The Cowrie log inside the shared volume. |
DROP_LOOPBACK |
ingestor | 1 |
Drops sessions from loopback addresses. docker-compose.yml sets 0 so your own test logins count. |
RETRY_ATTEMPTS |
ingestor | 3 |
Attempts per failed database write. |
RETRY_INITIAL_BACKOFF |
ingestor | 1.0 |
Seconds, doubling between attempts. Three attempts take about three seconds. |
FUSE_THRESHOLD |
ingestor | 50 |
Consecutive failures before the circuit breaker trips. |
FUSE_SLEEP |
ingestor | 30.0 |
Seconds to wait after tripping before probing again. |
QUEUE_MAX |
ingestor | 10000 |
Queue depth between the tailer and the writer. When full, the tailer blocks and lines stay in cowrie.json. |
HEALTHCHECK_PATH |
ingestor | /tmp/healthy |
Liveness file. The Dockerfile health check reads the same path, so change both or neither. |
METRICS_ENABLED |
ingestor | 0 |
1 serves Prometheus metrics on 127.0.0.1 inside the container. |
METRICS_PORT |
ingestor | 9101 |
docker-compose.yml passes only DROP_LOOPBACK. To change anything else, add
it to the ingestor's environment: block. The metrics endpoint binds to
loopback inside the container, so nothing outside can scrape it without a
sidecar or docker compose exec.
Cowrie¶
Cowrie takes no environment variables in docker-compose.yml. It is configured
by files bind-mounted read-only from cowrie/:
| File | Controls |
|---|---|
cowrie.cfg |
Hostname, prompt, SSH banner, offered ciphers, sensor name, kernel strings, log path. |
userdb.txt |
Which username and password pairs are accepted. |
fs/centos-stream10.pickle |
The fake filesystem tree, built from a real CentOS Stream 10 image. |
cmdoutput.json |
Canned output for some commands. |
honeyfs/ |
Contents of readable files such as /etc/os-release. |
txtcmds/ |
Static output for commands like df, lscpu and top. |
Edit on the host, then docker compose restart cowrie.
userdb.txt is an allowlist with a catch-all reject at the end, and it
explicitly rejects root:root, root:toor, root:12345 and root:password.
A file added to honeyfs/ only shows up if the same path also exists in the
filesystem pickle.
Production-Only Variables¶
.env.example lists several variables that docker-compose.yml never uses.
They belong to docker-compose.prod.yml, the maintainer's file for one
specific instance, and you do not need them.
| Variable | Purpose |
|---|---|
API_VERSION, INGESTOR_VERSION |
Image tags for prebuilt images. Not in .env.example. |
TS_IP |
Private-network address the internal proxy binds to. |
POSTGRES_INGESTOR_PASSWORD, POSTGRES_API_PASSWORD |
Passwords for the per-app roles created by postgres/init.sh. |
COWRIE_HONEYPOT_INTERNET_FACING_IP |
Makes ifconfig inside the honeypot show a public address instead of the container's NAT address. Either unset or non-empty; an empty value blanks the field. |
On the docker-compose.yml path there is no per-role split: the api and the
ingestor both connect as the bootstrap superuser.