Full Configuration Reference
This page mirrors the project’s two canonical sources of truth verbatim:
- the documented
.env.exampletemplate — every environment variable, and - the binary’s
--helpoutput — every CLI flag.
The .env.example block below is embedded straight from the repository at build time, so it can’t drift from the code. For how settings layer together (a CLI flag or its BIRDNET_* variable > the Settings page > birdnet.conf > the built-in default) and the web-UI-only settings, see Configuration.
.env.example names every setting by its environment variable (BIRDNET_…). The same settings have a different name in birdnet.conf, and a station configured by file wants that name, so the next section lists the file keys that neither block below spells out.
birdnet.conf keys by their file names
The file keys are the ones birdnet-behavior --doctor knows (crates/birdnet-core/src/config/known_keys.rs); a key outside that list is reported at startup with the nearest real name. For most of them the environment variable is BIRDNET_ plus the key, and the flag is the lower-case, hyphenated key; where it is not, the table says so. Defaults and meanings are the flag’s own, from the CLI reference below. The settings that are only in .env.example or the Configuration table are not repeated here.
| File key | Flag · environment variable | Default | What it does |
|---|---|---|---|
PIPEWIRE_DEVICE | --pipewire-device · BIRDNET_PIPEWIRE_DEVICE | — | PipeWire or PulseAudio source (default for the system default). Takes precedence over ALSA_CARD when both are set |
SEGMENT_DURATION | --segment-duration · BIRDNET_SEGMENT_DURATION | 15 | Seconds per recording segment, from 3 to 600 (--doctor checks the config file against that; the Settings page and PUT /api/v2/settings refuse a value outside it, and a fraction, a unit or a negative number). BirdNET-Pi’s RECORDING_LENGTH is read as this value when SEGMENT_DURATION is absent or blank (an explicit SEGMENT_DURATION wins), so a birdnet.conf carried over from BirdNET-Pi keeps its chunk length |
NIGHT_INHIBIT | --night-inhibit · BIRDNET_NIGHT_INHIBIT | off | Do not record at night; needs LATITUDE and LONGITUDE |
PRE_SUNRISE_OFFSET | --pre-sunrise-offset · BIRDNET_PRE_SUNRISE_OFFSET | unset: TWILIGHT_OFFSET | Minutes before sunrise at which a solar schedule starts recording |
POST_SUNSET_OFFSET | --post-sunset-offset · BIRDNET_POST_SUNSET_OFFSET | unset: TWILIGHT_OFFSET | Minutes after sunset at which a solar schedule stops (TWILIGHT_OFFSET itself defaults to 30) |
AUDIOFMT | --audio-format · BIRDNET_AUDIO_FORMAT | wav | BirdNET-Pi’s name for the clip format (wav, mp3, flac, ogg); AUDIO_FORMAT is the other accepted spelling, and AUDIOFMT wins if both are set. Non-WAV needs ffmpeg or sox |
CLIP_TARGET_LUFS | BIRDNET_CLIP_TARGET_LUFS (no flag) | off | Normalise saved clips to this integrated loudness, e.g. -18 (−40 to −6; a value outside that is ignored, not clamped); see Audio & Microphones |
CLIP_PEAK_CEILING_DBFS | BIRDNET_CLIP_PEAK_CEILING_DBFS (no flag) | -1 | The sample peak a normalised clip may not pass, in dBFS (−12 to 0) |
CUSTOM_IMAGE_DIR | --custom-image-dir · BIRDNET_CUSTOM_IMAGE_DIR | — | A folder of your own species pictures, one file per species, named {lowercase_scientific_name_with_underscores} plus .jpg, .jpeg, .png or .webp (the code tries them in that order), checked before the image cache. It replaces a species’ picture; it is not a single dashboard image |
INFO_SITE | --info-site · BIRDNET_INFO_SITE | ebird | Species info links: ebird, allaboutbirds or none |
SITENAME | --site-name · BIRDNET_SITENAME | BirdNet-Behavior | The name in page titles and the header |
STATION_NAME | — (the Station Name field on the Settings page) | — | The station’s own name: the Home Assistant discovery name when --site-name is not given, and the default location name in the eBird export |
LANG | — | — | Range-checked as a language code but not read as the language: the language key is DATABASE_LANG (--lang, BIRDNET_LANG; default en) |
MODEL | — | — | BirdNET-Pi’s model name; it is only echoed in the start-up log. The model that runs is MODEL_PATH |
LOG_LEVEL | --log-level · BIRDNET_LOG_LEVEL | the built-in filter | trace, debug, info, warn, error or off; RUST_LOG overrides it |
LOG_MODULES | --log-modules · BIRDNET_LOG_MODULES | — | Per-subsystem levels, e.g. audio=debug,web=warn (subsystems: audio, detection, web, db, integrations, analytics) |
WEEKLY_REPORT_SCHEDULE | --weekly-report-schedule · BIRDNET_WEEKLY_REPORT_SCHEDULE | monday | The weekday the weekly report is sent through Apprise, or disabled |
APPRISE_TRIGGER | --notify-trigger · BIRDNET_NOTIFY_TRIGGER | each | each, new-species (fewer than 5 detections this week), new-species-daily (first of the day) or off; a comma-separated list sends one message when any of them matches (BirdNET-Pi’s three APPRISE_NOTIFY_* switches) |
FULL_DISK | — (the When the disk is full field on the Settings page) | purge | purge or keep: at DISK_PURGE_THRESHOLD, delete the oldest clips, or stop recording and delete nothing. A held recording resumes by itself once usage is 2 points below the threshold (below 93 % for 95 %), or after a restart with a higher threshold. Only the recordings directory takes keep: if --watch-dir is the recordings directory it is ignored, and the start-up log says so |
APPRISE_WATCHLIST | --notify-species-only · BIRDNET_NOTIFY_SPECIES_ONLY | — | Notify only for these species (comma-separated scientific names) |
APPRISE_WATCHLIST_EXCLUDE | --notify-species-exclude · BIRDNET_NOTIFY_SPECIES_EXCLUDE | — | Never notify for these species |
APPRISE_COOLDOWN | — | 300 | Seconds before the same species can notify again |
MQTT_PORT | --mqtt-port · BIRDNET_MQTT_PORT | 1883, or 8883 with MQTT_TLS | Broker port |
MQTT_TLS | --mqtt-tls · BIRDNET_MQTT_TLS | off | Connect over TLS; the certificate is always verified, with no way to skip it |
MQTT_CA_FILE | --mqtt-ca-file · BIRDNET_MQTT_CA_FILE | — | PEM file of extra certificates to trust, for a broker behind a private or self-signed CA |
MQTT_CLIENT_CERT | --mqtt-client-cert · BIRDNET_MQTT_CLIENT_CERT | — | PEM file with this station’s client certificate, for a broker that requires mutual TLS (require_certificate true). Needs MQTT_CLIENT_KEY; implies MQTT_TLS. --doctor checks the files without connecting |
MQTT_CLIENT_KEY | --mqtt-client-key · BIRDNET_MQTT_CLIENT_KEY | — | PEM file with the private key for MQTT_CLIENT_CERT (PKCS#8, PKCS#1 or SEC1). Never quoted in an error |
MQTT_TLS_SERVER_NAME | --mqtt-tls-server-name · BIRDNET_MQTT_TLS_SERVER_NAME | the broker host | The name to check the broker’s certificate against, for a broker reached by IP |
BIRDWEATHER_THRESHOLD | — (the BirdWeather upload threshold field on the Settings page) | — | A confidence (0 to 1) a detection must reach to be uploaded to BirdWeather, on top of the station’s own threshold; the station’s log keeps everything it recorded. Unset or 0 uploads every detection the station records. A value above 1 (75 typed for 75 %) is refused, not clamped: the bar is then ignored (every detection is uploaded) and --doctor and the start-up log say so. It never affects the blur setting |
BIRDWEATHER_LOCATION_ACCURACY | — (the BirdWeather location blur field) | 0 (exact) | Metres, 0 to 50 000: every post carries the station’s position moved by one fixed offset (a distance up to this, in a direction, drawn once from a secret seed kept in birdweather-blur-seed beside the database), so the public map does not show the station’s address. The offset is the same on every post and after every restart, which is what stops someone averaging many posts back to the true spot; it is not hidden from anyone who learns it (the seed file, or another source that places the station), and changing the radius or losing the seed file draws a new one. A value that is set and cannot be used (5km, 60000, a negative number) turns BirdWeather uploads off rather than posting the exact position; --doctor and the start-up log say so |
EMBED_ORIGINS | — (the Sites that may embed this station field) | — (nobody) | Comma-separated origins (http://homeassistant.local:8123, https://abcd1234.ui.nabu.casa, https://*.example.com) allowed to show the station in an <iframe>. Anything else, * included, and a wildcard over a suffix strangers register under (*.ui.nabu.casa, *.github.io, *.co.uk) is refused and then no site may embed. Only /, /kiosk and /recordings can be framed. See Embedding |
MQTT_USERNAME | --mqtt-username · BIRDNET_MQTT_USERNAME | — | Broker user name |
MQTT_PASSWORD | --mqtt-password · BIRDNET_MQTT_PASSWORD, or BIRDNET_MQTT_PASSWORD_FILE | — | Broker password. Prefer the file form: a flag is visible in ps |
MQTT_TOPIC_PREFIX | --mqtt-topic-prefix · BIRDNET_MQTT_TOPIC_PREFIX | birdnet | Detections go to {prefix}/detection/{species}, the heartbeat to {prefix}/status |
TLS_CERT | --tls-cert · BIRDNET_TLS_CERT | — | Certificate chain (PEM) for TLS_MODE=manual |
TLS_KEY | --tls-key · BIRDNET_TLS_KEY | — | Private key (PEM) for TLS_MODE=manual |
TLS_DIR | --tls-dir · BIRDNET_TLS_DIR | a tls folder beside the database | Where the generated CA and server certificate are kept |
PRIVACY_RANK | --privacy-rank · BIRDNET_PRIVACY_RANK | 10 | BirdNET-Pi’s human-sound rule; 0 = off, up to 12000. On by default. A field on the Settings page (Privacy Rank) |
WATCHDOG_CHECK_SECS | BIRDNET_WATCHDOG_CHECK_SECS | 2 (1–300) | How often every capture source is checked |
WATCHDOG_STALL_SEGMENTS | BIRDNET_WATCHDOG_STALL_SEGMENTS | 4 (2–60) | Segments of missing output before a running source is called stalled |
WATCHDOG_STALL_FLOOR_SECS | BIRDNET_WATCHDOG_STALL_FLOOR_SECS | 120 (10–3600) | Floor under that stall threshold |
WATCHDOG_BACKOFF_BASE_SECS | BIRDNET_WATCHDOG_BACKOFF_BASE_SECS | 2 (1–60) | First retry delay after a source dies |
WATCHDOG_BACKOFF_CAP_SECS | BIRDNET_WATCHDOG_BACKOFF_CAP_SECS | 60 (2–3600) | Ceiling on the retry delay; there is no retry limit |
WATCHDOG_DOWN_WARN_AFTER_SECS | BIRDNET_WATCHDOG_DOWN_WARN_AFTER_SECS | 120 (10–86400) | How long a source may be down before the loud journal warning |
WATCHDOG_DOWN_WARN_EVERY_SECS | BIRDNET_WATCHDOG_DOWN_WARN_EVERY_SECS | 300 (30–86400) | How often that warning repeats |
A watchdog value outside its range is clamped and the adjustment is logged; one that is not a number falls back to the default. The ranges and defaults are in src/capture/watchdog.rs.
--offsite-backup accepts off, s3, sftp and rsync: the CLI help below says “off, s3 or sftp”, which predates the fourth. See Backups & Recovery.
Environment variables — .env.example
# =============================================================================
# BirdNet-Behavior — Docker environment variables
#
# QUICK START
# 1. cp .env.example .env
# 2. Uncomment and fill in the REQUIRED section below (lat/lon + one
# audio source). See "why some keys are commented out" below.
# 3. docker compose up -d ← base compose, works for RTSP/file-watch
# …or an overlay for live mic capture:
# docker compose -f docker-compose.yml -f docker-compose.alsa.yml up -d
# docker compose -f docker-compose.yml -f docker-compose.pulse.yml up -d
# 4. Open http://localhost:8502
#
# The classifier is installed automatically on first run. A new data volume gets
# BirdNET V2.4 (~85 MB with its range filter) — the network BirdNET-Pi and
# birdnet-go run, so detections agree with theirs — installed by the image's own
# `birdnet-behavior --install-model`, which checks every file's sha256 and size
# before it keeps it. The BirdNET+ V3.0 preview (~560 MB) is an explicit opt-in:
# BIRDNET_CLASSIFIER=birdnet-v3 (see "Classifier" below). A volume that already
# runs the V3.0 preview keeps running it. You do not need to fetch a model
# yourself.
#
# Settings with no env var (detection confidence threshold, sensitivity,
# per-species thresholds, email, rare-bird quarantine) live in the web UI at
# http://localhost:8502/admin/settings. Their defaults are BirdNET-Pi's —
# confidence 0.70, sensitivity 1.25 — and are what a BirdNET V2.4 station should
# run; they are not set here, so they follow the build.
#
# WHY SOME KEYS BELOW ARE COMMENTED OUT
# Uncomment a key to use it, and always give it a value. A key left in place
# with an empty value is NOT the same as leaving it unset: the daemon's
# argument parser reads a blank environment variable as a *supplied* value,
# so `BIRDNET_LATITUDE=` means "the latitude is the empty string" and refuses
# to start rather than falling back to the default. Every optional key is
# therefore shipped commented; `docker/entrypoint.sh` strips blanks as a
# backstop, but `docker compose exec … --doctor` reads this file's values
# directly and gets no such protection.
#
# A key that the web UI also sets is commented out for a second reason: an
# environment variable counts as an explicit flag and wins over
# /admin/settings for good. Set one here only to pin the value — the
# settings page then shows your choice and silently runs this one.
# =============================================================================
# =============================================================================
# REQUIRED — edit these three things before first run
# =============================================================================
# --- 1. Station location -----------------------------------------------------
# Used for the solar recording schedule, species frequency filter,
# BirdWeather uploads, and the dashboard sunrise/sunset markers.
#
# Format: decimal degrees (NOT degrees-minutes-seconds). Negative numbers
# mean South latitude or West longitude. Easiest way to find your numbers:
# 1. Open https://www.openstreetmap.org/
# 2. Right-click your house / garden / station location
# 3. Choose "Show address" — the two numbers at the top are lat,lon.
#
# Examples:
# Boston, MA, USA → BIRDNET_LATITUDE=42.3601 BIRDNET_LONGITUDE=-71.0589
# Berlin, Germany → BIRDNET_LATITUDE=52.5200 BIRDNET_LONGITUDE=13.4050
# Sydney, Australia → BIRDNET_LATITUDE=-33.8688 BIRDNET_LONGITUDE=151.2093
#
# Validation: latitude must be between -90 and 90, longitude between -180
# and 180. Run `birdnet-behavior --doctor` after editing to verify.
#BIRDNET_LATITUDE=
#BIRDNET_LONGITUDE=
#
# Timezone, as an IANA name (Europe/Berlin, America/New_York, Australia/
# Sydney). Detections are filed under the container's local hours, and
# those come from this. Unset, the station runs on UTC and every hour on
# the dashboard is a UTC hour; a name the image does not know also means
# UTC, and the entrypoint and `--doctor` say so.
#TZ=
# --- 2. Audio source — set exactly ONE of the following ---------------------
# Option A — USB / ALSA microphone (most Raspberry Pi users)
# Find the device: arecord -l
# → "card 1: Device [USB Audio], device 0:" → plughw:1,0
# Start with:
# docker compose -f docker-compose.yml -f docker-compose.alsa.yml up -d
#BIRDNET_ALSA_DEVICE=
# Several microphones at once: a comma-separated list, each analysed and
# recorded as its own source (the single-device key above is then ignored).
#BIRDNET_ALSA_DEVICES=
# Option B — RTSP stream (IP camera / network audio)
# Works with the base docker-compose.yml — no overlay needed.
# docker compose up -d
#BIRDNET_RTSP_URL=
# Option C — PulseAudio / PipeWire source (desktop Linux)
# Use "default" for the system default source, or a name from:
# pactl list short sources
# Start with:
# docker compose -f docker-compose.yml -f docker-compose.pulse.yml up -d
#BIRDNET_PIPEWIRE_DEVICE=
# --- 3. Which image to pull --------------------------------------------------
# latest — the current release. DuckDB behavioral analytics (activity
# sessions, resident-vs-migrant, dawn-chorus validation, …) is
# built into EVERY image and on by default — there is no separate
# "-analytics" tag. Pin a version in production, e.g. 0.5.0.
BIRDNET_IMAGE_TAG=latest
# =============================================================================
# OPTIONAL — safe defaults, edit only if you want to change behaviour.
# You can leave this entire section untouched and the container will work.
# =============================================================================
# --- Web server --------------------------------------------------------------
# Host port for the web UI (container always listens on 8502 internally).
BIRDNET_PORT=8502
# Custom site name shown in the web UI header.
#BIRDNET_SITENAME=
# Species info link target: ebird | allaboutbirds | none
#BIRDNET_INFO_SITE=ebird
# Web UI authentication (a session-cookie login at /login). Viewing the dashboard is always open; only
# the /admin panel (settings, backups, software update) is gated by a password.
# quickstart.sh and the bare-metal installer generate one. A hand-made .env does
# not, so until you set CADDY_PWD here, /admin is open to the whole LAN
# (BIRDNET_LISTEN below binds 0.0.0.0). Set a password, or front it with a
# reverse proxy / VPN. You sign in as "admin"; CADDY_USER only adds a second
# name for the same account. See docs/book/field/hardening.md.
CADDY_USER=
CADDY_PWD=
# Private mode: put the WHOLE station behind the sign-in, not only /admin. On a
# station reachable through a tunnel or a port forward, the open dashboard is
# the detection history and a live microphone feed for anyone with the URL.
# With this set, everything needs a session except the sign-in form, its
# assets, the health probe and what BIRDNET_PUBLIC_ACCESS names. Needs
# CADDY_PWD: a private station with no password answers 503 rather than
# falling open. Config file: PRIVATE_MODE.
#BIRDNET_PRIVATE_MODE=true
# What stays open on a private station, comma-separated: live_audio (the
# /stream endpoint, its HLS variant under /api/v2/streams/hls/ and the live
# spectrogram), share (operator-minted /r/<token>
# links) and metrics (/api/v2/metrics, for a scraper with no cookie). No effect
# without private mode. Config file: PUBLIC_ACCESS.
#BIRDNET_PUBLIC_ACCESS=share,metrics
# --- HTTPS -------------------------------------------------------------------
# How the dashboard terminates TLS: off | self-signed | manual
#
# off Plain HTTP (the default, and what every earlier release did).
# Fine on a trusted LAN, or behind a reverse proxy that already
# terminates TLS.
# self-signed Generates a small local CA and a server certificate it signs,
# under BIRDNET_TLS_DIR. Browsers warn until you import the CA
# file once (the startup log prints its path); a later
# certificate rotation does not bring the warning back.
# manual Serves BIRDNET_TLS_CERT / BIRDNET_TLS_KEY. Both are re-read
# when they change on disk, so an ACME client renewing them at
# 03:00 is picked up without a restart.
#
# `--doctor` checks all of this before startup does.
#BIRDNET_TLS_MODE=off
# Certificate chain and private key (PEM), for BIRDNET_TLS_MODE=manual.
#BIRDNET_TLS_CERT=/etc/ssl/birdnet/fullchain.pem
#BIRDNET_TLS_KEY=/etc/ssl/birdnet/privkey.pem
# Where HTTPS listens. Defaults to BIRDNET_LISTEN's host on port 8503, so plain
# HTTP keeps answering on 8502. Set this equal to BIRDNET_LISTEN to serve only
# HTTPS on the one port.
#BIRDNET_TLS_LISTEN=0.0.0.0:8503
# Answer plain HTTP with a 308 redirect to HTTPS instead of serving the app.
# Ignored (with a warning) when HTTPS and HTTP share one socket.
#BIRDNET_TLS_REDIRECT=1
# Names the generated certificate covers, comma-separated. Defaults to
# localhost, this machine's hostname, <hostname>.local, the bound address and
# 127.0.0.1. Changing this regenerates the server certificate (not the CA).
#BIRDNET_TLS_HOSTNAMES=birds.example.com,192.168.1.40
# Where the generated CA and server certificate live. Defaults to a `tls`
# directory beside the database.
#BIRDNET_TLS_DIR=/data/tls
# How many days a generated server certificate is valid for. The CA that signs
# it lives ten years regardless, so rotation never invalidates a trust import.
#BIRDNET_TLS_DAYS=397
# Extra seconds of audio before each detection in its saved clip.
#
# A clip is centred on its detection: with the default 6-second EXTRACTION_LENGTH
# and BirdNET's 3-second window that is 1.5 seconds either side. This adds to
# the front only, so 1.0 makes the lead-in 2.5 seconds and the clip 7 seconds.
#
# Worth having because a call is not centred in the window that detects it. A
# bird that started singing half a second before the analysed chunk opened has
# its first notes in the previous one — audible to a person deciding whether the
# identification is right, and exactly what an asymmetric lead-in recovers.
#
# Clips now span segment boundaries, so this reaches real audio even for a
# detection at the very start of a segment. It is refused across a gap in
# recording: a station that restarted, or a source that dropped, gives a shorter
# clip rather than one splicing two different times together.
#
# 0 (the default) keeps clips centred, exactly as before.
# Config-file key only: the daemon reads PRE_CAPTURE_SECS from birdnet.conf, not
# from the environment, so it has no effect in Docker (which ships no config file).
#PRE_CAPTURE_SECS=1.0
# Species tracking: first of the year, first of the season, returning.
#
# The season a date falls in depends on the station's latitude — March is
# spring in the north and autumn in the south, and near the equator there are
# wet and dry seasons rather than four temperate ones. Set BIRDNET_LATITUDE (or
# the latitude in Settings) and the station works this out; leave it unset and
# it reports no season at all rather than guessing a northern one.
#
# The tracking year defaults to the calendar year. A station that counts its
# year from the start of the breeding season can move the reset, and a date
# before it then belongs to the tracking year that began the previous time
# around. These are settings-table keys rather than environment variables
# because they belong with the station's other operator settings:
#
# tracking_year_reset_month 1-12, default 1
# tracking_year_reset_day 1-31, default 1
#
# "Returning after an absence" shares its threshold with the rare-species feeds
# (rare_species_days, default 30) so the RSS feed and the day list cannot
# disagree about which birds are notable.
#
# Read it at GET /api/v2/species/tracking?date=YYYY-MM-DD[¬able_only=true].
# Dynamic per-species confidence threshold. Off by default.
#
# A fixed confidence threshold settles two different questions with one number:
# "is this a bird at all" and "is this bird plausible here". Once a Tawny Owl
# has been recorded in the wood at 0.9, a later 0.45 Tawny Owl is very probably
# another Tawny Owl; a 0.45 for a species never recorded within 500 km is not.
# Raising the threshold to suppress the second suppresses the first as well.
#
# When enabled, a species is confirmed by a detection at or above the trigger
# that survived EVERY other gate — the privacy and noise filters, the species
# occurrence (geomodel) filter, the plausible-hour filter, the corroboration
# filter, and the threshold itself. Each confirmation lowers that species'
# threshold one step (x0.75, x0.50, x0.25), floored, and the level lapses after
# the configured hours without reinforcement. A single singing bout counts once:
# a species can advance at most one level per fifteen minutes.
#
# Enabling this also lowers the threshold the classifier itself runs at, to the
# floor below — otherwise the detections it exists to recover are discarded
# inside the model and the feature reaches nothing. The extra cost is in
# post-processing, not inference.
#BIRDNET_DYNAMIC_THRESHOLD=1
# Confidence at or above which a surviving detection confirms its species.
# Should sit well above the global threshold: the point is that the station was
# sure, not merely that it accepted the detection. Default 0.80.
#BIRDNET_DYNAMIC_THRESHOLD_TRIGGER=0.80
# Hard floor. No species goes below this whatever its level. This is the number
# that bounds the damage if the reasoning above is wrong for your site.
# Must be BELOW the global confidence or nothing can be adjusted; the station
# warns at startup if it is not. Default 0.25.
#BIRDNET_DYNAMIC_THRESHOLD_MIN=0.25
# How long a level survives without reinforcement, in hours. Default 24.
#BIRDNET_DYNAMIC_THRESHOLD_HOURS=24
# Sound-level calibration, in dB SPL at digital full scale.
#
# Unset means the station reports dBFS: negative, relative to its own gain, and
# not comparable with another station or a published figure. That is fine for
# every question about change at one place over time — is it getting louder, is
# the microphone dying, when does the dawn chorus start — which is what the
# third-octave series is for.
#
# To calibrate, play a known level at the microphone (a 94 dB SPL acoustic
# calibrator at 1 kHz is the usual instrument), read what /api/v2/soundlevel
# reports for the 1 kHz band uncalibrated, and set this to
# 94 minus that reading. Readings then come back as dB SPL and say so; the unit
# travels with the data, and each stored hour records the offset in force when
# it was measured, so a series that spans a calibration has a visible seam
# rather than a silent one.
#BIRDNET_SPL_CALIBRATION_DB=
# Reverse proxies whose forwarded client-IP headers may be believed.
#
# Comma-separated CIDR blocks, bare addresses, and the reserved names
# `loopback`, `private` and `cloudflare`. Loopback is always trusted whether or
# not it is named — a proxy on the same host is not a stranger.
#
# The default is `private`: loopback plus RFC1918, unique-local and link-local.
# That covers the two common topologies (a proxy on this host, a proxy on
# another box on the LAN) and is why a proxied station records each visitor
# separately rather than filing the whole household under the proxy's address.
#
# When the peer is NOT in this list, forwarded headers are ignored completely
# and the connection's own address is used. That is what stops a directly
# exposed station from being told who its callers are by the callers.
#
# Set `loopback` to trust nothing but this machine. Add `cloudflare` when the
# station sits behind Cloudflare, so the right-to-left walk of
# `X-Forwarded-For` passes the edge and reaches the real visitor instead of
# stopping at one of a few hundred Cloudflare addresses. Naming it is also
# what makes `CF-Connecting-IP` believed: only Cloudflare overwrites that
# header, and Caddy or nginx pass a visitor's own copy straight through, so
# without the name it is ignored. (A Cloudflare Tunnel works either way — the
# edge appends the visitor to `X-Forwarded-For`.)
#
# A value that does not parse is not ignored: it is logged and the station
# falls back to loopback only.
#BIRDNET_TRUSTED_PROXIES=private
#BIRDNET_TRUSTED_PROXIES=private,cloudflare
#BIRDNET_TRUSTED_PROXIES=loopback
#BIRDNET_TRUSTED_PROXIES=10.0.0.0/8,192.168.1.5
# Host names that may use the admin panel while no admin password is set.
#
# A station with no password lets anyone on its network into /admin. It only
# does that for names nobody outside your network can point at it: an IP
# address, `localhost`, a one-word name like `birdnet`, and names ending in
# .local, .lan, .home, .home.arpa, .internal or .localdomain. Any other name —
# one somebody registered — is refused, because DNS rebinding would otherwise
# let any website you visit operate the station through your browser.
#
# Setting an admin password is the better fix and makes this setting moot. If
# you reach an open station by your own domain and want that to keep working,
# list it here (comma-separated, no port):
#BIRDNET_ALLOWED_HOSTS=birds.example.org
# Serve the station from under a path, e.g. https://home.example/birdnet
#
# For the common home setup: one hostname, a reverse proxy, several services
# under different paths. Without this the station needs its own subdomain or
# its own port. Home Assistant ingress works this way too.
#
# Empty (the default) serves from the root and changes nothing.
#
# The prefix is applied to every URL the station emits — links, form actions,
# HTMX targets, redirects, the session cookie's Path, and the WebSocket URLs
# the live pages build in the browser. Your proxy should pass the full path
# through *unchanged* rather than stripping it: the station expects to see
# /birdnet/today, not /today.
#
# Caddy: handle_path is wrong here; use `handle /birdnet/*` and
# `reverse_proxy localhost:8080` with no path rewriting.
# nginx: location /birdnet/ { proxy_pass http://127.0.0.1:8080; }
# (note: no trailing path on proxy_pass, or nginx strips the prefix)
#
# A visitor who reaches the host root is redirected to the prefix, so a
# mistyped bookmark lands on the station rather than on a 404.
#
# A value that does not parse (a `..` segment, whitespace, a query string) is
# refused and logged, and the station serves from the root — it is not
# silently corrected, because a corrected prefix will not match what the proxy
# sends and every request then 404s with nothing in the log to say why.
#BIRDNET_BASE_PATH=/birdnet
# Cross-origin (CORS) allow-list. Empty = same-origin only (the secure default).
# Set a comma-separated list of origins ONLY if you call the API from another
# host, e.g. https://dashboard.example.com
#BIRDNET_CORS_ALLOWED_ORIGINS=
# Secret used to sign cookie sessions on the new branded login page (O-14).
# Set a random value of 32+ bytes so sessions survive restarts and process
# moves. If unset, the secret is derived deterministically from CADDY_PWD
# (rotating the password signs out every existing session — the intended
# semantics). If both are unset, a per-process random secret is used and
# every outstanding session invalidates on restart. Generate one with:
# openssl rand -base64 48
BNB_SESSION_SECRET=
# Cookie session lifetime in days, default 14, range 1-365. "Remember me"
# extends the issued cookie to 90 days regardless of this value.
BNB_SESSION_TTL_DAYS=14
# Where the rendered manual lives, for the in-app help drawer. Unset, the
# binary looks beside itself and under docs/book/_generated/html.
#BNB_HELP_DIR=
# Token for the token-gated /api/v2 endpoints (O-1): eighteen of them, ten
# writes and eight reads (the lists are WRITE_ROUTES and READ_ROUTES in
# crates/birdnet-web/src/routes/api_write.rs; the manual's API page names them).
# Leave this blank and the station has NO write API at all: every one of the
# eighteen answers 404, which is the default and the safe one. Set it and they
# accept `Authorization: Bearer <this value>` — which is how Home Assistant,
# Node-RED or a shell script confirms a detection, locks a clip against the
# purge, deletes a misidentification, changes a setting (PUT /api/v2/settings)
# or restarts the station (POST /api/v2/control/restart).
#
# This is NOT the /admin password and does not inherit its behaviour: /admin is
# open when CADDY_PWD is unset, the write API is closed when this is unset.
# Anything shorter than 32 bytes is refused and leaves the write API off, with a
# warning in the log. Generate one with:
# openssl rand -base64 48
BNB_API_TOKEN=
# Absolute base URL of this station (e.g. https://birds.example.com). When
# the URL starts with https://, the session cookie is issued with Secure;
# on a plain-HTTP LAN this stays off so browsers send the cookie back.
# Independent of BNB_BASE_URL below: the cookie reads only this one. The links
# in push notifications ($listenurl, $friendlyurl, $image) read this first and
# then BNB_BASE_URL, and fall back to http://<hostname>.local:<port>.
BNB_PUBLIC_URL=
# --- Sharing, feeds & station coordinates (web features) ---------------------
# Secret used to sign public share links (/r/<token>) with HMAC-SHA256. Set a
# random value of 32+ bytes. If left blank the key is derived from the station's
# own session secret (random, persisted beside the database), so links survive
# restarts either way; set this only to rotate share links independently.
# Generate one with: openssl rand -base64 48
BNB_SHARE_SECRET=
# Absolute base URL of this station as seen by the outside world. Used to build
# absolute links in the RSS/iCal feeds and share pages (e.g. http://birdnet.local
# or https://birds.example.com). Defaults to http://localhost:8502 if unset.
BNB_BASE_URL=
# Station latitude/longitude used for the dawn-chorus sunrise/sunset overlay on
# /analytics/dawn-chorus. Falls back to BIRDNET_LATITUDE/LONGITUDE, then to a
# conservative 05:30/20:00 if neither is set. Same values feed the weather
# poll loop below — locale-tolerant (42,3601 works the same as 42.3601).
BNB_STATION_LAT=
BNB_STATION_LON=
# --- Weather overlay (O-23) --------------------------------------------------
# Off by default — a fresh install does NOT contact api.open-meteo.com until
# you opt in. Set BNB_WEATHER_ENABLED=1 to start a background task that pulls
# the hourly forecast every 30 minutes and paints it as a signal-context band
# on the day-strip and dawn-chorus charts. The moon-phase badge is always
# shown regardless (it's a local calculation).
#
# Privacy-sensitive operators can self-host Open-Meteo and point the client
# at it via BNB_WEATHER_BASE_URL (e.g. http://open-meteo.lan); the upstream
# default is the free api.open-meteo.com tier.
BNB_WEATHER_ENABLED=
BNB_WEATHER_BASE_URL=
#
# WHERE THE WEATHER COMES FROM (G-26). Four choices, set in birdnet.conf as
# WEATHER_PROVIDER (there is no env var; it is a config-file key):
#
# open-meteo the default, and what you get if the key is absent. Free, no
# key, self-hostable.
# met-no the Norwegian Meteorological Institute's Locationforecast.
# Also keyless, and a better model over Europe.
# wunderground YOUR OWN personal weather station. Needs WEATHER_STATION_ID
# and WEATHER_API_KEY. This is the interesting one: an
# anemometer ten metres from the microphone is ground truth,
# and a far better covariate for bird activity than any
# gridded forecast.
# pirate-weather Pirate Weather's hourly forecast (api.pirateweather.net).
# Needs WEATHER_API_KEY; a free key is available from them.
#
# A provider name nobody implements does not start the poll at all, rather than
# quietly serving you a forecast you did not ask for.
#
# The API key can be mounted from a file instead of set here, like the other
# credentials -- see the mounted-secrets block further down.
# --- eBird corroboration (G-27) ------------------------------------------------
# "Has anybody else seen this bird around here lately?" eBird's recent
# observations are the best available answer, and a much better one than the
# range model, which is a static climatological prior with no idea that the
# first migrant of the year arrived last Tuesday.
#
# Used in two places: the suspect-species report under Settings > Data marks a
# flagged species that somebody reported nearby -- a strong reason NOT to
# exclude it -- and a detection's detail page says so too.
#
# OFF unless EBIRD_API_KEY is set. There is no enable flag: eBird needs a key
# for every endpoint, so the key is the opt-in, and a station without one
# never contacts eBird at all. Get a free key at
# https://ebird.org/api/keygen
#
# These are birdnet.conf keys, not environment variables. EBIRD_API_KEY can
# also be mounted from a file -- see the mounted-secrets block further down.
#
# EBIRD_API_KEY your eBird API token. Absent = the whole feature is off.
# EBIRD_REGION optional. An eBird region code (US-MA, GB-ENG-CAM). Leave
# it empty and the station's own LATITUDE/LONGITUDE are used
# instead, which is the better default: "reported within
# 25 km" says far more than "reported somewhere in this
# state". Set it if your area genuinely has no eBird
# observers within 50 km -- a region will find some.
# EBIRD_DIST_KM search radius when using coordinates. 0-50, default 25.
# EBIRD_BACK_DAYS how many days back to ask about. 1-30, default 14.
#
# Your coordinates are sent rounded to two decimal places (about a
# kilometre), which is all eBird accepts and all it needs.
#
# A species eBird says nothing about is shown no differently from before.
# That silence is not evidence: eBird coverage follows birdwatchers, not
# birds, so a quiet valley has no checklists for any species.
#
# BNB_EBIRD_BASE_URL overrides eBird's host, for a caching proxy in front of
# it. Leave it empty for api.ebird.org.
BNB_EBIRD_BASE_URL=
# --- Multi-stream RTSP (overrides BIRDNET_RTSP_URL) --------------------------
# Comma-separated; each URL gets its own capture pipeline.
#BIRDNET_RTSP_URLS=
# --- Network / privacy ---------------------------------------------------------
# Two connections are made with no configuration: a daily release check against
# api.github.com, and Wikipedia species-image downloads. Everything else is off
# until you configure it. See docs/book/getting-started/configuration.md.
#
# Any non-empty value turns off BOTH of the above, so the station makes no
# outbound connection you did not ask for. Integrations you configured
# explicitly (Apprise, BirdWeather, MQTT, SMTP, heartbeat, weather) keep working.
#BIRDNET_OFFLINE=
# Non-empty = skip only the daily release check.
#BIRDNET_NO_UPDATE_CHECK=
# --- Recording schedule ------------------------------------------------------
# Seconds per captured segment, range 3–600 (default 15 is a good balance).
#BIRDNET_SEGMENT_DURATION=15
# Schedule mode:
# all-day — capture audio 24/7 (default)
# solar — sunrise to sunset only (needs lat/lon)
# fixed:HH:MM-HH:MM — local-time window (e.g. fixed:06:00-20:00)
#BIRDNET_RECORDING_SCHEDULE=all-day
# Non-empty value = suppress recording during night hours (needs lat/lon).
#BIRDNET_NIGHT_INHIBIT=
# Minutes offset from sunrise/sunset for twilight recording. Applies to both
# ends of the day unless one of the two below overrides it for its own end.
BIRDNET_TWILIGHT_OFFSET=30
# Set either of these to give dawn and dusk different windows — the dawn chorus
# usually wants a longer lead-in than the evening needs a tail. Unset falls back
# to BIRDNET_TWILIGHT_OFFSET. Both are also editable at /admin/settings.
#BIRDNET_PRE_SUNRISE_OFFSET=
#BIRDNET_POST_SUNSET_OFFSET=
# --- Classifier --------------------------------------------------------------
# Which network the container runs:
#
# birdnet-v24 BirdNET V2.4 + its range filter v2. The default for a NEW data
# volume. The network BirdNET-Pi and birdnet-go run (48 kHz, 3 s
# windows); with the settings below at their defaults, its
# detections agree with BirdNET-Pi's. Labels carry BirdNET-Pi's
# English names. Installed by `birdnet-behavior --install-model`.
# birdnet-v3 BirdNET+ V3.0 preview + the BirdNET geomodel. A different
# network (4.5 s windows at 32 kHz, calibrated probabilities,
# ~11 500 species): its detections differ from BirdNET-Pi's by
# design. ~560 MB.
#
# Left unset, a volume that already holds the V3.0 model keeps running it: an
# image upgrade must never swap the network under a running station's history
# (the container log says so once, at start). Set birdnet-v24 to move such a
# volume on purpose — earlier detections stay in the database and were scored by
# the other network. The two occurrence filters are never mixed: V2.4 gets the
# range filter, V3.0 the geomodel. Read once by the entrypoint; the daemon never
# sees it.
#BIRDNET_CLASSIFIER=birdnet-v24
# --- Detection / inference ---------------------------------------------------
# Note: confidence threshold and sensitivity are configured in /admin/settings
# (defaults 0.70 and 1.25, BirdNET-Pi's).
# Analysis window overlap, 0.0–2.9 seconds (higher = more sensitive, more CPU).
BIRDNET_OVERLAP=0.0
# Seconds of audio saved around each detection (BirdNET-Pi: EXTRACTION_LENGTH).
# Also editable at /admin/settings. 1-60; longer clips verify better by ear and
# cost proportionally more disk.
#BIRDNET_EXTRACTION_LENGTH=6
# --- Logging ---------------------------------------------------------------
# Global level: trace, debug, info, warn, error, off. RUST_LOG overrides this.
#BIRDNET_LOG_LEVEL=info
# Per-subsystem levels, e.g. `audio=debug,web=warn`. Names are operator-facing:
# audio, detection, web, db, integrations, analytics. An explicit Rust path
# (birdnet_core::inference=trace) also works. Unknown names are reported at
# startup rather than ignored.
#BIRDNET_LOG_MODULES=
# --- Species occurrence filtering -------------------------------------------
# The occurrence ("geo") filter drops species that do not occur near the station
# at this time of year. Each classifier has its own, and they are never mixed:
#
# BirdNET V2.4 (the default) -> the range filter v2, installed on first start
# by `birdnet-behavior --install-model birdnet-v24-range-v2`. It is indexed
# like the V2.4 labels, so it needs coordinates and the one file
# (BIRDNET_METADATA_MODEL) and NO label file: leave BIRDNET_METADATA_LABELS
# unset with it, and do not point V2.4 at the geomodel or V3.0 at this.
# BirdNET+ V3.0 (the preview) -> the geomodel, below.
#
# The geomodel needs THREE things and is inert without all three:
#
# 1. station coordinates (BNB_STATION_LAT / BNB_STATION_LON, or the dashboard)
# 2. the geomodel file -> BIRDNET_METADATA_MODEL
# 3. that model's own label file -> BIRDNET_METADATA_LABELS
#
# For V3.0 the entrypoint downloads (2) and (3) on first start and sets both
# variables for you; for V2.4 it installs the range filter and sets
# BIRDNET_METADATA_MODEL. Leave them blank unless you are staging your own. Set
# BIRDNET_METADATA_MODEL yourself and the entrypoint leaves the geomodel
# entirely alone; set only BIRDNET_METADATA_LABELS and it keeps your label file
# beside the geomodel it downloads. If the ~14 MB download fails the container still starts and
# detects; it just does not filter by location, and says so.
# `birdnet-behavior --doctor` reports which of the three is missing.
#
# The geomodel and the classifier do NOT score the same species list (12 012
# against the V3.0 Global 11K classifier's 11 560), so the label file is what
# maps one to the other and the two are matched by scientific name. Leave it
# blank only for a metadata model indexed identically to the classifier -- a
# matched BirdNET pair -- which the station verifies at startup and refuses if
# it does not hold. The label file also carries each species' eBird code, which
# the species page's "View on eBird" link is built from; without it that link
# is not shown.
#BIRDNET_METADATA_MODEL=
#BIRDNET_METADATA_LABELS=
# Point the geomodel fetch at a mirror (air-gapped / bandwidth-limited sites):
#BIRDNET_GEOMODEL_BASE=
# ---------------------------------------------------------------------------
# Running more than one classifier (G-10 Stage 2)
# ---------------------------------------------------------------------------
#
# A second classifier is a second opinion. Two independent models agreeing on
# a species is far stronger evidence than one model being confident, and it is
# the honest way at the false-positive problem. It is also several hundred
# megabytes of RAM.
#
# The station refuses to load a classifier it cannot show will fit: all
# classifiers together may use at most HALF the smaller of physical RAM and
# any cgroup limit, counting each model's file size plus a 256 MiB working-set
# allowance. On a 1 GB board that budget is 512 MiB, which one BirdNET model
# already fills — so a second is skipped, with the arithmetic in the journal.
# A machine that does not report its memory gets one classifier, because
# "unknown" must not be read as "plenty".
#
# The primary classifier (BIRDNET_MODEL / BIRDNET_LABELS) is always loaded. It
# is what the station is.
#
# Name the primary, if you want it called something other than `birdnet` in
# routes and on detections:
#BIRDNET_MODEL_ID=birdnet
#BIRDNET_MODEL_THRESHOLD=
#BIRDNET_MODEL_SAMPLE_RATE=
# A second and third classifier. Each needs its OWN labels file: labels are
# positional against a model's output, so the wrong ones report species under
# other species' names with full confidence.
#BIRDNET_MODEL_2_PATH=/data/model/perch.onnx
#BIRDNET_MODEL_2_LABELS=/data/model/perch_labels.csv
#BIRDNET_MODEL_2_ID=perch
#BIRDNET_MODEL_2_THRESHOLD=
# The sample rate the classifier wants, when its tensor cannot say. Perch v2
# declares [-1, 160000], which is 5 s at 32 kHz and equally 3.33 s at 48 kHz —
# nothing in the shape distinguishes them, so it must be told.
#BIRDNET_MODEL_2_SAMPLE_RATE=32000
#BIRDNET_MODEL_3_PATH=
#BIRDNET_MODEL_3_LABELS=
#BIRDNET_MODEL_3_ID=
#BIRDNET_MODEL_3_THRESHOLD=
#BIRDNET_MODEL_3_SAMPLE_RATE=
# Which classifiers judge which audio source: `source:model`, or
# `source:model+model` for more than one, comma-separated. The source is its
# `audio_sources` row id, the same label GET /api/v2/system/capture lists.
#
# A source not named here uses the PRIMARY classifier — never nothing. A route
# naming a classifier that is not configured stops the station at startup
# rather than leaving that microphone silently unjudged for months.
#BIRDNET_MODEL_ROUTES=pond:perch,garden:birdnet+perch
#
# The two label files were frozen at different points in a moving taxonomy, so
# they do not always spell a species the same way: the geomodel writes
# `Leuconotopicus villosus` where the classifier writes `Dryobates villosus`,
# and both mean Hairy Woodpecker. A name that does not match exactly is tried a
# second way -- on the common name together with the specific epithet -- which
# on the pinned pair recovers 62 species, 41 of them birds, that were otherwise
# undetectable while the occurrence filter ran. `--doctor` reports the counts.
#
# For a pair that shares neither the genus nor the common name, point this at a
# tab-separated `legacy<TAB>canonical` file of your own. Blank lines and `#`
# comments are skipped; a malformed line is skipped rather than fatal, so a typo
# here cannot take the occurrence filter off a running station. No table ships:
# the obvious one (OpenFauna's aliases.json) is CC BY-SA 4.0 against this
# project's CC BY-NC-SA 4.0, and recovers none of the above in any case.
#BIRDNET_SPECIES_ALIASES=
# Occurrence threshold, range 0.0–1.0. No effect while the filter is off.
# Higher = stricter: only species highly likely at your station/season.
# Lower = looser: more rare/migrant species allowed through.
# Default 0.03 is BirdNET-Pi's recommended starting point.
BIRDNET_SF_THRESH=0.03
# Human-voice privacy filter, range 0.0–1.0.
# 0.0 = disabled (no privacy filtering — fastest).
# 0.02 = recommended starting point if voices are being captured.
# 0.10 = aggressive; may suppress some quiet bird calls too.
BIRDNET_PRIVACY_THRESHOLD=0.0
# BirdNET-Pi's human-sound rule (its filter_humans): drop a window, and the one
# either side, when a human class ranks within the model's top N classes. 10 is
# BirdNET-Pi's default and this station's; 0 turns the rank rule off. It is
# independent of BIRDNET_PRIVACY_THRESHOLD above, and either one suppresses.
#BIRDNET_PRIVACY_RANK=10
# Discard an audio chunk a dog barked in (0.0 = disabled, typical 0.5-0.8). A
# bark is broadband, and the classifier also scores whatever species it most
# resembles — so the bark becomes a bird that was never there.
BIRDNET_NOISE_THRESHOLD=0.0
# Non-bird label names to watch, comma-separated. Unset means Dog; an empty
# value means watch nothing. Others in the label set: Siren, Engine,
# Power tools, Fireworks, Gun, Environmental, Noise — the last two score highly
# on ordinary quiet recordings, so watching them suppresses a great deal.
#BIRDNET_NOISE_CLASSES=
#
# NOISE_REMEMBER_SECS (a birdnet.conf key, no env var): keep suppressing the
# species a bark produced for this many seconds after it. 0 = off, the default.
#
# Why: a dog that barks for a minute is a bark, a gap, a bark, a gap. In the
# gaps there is no Dog above threshold, so the filter above has nothing to fire
# on -- and the classifier, still hearing the tail and the room, produces the
# same phantom species. The filter silences the barks and lets the gaps
# through, which is backwards.
#
# It suppresses ONLY the species that were in the barking chunk, not every
# species. A blanket window would be a mute button: a dog barking through the
# dawn chorus would erase the chorus. A blackbird singing in the same gap is
# still recorded.
#
# Off by default because it does remove real detections whenever a real bird
# happens to be the species a dog resembles. Try 30-60 if a regular dog is
# filling your log with one phantom species.
#
# It reaches only to the end of the recording being analysed, not across
# segments: carrying it further would need state on a filter shared by every
# audio source, and a bark on one microphone must not silence a species on
# another.
# ---------------------------------------------------------------------------
# Offsite backups
# ---------------------------------------------------------------------------
# The weekly snapshot lives beside the database, on the same card. That covers
# a corrupt page and a bad import; it does not cover the card failing, the
# enclosure flooding, or the Pi being stolen. Set this and each snapshot is
# encrypted on the station and uploaded. Off by default.
#
# off | s3 (AWS, B2, R2, Wasabi, MinIO, Ceph, Garage) | sftp (any SSH host)
BIRDNET_OFFSITE_BACKUP=off
# The passphrase every backup is encrypted under. There is NO way to turn
# encryption off, and NO recovery without this: it is not stored anywhere, not
# derivable from the backup, and not known to anyone else. Write it down
# somewhere that is not this station. At least 12 characters.
#
# Deliberately no command-line flag: an argument is visible in `ps` to every
# user on the machine and is copied into the journal by systemd.
#BIRDNET_OFFSITE_PASSPHRASE=
# How many backups to keep at the destination. 0 keeps everything. Only files
# this station wrote are ever removed.
#BIRDNET_OFFSITE_KEEP=8
# --- S3-compatible ---------------------------------------------------------
#BIRDNET_OFFSITE_S3_ENDPOINT=https://s3.eu-west-2.amazonaws.com
#BIRDNET_OFFSITE_S3_BUCKET=my-birdnet-backups
#BIRDNET_OFFSITE_S3_PREFIX=stations/garden
#BIRDNET_OFFSITE_S3_REGION=eu-west-2
#BIRDNET_OFFSITE_S3_ACCESS_KEY=
#BIRDNET_OFFSITE_S3_SECRET_KEY=
# auto picks by endpoint: virtual-host for AWS, path for everything else.
#BIRDNET_OFFSITE_S3_ADDRESSING=auto
# --- SSH file server -------------------------------------------------------
# Key authentication only; passwords are disabled because a batch upload
# cannot answer a prompt and would hang until something killed it.
#BIRDNET_OFFSITE_SFTP_HOST=backup.example.net
#BIRDNET_OFFSITE_SFTP_PORT=22
#BIRDNET_OFFSITE_SFTP_USER=birdnet
#BIRDNET_OFFSITE_SFTP_DIR=/srv/backups/garden
#BIRDNET_OFFSITE_SFTP_IDENTITY=/var/lib/birdnet/ssh/id_ed25519
# Defaults beside the identity file.
#BIRDNET_OFFSITE_SFTP_KNOWN_HOSTS=/var/lib/birdnet/ssh/known_hosts
# `yes` or `accept-new`. There is no `no`: host key checking is what makes the
# upload go to your server rather than to whoever answers.
#BIRDNET_OFFSITE_SFTP_HOST_KEY_POLICY=yes
# --- How often a backup is taken --------------------------------------------
# weekly (the default) or daily. A birdnet.conf key, not an env var.
#
# Daily means losing at most a day rather than a week -- and seven times the
# offsite upload, which matters on a metered link. Weekly stays the default so
# an existing station's bandwidth does not change underneath it.
#
# This schedules the BACKUP only. The space reclaim (WAL checkpoint, free pages
# returned) is a separate weekly job and stays weekly whatever you set here:
# asking for a daily backup is not asking to rewrite the database file every
# day, which on an SD card is write endurance spent for nothing.
#
# Both jobs are visible at GET /api/v2/system/jobs.
# --- Over rsync, when the link is bad ---------------------------------------
# OFFSITE_BACKUP=rsync sends to the SAME server with the SAME settings above --
# only the program moving the bytes changes, so switching is one word.
#
# Use it when your uplink drops: rsync keeps what already arrived, so a
# transfer that dies at 90% of a 1.3 GB backup continues from there. Over SFTP
# the same backup starts again from zero.
#
# It is NOT faster for being "incremental". Backups are encrypted under a fresh
# random salt each time, so consecutive backups share no blocks at all -- not
# even when the database has not changed. Measured on 1 MiB: 1496/1497 blocks
# reusable in plaintext, 0/1498 once encrypted. The resume and the speed limit
# below are the reasons to use it.
#
# Needs both rsync and sftp installed: rsync moves the bytes, sftp makes the
# directory, lists it and deletes what retention drops.
#
# KiB/s. 0 or unset means no limit.
#BIRDNET_OFFSITE_RSYNC_BWLIMIT=512
# Require a species to be heard in more than one nearby analysis window before
# it is recorded: off, lenient, moderate, balanced, strict. A real bird sings
# across several windows; a classifier artefact usually fires once. Each level
# is a fraction of the windows within six seconds — 20%, 30%, 50%, 70% —
# rounded up.
#
# This does NOTHING without BIRDNET_OVERLAP: with no overlap a six-second
# neighbourhood is two 3-second windows, and 20% of two rounds to one, which
# every detection already meets. `--doctor` reports the overlap each level
# needs. Off by default because it removes rows.
BIRDNET_CONFIRMATION_LEVEL=off
# Suppress a repeat of the same species within this many seconds (0 = off).
# A 15-second recording is five 3-second chunks, so a bird singing throughout
# is recorded five times, and every count in the app is a row count. Off by
# default because switching it on changes how many rows a station records.
BIRDNET_DUPLICATE_INTERVAL_SECS=0
# Quarantine day birds heard in the middle of the night. Owls, nightjars,
# rails, bitterns and thick-knees are exempt by genus. Needs station
# coordinates. Detections are quarantined for review, never dropped, because
# the taxonomy is genus-level and cannot be complete.
BIRDNET_NIGHT_FILTER=0
# Minutes after sunset / before sunrise before the night window opens. Birds
# sing through dusk and start again well before sunrise.
BIRDNET_NIGHT_MARGIN_MINS=60
# Extra genera or scientific names always allowed at night, comma-separated.
# A station recording nocturnal flight calls (migrating thrushes and warblers
# calling overhead) should leave the filter off or name those genera here.
#BIRDNET_NIGHT_EXTRA_NOCTURNAL=
# Language common names are shown in (a display setting: the names stored with
# detections stay the classifier's). 35 languages besides English are built in:
# af ar ca cs da de es et fi fr hr hu id is it ja ko lt lv nl no pl pt ro ru sk sl
# sr sv th tr uk vi zh_CN zh_TW. A regional spelling (de_AT, pt-BR) works; plain
# `zh` does not. Also settable on /admin/settings as DATABASE_LANG.
BIRDNET_LANG=en
# --- Notifications -----------------------------------------------------------
# Delivered in-process, no Apprise installation involved. Apprise URL syntax,
# comma- or newline-separated. Natively supported schemes:
# discord:// slack:// tgram:// ntfy:// gotify:// pover:// json://
# (and ntfys:// gotifys:// jsons://)
#BIRDNET_NOTIFY_URLS=
# For any other scheme, Apprise is still used: either its API server...
#BIRDNET_APPRISE_URL=
# ...or the `apprise` CLI with a config file. When every URL in that file is
# one of the schemes above, the CLI is not invoked at all.
#BIRDNET_APPRISE_CONFIG=
# Ceiling on notifications per destination per minute; 0 disables it. The cap
# is about the services' limits, not ours: Discord allows 5 requests/second per
# webhook, Telegram ~30/second, and Pushover 10,000 messages a *month*.
BIRDNET_NOTIFY_RATE_PER_MINUTE=12
#BIRDNET_NOTIFY_CONFIDENCE=0.8
# Trigger mode: each | new-species | new-species-daily
#BIRDNET_NOTIFY_TRIGGER=each
#BIRDNET_NOTIFY_SPECIES_EXCLUDE=
#BIRDNET_NOTIFY_SPECIES_ONLY=
#BIRDNET_NOTIFY_TITLE_TEMPLATE=
#BIRDNET_NOTIFY_BODY_TEMPLATE=
# Weekly report day: monday | tuesday | … | sunday | disabled
#BIRDNET_WEEKLY_REPORT_SCHEDULE=monday
# --- Detection deadman (end-to-end "is it actually detecting?" watchdog) ------
# Hours of zero detections before a warning is logged and (if Apprise is
# configured) one alert is sent, with a recovery notice when birds return.
# A station that has never detected anything never alarms. 0 disables the
# alert but still exports the birdnet_detection_silence_seconds gauge.
# Raise it for genuinely sparse habitats. Default: 24.
BIRDNET_DEADMAN_HOURS=24
# --- Station health (the faults the deadman cannot see) ----------------------
# The deadman answers "is the station detecting at all?". This answers the
# faults a station keeps detecting straight through, so the deadman stays quiet:
# * an audio source down while others keep recording
# * the disk full enough that recordings are being purged to make room
# * the CPU at or above its throttling point (80 C on a Pi)
# * a backup or integrity check that has not completed in three weeks
# One alert per episode with a recovery notice when it clears, through the same
# Apprise notifier as the deadman, and only after three consecutive five-minute
# polls so a mic that re-enumerates or a momentary disk spike stays quiet.
# Default: true.
BIRDNET_STATION_HEALTH_ALERTS=true
# --- BirdWeather -------------------------------------------------------------
#BIRDNET_BIRDWEATHER_TOKEN=
# Self-hosted ingest endpoint override. Leave blank for the public
# app.birdweather.com map. Set to your own endpoint implementing the
# BirdWeather station API (only the host changes; the /stations/<token>/...
# path shape is preserved) to keep observation locations under your own
# governance — e.g. for rare or endangered species where a public map is a
# poaching risk. The offline queue and ordered replay come with it.
#BIRDNET_BIRDWEATHER_URL=
# --- MQTT (Home Assistant, Node-RED, Mosquitto, …) ---------------------------
#BIRDNET_MQTT_HOST=
# Leave unset for 1883, or 8883 when BIRDNET_MQTT_TLS is on.
#BIRDNET_MQTT_PORT=
BIRDNET_MQTT_CLIENT_ID=birdnet-behavior
# Connect over TLS. The certificate is always verified — there is no way to
# turn that off. Setting BIRDNET_MQTT_CA_FILE turns TLS on by itself.
#BIRDNET_MQTT_TLS=
# PEM file of extra trust anchors. For a broker behind a private CA this is the
# *CA's* certificate; for a self-signed broker it is the broker's own. Either
# way the broker's certificate must carry CA:FALSE — plain `openssl req -x509`
# sets CA:TRUE, which rustls refuses from a server.
#BIRDNET_MQTT_CA_FILE=
# For a broker that requires the station to present a certificate (mutual TLS):
# PEM files of the station's client certificate and its private key. Both or
# neither; either turns TLS on by itself, and `--doctor` checks the pair.
#BIRDNET_MQTT_CLIENT_CERT=
#BIRDNET_MQTT_CLIENT_KEY=
# Hostname to check the certificate against, when connecting by IP.
#BIRDNET_MQTT_TLS_SERVER_NAME=
#BIRDNET_MQTT_USERNAME=
#BIRDNET_MQTT_PASSWORD=
BIRDNET_MQTT_TOPIC_PREFIX=birdnet
#BIRDNET_MQTT_RETAIN=
#BIRDNET_MQTT_HA_DISCOVERY=
# --- Heartbeat (uptime monitoring ping URL) ----------------------------------
# Pinged every 5 minutes while the process runs, and once immediately at
# startup. Its ABSENCE is the alarm, so set your monitor's grace period to
# ~15 minutes (three missed pings). This says "the box is still there" and
# nothing else: "has it stopped detecting?" is DEADMAN_HOURS below.
#BIRDNET_HEARTBEAT_URL=
# --- Credentials as mounted files --------------------------------------------
# Each of the five variables below takes the *path* of a file whose contents
# become the value of the variable it is named after. This is how Docker
# (`secrets:` → /run/secrets/<name>) and Kubernetes (a projected secret volume)
# hand a credential to a process without putting it in the environment, where
# `docker inspect`, /proc/<pid>/environ and anything that logs its own
# environment can all read it.
#
# Rules, all of them reported in the journal at startup:
# * The direct value wins. Set both and the file is not read.
# * A file that cannot be read, or that is empty, leaves the feature OFF —
# and says so at error level, because a station silent for that reason
# looks exactly like one that was told to be quiet.
# * Leading and trailing whitespace is trimmed; a newline at the end of the
# file is expected and harmless. Inner newlines are kept, so a
# NOTIFY_URLS file may list one URL per line.
# * A value read from a file is NOT copied into the settings table, so it
# stays out of the database and out of every backup of it.
#BIRDNET_NOTIFY_URLS_FILE=/run/secrets/birdnet_notify_urls
#BIRDNET_APPRISE_URL_FILE=
#BIRDNET_BIRDWEATHER_TOKEN_FILE=
#BIRDNET_MQTT_PASSWORD_FILE=
#BIRDNET_HEARTBEAT_URL_FILE=
#BIRDNET_WEATHER_API_KEY_FILE=
#BIRDNET_EBIRD_API_KEY_FILE=
# --- Capture watchdog (expert) -------------------------------------------------
# Every source is supervised independently: a dead subprocess is restarted with
# capped exponential backoff, and one that is alive but has stopped writing
# segments is caught as a silent stall and restarted the same way. The timings
# below are that supervisor's, and the defaults are what the station has always
# used — leave them alone unless a specific site misbehaves. A value outside its
# range is clamped and the adjustment is logged, so a station never runs on
# timings its config file does not describe.
#
# NOTE: none of these is "how long may the station go without a detection" —
# that is BIRDNET_DEADMAN_HOURS above. A microphone writes segments through
# hours of silence, so no amount of quiet trips the stall threshold.
#
# There is deliberately NO maximum retry count. A field sensor unreachable for
# six hours must still be reachable on hour seven, and a supervisor that has
# given up is a station that is silently not recording.
#
# How often every source is reconciled, in seconds. 1-300.
#BIRDNET_WATCHDOG_CHECK_SECS=2
# Segment-durations of missing output before a live process is called stalled.
# 2-60. Raise it for a camera that legitimately pauses.
#BIRDNET_WATCHDOG_STALL_SEGMENTS=4
# Floor under the stall threshold, in seconds, so short segments do not make
# the verdict hair-triggered. 10-3600.
#BIRDNET_WATCHDOG_STALL_FLOOR_SECS=120
# First retry delay after a source is found dead, in seconds. 1-60.
#BIRDNET_WATCHDOG_BACKOFF_BASE_SECS=2
# Ceiling on the retry delay, in seconds. 2-3600. A ceiling below the first
# delay is raised to it — a shrinking backoff is nobody's intent.
#BIRDNET_WATCHDOG_BACKOFF_CAP_SECS=60
# How long a source may be down before the loud journal warning, in seconds.
# 10-86400.
#BIRDNET_WATCHDOG_DOWN_WARN_AFTER_SECS=120
# How often to repeat that warning, in seconds. 30-86400. This is also the
# repeat cadence for the "flapping" warning.
#BIRDNET_WATCHDOG_DOWN_WARN_EVERY_SECS=300
# --- Saved clips: loudness -----------------------------------------------------
# Normalise each saved clip to this integrated loudness (ITU-R BS.1770), in
# LUFS. Unset — the default — writes clips at capture level.
#
# What it fixes: a gallery of clips recorded at different levels sends the
# listener to the volume control between every one, and a quiet clip at the end
# of a playlist gets missed. Measuring each clip and applying one gain is the
# standard remedy.
#
# Applied to the SAVED CLIP ONLY. The audio the classifier hears is untouched,
# so this cannot move a confidence score or make two stations' thresholds mean
# different things.
#
# Useful values: -14 (loud, streaming), -18 (recommended), -23 (EBU R128
# broadcast). Anything outside -40..-6 is ignored with a log line rather than
# clamped, because a mistyped target should not quietly normalise every clip to
# a level nobody chose. Also settable from Settings -> Audio Capture.
#BIRDNET_CLIP_TARGET_LUFS=-18
# Sample peak no sample may pass after the gain, in dBFS. Must be in -12..0.
# This is a SAMPLE peak, not an ITU true peak: the station does not oversample
# to find inter-sample peaks, and the default leaves about a decibel of headroom
# for them. A clip that is quiet but peaky — one loud wing-beat over a distant
# song — is turned up only as far as this allows and does not reach the target.
#BIRDNET_CLIP_PEAK_CEILING_DBFS=-1
# --- Storage / retention -----------------------------------------------------
# wav | mp3 | flac | ogg (non-WAV needs ffmpeg or sox in the image)
#BIRDNET_AUDIO_FORMAT=wav
# Maximum extracted recordings kept per species (0 = unlimited).
BIRDNET_MAX_FILES_PER_SPECIES=0
# Reclaim the audio of detections older than this many days (blank/0 = keep
# audio forever, the default). Detection records are always kept — only the
# sound file goes — and locked clips are exempt.
#BIRDNET_CLIP_RETENTION_DAYS=
# Frequency shift applied to saved clips, in Hz (0 = disabled). A true shift:
# -3000 plays an 8 kHz song at 5 kHz and leaves the clip's length alone. Negative
# lowers, which is the direction that helps high-frequency hearing loss (try
# -3000); positive raises. Needs ffmpeg 4.3+ (afreqshift); without it the clip
# is saved unshifted and a warning says so.
#BIRDNET_FREQ_SHIFT_HZ=0
# Comma-separated paths the disk manager must never auto-purge.
#BIRDNET_DISK_EXCLUDE=
# Disk-usage % at which recordings start being purged (blank = 95). The purge
# takes from the most-recorded species first (lowest confidence, then oldest,
# within it); locked clips are never purged, whatever this is set to.
#BIRDNET_DISK_PURGE_THRESHOLD=
# Clips every species keeps when that purge runs (blank = 5, 0 = no floor):
# a species with this many clips or fewer is never touched, so the one clip
# of a rare bird outlives the thousandth of a common one.
#BIRDNET_PURGE_SPECIES_FLOOR=
# Seconds a raw capture segment is kept in the temporary streaming folder
# before being cleared (blank = 600). Only needed until the detector has read
# it; the default is far longer than that.
#BIRDNET_STREAM_RETENTION_SECS=
# Hard ceiling in MB on that same streaming folder; oldest segments go first
# (blank = 512). It is usually RAM-backed, so this stops a busy station
# filling it.
#BIRDNET_STREAM_MAX_MB=
# Keep one raw capture segment in N on the data disk, under <recordings>/raw,
# before the streaming folder drains it. Blank or 0 keeps none (the default);
# a value of one keeps every segment. This is what a season can be
# re-analysed from under a new model. Budget it: every segment is about 8 GB
# a day per source, one in ten about 0.8 GB. The disk-full purge takes kept
# raw audio before any clip.
#BIRDNET_RAW_AUDIO_KEEP_EVERY=
# Directory with custom species images (overrides Wikipedia cache).
#BIRDNET_CUSTOM_IMAGE_DIR=
# Where species photographs come from: `wikipedia` (default), `flickr` or
# `avicommons`.
#
# Wikipedia needs no key and covers most species, but for a long tail it has no
# photograph at all — and for many others it has a museum skin, an egg, or a
# range map.
#
# `avicommons` needs no key: it uses https://avicommons.org's cropped bird
# photographs, but only the CC0, CC BY and CC BY-SA ones (about a quarter of the
# species), with Wikipedia behind it for the rest. It downloads the collection's
# 1.6 MB index once a month, not on every page.
#
# `flickr` searches Flickr first and falls back to Wikipedia when Flickr has
# nothing, so choosing it can only add coverage. It needs an API key, free from
# https://www.flickr.com/services/apps/create/apply/
#
# Only the Creative Commons licences that permit commercial use are requested
# (CC BY, CC BY-SA, CC BY-ND, CC0, Public Domain, Flickr Commons, US Government
# Work). Your station's page may be published, and the station cannot know
# whether that counts as commercial, so the permissive set is the safe one.
# Every photograph is shown with its photographer's name and a link to the
# Flickr page, which is what those licences require.
#
# FLICKR_FILTER_EMAIL restricts the search to one photographer's photostream —
# set it to your own Flickr account's email address to show your own
# photographs of the birds your own station heard. Wikipedia still fills in the
# species you have not photographed.
#
# The key is masked in support bundles; so is the local part of the address.
# Config-file keys only: IMAGE_PROVIDER, FLICKR_API_KEY and FLICKR_FILTER_EMAIL
# are read from birdnet.conf, not from the environment, so they have no effect in
# Docker (which ships no config file).
#IMAGE_PROVIDER=wikipedia # or flickr (needs a key) or avicommons (no key)
#FLICKR_API_KEY=
#FLICKR_FILTER_EMAIL=
# =============================================================================
# ADVANCED — internal container paths, custom models, local builds.
# Most users should leave this entire section blank.
# =============================================================================
# Container paths (correct by default; change only if you remap the volume).
BIRDNET_LISTEN=0.0.0.0:8502
BIRDNET_WATCH_DIR=/data/recordings
# Species image cache. Defaults to an `images/` dir beside the database when
# unset (stock installs show photos out of the box); set empty to disable.
BIRDNET_IMAGE_CACHE_DIR=/data/cache
BIRDNET_ANALYTICS_DB=/data/analytics.db
BIRDNET_MODEL_DIR=/data/model
# Upgrade without the pre-migration backup. A few migrations rewrite stored
# history, and the station copies the database first; that copy needs as much
# free space as the database itself. On a disk too full for it the upgrade
# stops and the log names this variable. Setting it (to anything but empty or
# 0) lets the rewrite run with no backup: the previous values cannot be
# recovered. Leave it commented unless the log told you to.
#BIRDNET_SKIP_MIGRATION_BACKUP=1
# DuckDB memory cap for behavioural-analytics queries.
#
# LEAVE THIS UNSET unless you have measured your own station. Unset, the pool is
# sized to a quarter of whatever this process may actually use — the smaller of
# physical RAM and any cgroup limit (the systemd unit's MemoryMax=1G, or a
# container's) — capped at 256MB and floored at 64MB. On the shipped unit that
# comes to exactly 256MB, the value this line used to pin; on a 512MB board it
# comes to 128MB instead of handing one subsystem half the machine, which is
# what this line did. Below a 256MB ceiling the floor cannot be met and
# analytics is refused at startup with an explanation, rather than being
# OOM-killed mid-query.
#
# Set it and it is used verbatim, on any machine — including one the sizing
# would have refused. `birdnet-behavior --doctor` reports which of those
# happened under "Analytics memory".
#
# (This line was uncommented until 0.16. A container using this file as its .env
# therefore pinned 256MB on every board, which is the case the sizing exists
# for.)
#BIRDNET_DUCKDB_MEMORY_LIMIT=256MB
# Threads the analytics engine may use. Unset: the CPUs this process may run on,
# less the two the bird classifier uses, and at least one (2 on a Raspberry Pi 4).
# DuckDB's own default is every core, which slows classification while a
# dashboard query runs.
#BIRDNET_DUCKDB_THREADS=2
# Path of the birdnet.conf file the binary reads (BirdNET-Pi-compatible keys).
# Docker ships no config file; everything here is an environment variable.
#BIRDNET_CONFIG=/etc/birdnet/birdnet.conf
# Seconds a starting instance waits for a previous one to release the data
# directory before refusing to start (default 30, matching the unit's
# TimeoutStopSec). Only one instance may use a data directory at a time.
#BNB_INSTANCE_LOCK_GRACE_SECS=30
# Test-suite only: with this set, a test that would skip because the DuckDB
# behavioral extension could not be fetched fails instead. Not a station
# setting.
#BIRDNET_REQUIRE_LIVE_EXTENSION=1
# Bring-your-own model (leave blank — the entrypoint installs the classifier
# above on first run; set these ONLY if you have a custom model). With
# BIRDNET_SKIP_MODEL_DOWNLOAD=1 nothing is installed at all: stage
# birdnet-v24.onnx, birdnet-v24_labels.txt and birdnet-v24-range-v2.onnx in
# /data/model yourself (`birdnet-behavior --install-model birdnet-v24`, run on a
# machine with internet, writes exactly those files). BIRDNET_MODEL_BASE and
# BIRDNET_ZENODO_BASE (air-gapped mirrors) apply to the V3.0 preview only: V2.4 is
# installed by the binary, which has no mirror setting.
#BIRDNET_SKIP_MODEL_DOWNLOAD=
#BIRDNET_MODEL=
#BIRDNET_LABELS=
# Optional directory of {code}_labels.txt files (`Scientific name_Common name`,
# one per line) whose names replace the built-in ones for that language.
#BIRDNET_LABELS_DIR=
# (BIRDNET_METADATA_MODEL / BIRDNET_METADATA_LABELS are documented under
# "Species occurrence filtering" above — they are not part of the classifier
# download, and nothing auto-downloads them.)
# Host UID whose PulseAudio/PipeWire socket the pulse overlay mounts
# (run `id -u` on the host if your UID is not 1000).
PULSE_UID=1000
# Local builds only — pass `BUILD_FEATURES=analytics` to `docker compose build`
# to compile the analytics variant yourself instead of pulling it from GHCR.
BUILD_FEATURES=
CLI flags — birdnet-behavior --help
BirdNet-Behavior: Real-time acoustic bird classification with DuckDB behavioral analytics
Usage: birdnet-behavior [OPTIONS]
Options:
-c, --config <CONFIG>
Path to configuration file
[env: BIRDNET_CONFIG=]
[default: /etc/birdnet/birdnet.conf]
--listen <LISTEN>
Web server listen address. Defaults to all interfaces so the dashboard is reachable on the LAN out of the box; the `/admin` panel is gated by a password (set `CADDY_PWD`). Bind `127.0.0.1:8502` to restrict to this host
[env: BIRDNET_LISTEN=]
[default: 0.0.0.0:8502]
--tls-mode <TLS_MODE>
How the dashboard terminates TLS: `off`, `self-signed`, or `manual`.
`off` (the default) serves plain HTTP, exactly as every release before this one did.
`self-signed` generates a small local CA and a server certificate it signs, both kept in `--tls-dir` and rotated before they expire. Browsers warn until you import the CA file once (the startup log prints its path); after that they stop, and a later certificate rotation does not bring the warning back.
`manual` serves `--tls-cert` and `--tls-key`. Both are re-read when they change on disk, so an ACME client renewing them in the small hours is picked up without a restart.
[env: BIRDNET_TLS_MODE=]
[default: off]
--tls-cert <TLS_CERT>
Certificate chain (PEM) for `--tls-mode manual`
[env: BIRDNET_TLS_CERT=]
--tls-key <TLS_KEY>
Private key (PEM) for `--tls-mode manual`
[env: BIRDNET_TLS_KEY=]
--tls-listen <TLS_LISTEN>
Where HTTPS listens. Defaults to `--listen`'s host on port 8503.
Set this equal to `--listen` to serve **only** HTTPS on the one port; otherwise plain HTTP keeps answering on `--listen` (see `--tls-redirect`).
[env: BIRDNET_TLS_LISTEN=]
--tls-redirect
Answer plain HTTP with a redirect to HTTPS instead of serving the app.
Ignored when HTTPS shares the one socket with `--listen`, because then there is no plain port to redirect from.
[env: BIRDNET_TLS_REDIRECT=]
--tls-hostnames <TLS_HOSTNAMES>
Names the generated certificate should cover, comma-separated.
Defaults to `localhost`, this machine's hostname and `<hostname>.local`, the bound address, and `127.0.0.1` — the three ways a station is normally reached. Adding a name here regenerates the certificate.
[env: BIRDNET_TLS_HOSTNAMES=]
--tls-dir <TLS_DIR>
Directory for the generated CA and server certificate.
Defaults to a `tls` directory beside the database.
[env: BIRDNET_TLS_DIR=]
--tls-days <TLS_DAYS>
How many days a generated server certificate is valid for.
The local CA that signs it lives for ten years regardless, so rotating the server certificate never invalidates a trust-store import.
[env: BIRDNET_TLS_DAYS=]
[default: 397]
--web-only
Run only the web server (skip analysis daemon)
--check-db
Run database integrity check and exit
--apply-config <FILE>
Validate FILE, install it as the configuration, restart the service, and exit.
The safe way to change the configuration on a running station: a file with errors is refused and nothing changes; a good one is installed behind a timestamped backup of the previous file and the service is restarted. Should the station fail to start on it anyway, it runs on the last configuration a start succeeded on and says so at /station.
--backup-db
Create database backup and exit
--rebuild-species-summary
Recompute the maintained species summary from the detections, and exit.
`species_summary` holds the per-species totals the species list and the per-species charts read, kept up to date on write so those screens do not re-aggregate the whole detection history on every load. It is derived data: rebuilding it cannot lose anything.
Run this if `--doctor` reports the summary disagreeing with the detections. Nothing else should need it.
--doctor
Run the preflight diagnostic and exit.
Validates the configuration, audio source, model file, database integrity, disk space, and tool dependencies. Returns: - 0 — all checks passed - 1 — at least one warning (system will run, features degraded) - 2 — at least one error (system will not work until fixed)
Run after install and any time the system behaves unexpectedly.
[alias: --preflight]
--doctor-gate
Run the doctor as the service's start gate, then exit.
The same checks and report as `--doctor`, but only a failure the station cannot run past — an unreadable config file, an invalid listen address, an unwritable database directory, an unusable HTTPS setup — exits 2. Any other failure exits 1: the station starts, runs what it can, and `/admin/doctor` shows the same report. The systemd unit's `ExecStartPre` uses this; `--doctor` keeps its own exit codes.
--install-model <ID>
Install a classifier from the built-in catalogue, then exit (`G-10`).
`--install-model perch-v2`. Pass `list` to print the catalogue without installing anything.
In the foreground rather than behind an API call, deliberately: this is a 400 MB download that takes hours on the uplink a field station has, and an operator should be able to watch it, interrupt it, and see it fail. The download is checked against a sha256 compiled into this binary — a file that does not match is deleted rather than installed, and nothing lands under a name the station would load until it has verified.
--extraction-length <EXTRACTION_LENGTH>
Seconds of audio saved around each detection (default 6).
BirdNET-Pi's `EXTRACTION_LENGTH`. Longer clips are easier to verify by ear and cost proportionally more disk; shorter ones can clip the end of a song. Also settable on `/admin/settings`.
[env: BIRDNET_EXTRACTION_LENGTH=]
--log-level <LEVEL>
Global log level: `trace`, `debug`, `info`, `warn`, `error`, `off`.
Also settable as `LOG_LEVEL` in the config file. `RUST_LOG` overrides both when it is set, so a developer's export still wins.
[env: BIRDNET_LOG_LEVEL=]
--log-modules <SPEC>
Per-subsystem log levels, e.g. `audio=debug,web=warn`.
Subsystem names are operator-facing rather than Rust module paths: `audio`, `detection`, `web`, `db`, `integrations`, `analytics`. An explicit path (`birdnet_core::inference=trace`) is accepted too.
This is the equivalent of BirdNET-Pi's per-service log levels: one process rather than eight services, so the knob is per subsystem. Unknown names are reported at startup rather than ignored. Also settable as `LOG_MODULES` in the config file.
[env: BIRDNET_LOG_MODULES=]
--support-bundle [<PATH>]
Collect a support bundle and exit.
Writes a `.tar.gz` holding the diagnostic report (both JSON and text), the station's version, a **redacted** copy of the configuration, and the last 2000 journal lines — everything a maintainer asks for in the first three replies to a bug report, in one file.
Passwords, tokens and URL credentials are masked. Read it before posting it anywhere public all the same.
Defaults to `birdnet-support.tar.gz` in the working directory; pass a path to write it elsewhere.
--doctor-json
Run the preflight diagnostic and emit a single-line JSON document.
Same checks and exit codes as `--doctor`, but the output is a machine-readable JSON object with `summary` and `checks` fields. Use for monitoring scripts (Nagios / Zabbix / Prometheus textfile collector / Home Assistant command sensor).
--channel-report
Report what a stereo microphone is actually delivering to the model.
Records a few seconds from the configured ALSA device and prints, for the same audio: the level of each channel, the inter-channel delay (and the capsule spacing that implies), and what each way of reducing two channels to one would hand BirdNET.
The model takes a single channel, so two must become one somewhere. Today that is a plain average, which is harmless for coincident capsules and a comb filter for spaced ones — at half a period of delay it cancels almost the entire signal. Whether a given station sits in that case depends on its microphone and cannot be answered anywhere but on the station.
STOP THE SERVICE FIRST. An ALSA capture device is exclusive, so this cannot open a microphone the running station is already holding: `sudo systemctl stop birdnet-behavior`, run the report, start it again.
--channel-report-secs <CHANNEL_REPORT_SECS>
Seconds of audio to record for `--channel-report` (default 5).
Longer is steadier. Run it while birds are actually singing: the cancellation is direction-dependent, so ambient noise alone can look benign on a microphone that loses badly on real song.
[default: 5]
--migration-report
Report what a pending migration would do to existing detections, and exit.
Most migrations only change the schema around the data. A few rewrite rows that are already on disk, and those destroy their own input — after migration 24 folds each detection's chunk offset into its timestamp, nothing records what that timestamp used to be. This shows how many rows would move, by how much, how many roll onto the next day, and which are left alone, before any of it happens.
Opens the database read-only and changes nothing, so it is safe to run on a live station. The rewrite itself runs on the next normal start, which first copies the database to `<db>.pre-migration-<version>.backup`.
--import-birdnet-pi-conf <PATH>
Read a BirdNET-Pi installation's settings and show what each becomes here.
Give the BirdNET-Pi folder (`~/BirdNET-Pi`) or its `birdnet.conf`. Reads that file and, beside it, `apprise.txt`, `body.txt` and the three species list files, and prints every key with what it becomes, what is converted on the way and what has no equivalent. Changes nothing unless `--apply-import` is also given, which writes the settings through the same checks as the Settings page. The same as Station, Data, in the browser. Credentials are never printed.
--import-birdnet-pi-clips <PATH>
Bring BirdNET-Pi's recordings across, for the detections already imported.
Give BirdNET-Pi's clip folder: `~/BirdSongs`, or the `Extracted` or `By_Date` folder inside it. Looks only; add `--apply-import` to do it, and `--clips-mode` to choose copy, link or move. Refuses a copy that would take the disk past the purge threshold.
--clips-mode <MODE>
What `--import-birdnet-pi-clips` does with each recording: `copy` (BirdNET-Pi keeps its own), `hardlink` (no extra space; same disk only), `move` (BirdNET-Pi loses them) or `leave` (count them, change nothing)
[default: copy]
--apply-import
Do what `--import-birdnet-pi-conf` and `--import-birdnet-pi-clips` show. Without it they change nothing
--fix
Attempt safe automatic repairs, then run the diagnostic.
Implies `--doctor` (use with `--doctor-json` for machine-readable output). Repairs are idempotent and never destructive — currently they create any missing configured directories (the recordings/watch directory and the image-cache directory), the most common cause of "service runs but nothing is recorded" (e.g. after a tmpfs reset on reboot). The diagnostic then runs as usual and reflects the repaired state. Anything that needs root (ownership, packages) is reported, not changed.
--model <MODEL>
Path to the ONNX model file (overrides config)
[env: BIRDNET_MODEL=]
--labels <LABELS>
Path to the species labels file (overrides config)
[env: BIRDNET_LABELS=]
--watch-dir <WATCH_DIR>
Directory to watch for new audio files (overrides config)
[env: BIRDNET_WATCH_DIR=]
--process-existing
Process audio files already present in watch directory on startup
--analytics-db <ANALYTICS_DB>
Path to the `DuckDB` analytics database file.
Analytics is on by default at `<database>.duckdb` beside the SQLite file; this overrides the path. Pass an empty value (`--analytics-db ""` or `BIRDNET_ANALYTICS_DB=`) to run without `DuckDB` at all.
[env: BIRDNET_ANALYTICS_DB=]
--refresh-extension
Reinstall the behavioral `DuckDB` extension and exit.
Force-downloads the latest `behavioral` build for the bundled `DuckDB` version from the community registry, loads it to verify, then exits. Requires `--analytics-db` (or `ANALYTICS_DB_PATH`) and network access.
--verify-extension
Verify the behavioral `DuckDB` extension actually loads, and exit.
Opens a throwaway `DuckDB` database, loads the extension through the normal three-stage fallback, and reports the engine version, the extension version, and what the build-time embedded copy targets. Exits 0 when the extension loads and non-zero when it does not.
It then runs the date-window query every dashboard opens with. That covers the other extension a station needs, `icu`, which is where `CURRENT_DATE` lives — and which DuckDB otherwise autoinstalls into `$HOME/.duckdb`, a path the shipped systemd unit mounts read-only.
Unlike `--refresh-extension` this needs no network and writes nothing you care about, so it is the check to run on a station whose analytics pages are empty. Run it with networking disabled to prove the *offline* guarantee specifically: with no network neither the extension cache nor the community registry can satisfy either load, so only the embedded copies can.
--offline
Make no unsolicited outbound connections.
Turns off every network call the station would otherwise make on its own: the daily update check against `api.github.com` and the Wikipedia species-image downloads. Integrations you configure explicitly — Apprise, `BirdWeather`, MQTT, SMTP, the heartbeat ping, the weather poll — are left alone, because asking for one is a decision the operator already made.
For metered or cellular links, air-gapped deployments, and institutional review, where "which hosts does this contact?" needs one answer rather than a per-feature audit. See `docs/book/getting-started/configuration.md` for the full egress list.
[env: BIRDNET_OFFLINE=]
--private-mode
Put the whole station behind the sign-in, not only the admin panel.
By default viewing is open: the dashboard, the read API, the live audio stream and both `WebSockets` are served to anyone who can reach the port. Behind a tunnel or a port forward that is the internet. With this set, everything needs a session except the sign-in form, its assets, the health probe and what `--public-access` names. A private station with no admin password (`CADDY_PWD`) answers 503 to everything else rather than falling open. Config file: `PRIVATE_MODE`.
[env: BIRDNET_PRIVATE_MODE=]
--public-access <PUBLIC_ACCESS>
What stays open on a private station: a comma-separated list of `live_audio` (the live audio, MP3 and HLS, and the live spectrogram), `share` (operator-minted `/r/<token>` links) and `metrics` (`/api/v2/metrics`).
No effect without `--private-mode`. An unknown name is reported and skipped. Config file: `PUBLIC_ACCESS`.
[env: BIRDNET_PUBLIC_ACCESS=]
--no-update-check
Skip the daily check for a new release.
The station otherwise contacts `api.github.com` 60 seconds after start and every 24 hours after that, purely to log whether a newer version exists. It never installs anything on its own — updates are applied only from the admin panel. Implied by `--offline`.
[env: BIRDNET_NO_UPDATE_CHECK=]
--apprise-url <APPRISE_URL>
Apprise notification server URL (e.g., `http://localhost:8000`)
[env: BIRDNET_APPRISE_URL=]
--notify-urls <NOTIFY_URLS>
Notification URLs delivered in-process, comma- or newline-separated.
Uses Apprise's URL syntax — `discord://{id}/{token}`, `tgram://{bot_token}/{chat_id}`, `ntfy://{topic}`, `gotify://{host}/{token}`, `pover://{user}@{token}`, `slack://{a}/{b}/{c}`, `jsons://{host}/{path}` — but sends them directly, so no Apprise installation is involved. A scheme without a native sender is reported once at startup and needs `--apprise-config`.
[env: BIRDNET_NOTIFY_URLS=]
--notify-rate-per-minute <NOTIFY_RATE_PER_MINUTE>
Ceiling on notifications per destination per minute; `0` disables it.
About the services' limits rather than ours: Discord allows five requests a second per webhook, Telegram about thirty, and Pushover ten thousand messages a month. Config-file key: `NOTIFY_RATE_PER_MINUTE`.
[env: BIRDNET_NOTIFY_RATE_PER_MINUTE=]
--notify-confidence <NOTIFY_CONFIDENCE>
Minimum confidence threshold for Apprise notifications (0.0–1.0)
[env: BIRDNET_NOTIFY_CONFIDENCE=]
[default: 0.8]
--deadman-hours <DEADMAN_HOURS>
Hours of zero detections before the deadman watchdog raises an alert.
The end-to-end "is the station actually detecting?" check: when no detection has been recorded for this many hours, a loud warning is logged and (if Apprise is configured) a notification is sent — once per quiet episode, with a recovery notice when detections resume. `0` disables alerting; the freshness gauge (`birdnet_detection_silence_seconds`) is exported regardless. Defaults to 24; config-file key: `DEADMAN_HOURS`.
[env: BIRDNET_DEADMAN_HOURS=]
--station-health-alerts <STATION_HEALTH_ALERTS>
Alert on operational faults the detection deadman cannot see.
The deadman answers "is the station detecting at all?". This answers the questions that leave it quiet: one of several microphones down while the rest keep working, a disk full enough that recordings are being purged, a CPU at its throttling temperature, or a backup / integrity check that has not completed in weeks. Each alerts once per episode with a recovery notice, like the deadman. Defaults to on; config-file key: `STATION_HEALTH_ALERTS`.
[env: BIRDNET_STATION_HEALTH_ALERTS=]
[possible values: true, false]
--birdweather-token <BIRDWEATHER_TOKEN>
`BirdWeather` station token for uploading detections
[env: BIRDNET_BIRDWEATHER_TOKEN=]
--latitude <LATITUDE>
Station latitude for `BirdWeather` uploads
[env: BIRDNET_LATITUDE=]
--longitude <LONGITUDE>
Station longitude for `BirdWeather` uploads
[env: BIRDNET_LONGITUDE=]
--image-cache-dir <IMAGE_CACHE_DIR>
Directory for caching species images from Wikipedia.
Defaults to an `images/` directory beside the database when unset, so a stock install shows species photos out of the box. Pass an empty value (`--image-cache-dir ""`) to disable image caching entirely — no Wikipedia fetches, e.g. for air-gapped deployments.
[env: BIRDNET_IMAGE_CACHE_DIR=]
--alsa-device <ALSA_DEVICE>
ALSA device for microphone capture (e.g., `plughw:1,0`).
For a single microphone. Use `--alsa-devices` to capture from several local mics at once.
[env: BIRDNET_ALSA_DEVICE=]
--alsa-devices <ALSA_DEVICES>
ALSA devices for multi-microphone capture (semicolon-separated, or repeat the flag).
Each device gets its own independent capture pipeline with filenames prefixed `MIC_1-`, `MIC_2-`, etc. (and matching per-source health metrics). Overrides `--alsa-device` when set. A semicolon — not a comma — separates devices because ALSA names themselves contain commas (e.g. `plughw:1,0`). Config-file equivalent: `ALSA_CARDS` (semicolon-separated); `ALSA_CARD` remains the single-mic key.
[env: BIRDNET_ALSA_DEVICES=]
--pipewire-device <PIPEWIRE_DEVICE>
PipeWire/PulseAudio source for microphone capture.
Uses `ffmpeg -f pulse` which works with both native `PulseAudio` and `PipeWire` (via `pipewire-pulse`). Leave empty or use `default` for the system default source. Takes precedence over `--alsa-device` when set. BirdNET-Pi equivalent: ALSA device pointing to a `PulseAudio` sink.
[env: BIRDNET_PIPEWIRE_DEVICE=]
--rtsp-url <RTSP_URL>
Network stream URL for audio capture (e.g., `rtsp://camera.local:554/stream`).
Also `http(s)://` (Icecast, HLS), `rtmp://`, `udp://` (MPEG-TS) and `srt://`. For a single stream. Use `--rtsp-urls` for multiple streams.
[env: BIRDNET_RTSP_URL=]
--rtsp-urls <RTSP_URLS>
Comma-separated network stream URLs for multi-stream capture.
Each URL gets its own independent capture pipeline with filenames prefixed `RTSP_1-`, `RTSP_2-`, etc. Overrides `--rtsp-url` if set.
[env: BIRDNET_RTSP_URLS=]
--segment-duration <SEGMENT_DURATION>
Duration of each recording segment in seconds (default: 15)
[env: BIRDNET_SEGMENT_DURATION=]
[default: 15]
--recording-schedule <RECORDING_SCHEDULE>
Recording schedule mode: "all-day" (24/7), "solar" (sunrise-to-sunset), or "fixed:HH:MM-HH:MM" (e.g., "fixed:06:00-20:00")
[env: BIRDNET_RECORDING_SCHEDULE=]
[default: all-day]
--night-inhibit
Inhibit recording during night hours (requires --latitude and --longitude)
[env: BIRDNET_NIGHT_INHIBIT=]
--twilight-offset <TWILIGHT_OFFSET>
Minutes offset from sunrise/sunset for twilight recording (default: 30).
Applies to both ends of the day. Use `--pre-sunrise-offset` / `--post-sunset-offset` to set them independently; either one overrides this for its own end.
[env: BIRDNET_TWILIGHT_OFFSET=]
[default: 30]
--pre-sunrise-offset <PRE_SUNRISE_OFFSET>
Minutes *before* sunrise at which recording starts.
Overrides `--twilight-offset` for the morning end only. Dawn and dusk activity are rarely symmetric — the dawn chorus starts well before first light while evening song tails off quickly — so a station usually wants a longer pre-sunrise window than post-sunset one. Unset falls back to `--twilight-offset`.
[env: BIRDNET_PRE_SUNRISE_OFFSET=]
--post-sunset-offset <POST_SUNSET_OFFSET>
Minutes *after* sunset at which recording stops.
Overrides `--twilight-offset` for the evening end only. Unset falls back to `--twilight-offset`.
[env: BIRDNET_POST_SUNSET_OFFSET=]
--heartbeat-url <HEARTBEAT_URL>
Uptime-monitor URL to ping every five minutes while the station runs.
A liveness signal for an external monitor (Healthchecks.io, Uptime Kuma): its *absence* is the alarm, so it must not depend on a bird singing. Whether the station is still detecting is a different question, answered by `--deadman-hours`.
[env: BIRDNET_HEARTBEAT_URL=]
--notify-trigger <NOTIFY_TRIGGER>
Notification trigger: "each", "new-species", "new-species-daily", or "off"; several may be listed, separated by commas ("each,new-species"). `BirdNET-Pi`'s three `APPRISE_NOTIFY_*` switches are this list
[env: BIRDNET_NOTIFY_TRIGGER=]
[default: each]
--notify-species-exclude <NOTIFY_SPECIES_EXCLUDE>
Species to exclude from notifications (comma-separated scientific names)
[env: BIRDNET_NOTIFY_SPECIES_EXCLUDE=]
--notify-species-only <NOTIFY_SPECIES_ONLY>
Only notify for these species (comma-separated scientific names)
[env: BIRDNET_NOTIFY_SPECIES_ONLY=]
--notify-title-template <NOTIFY_TITLE_TEMPLATE>
Custom notification title template (supports $comname, $sciname, $confidence, etc.)
[env: BIRDNET_NOTIFY_TITLE_TEMPLATE=]
--notify-body-template <NOTIFY_BODY_TEMPLATE>
Custom notification body template (supports $comname, $sciname, $confidence, etc.)
[env: BIRDNET_NOTIFY_BODY_TEMPLATE=]
--metadata-model <METADATA_MODEL>
Path to the metadata ONNX model for species occurrence filtering.
When set, the metadata model predicts which species are likely present at the station's location and time of year, filtering out unlikely species.
[env: BIRDNET_METADATA_MODEL=]
--metadata-labels <METADATA_LABELS>
Path to the metadata model's own label file.
The metadata model and the classifier do not score the same species list — BirdNET Geomodel v3.0 covers 12 012 species where the V3.0 Global 11K classifier emits 11 560 — so the model's output positions mean nothing without the labels it shipped with. Supply them and the two are matched by scientific name.
Omit this only for a metadata model indexed identically to the classifier (a matched BirdNET pair). The station verifies that at startup and refuses a mismatched model rather than reporting one bird under another bird's name.
This file also carries each species' eBird code, which the species page's "View on eBird" link is built from; without it that link is not shown.
[env: BIRDNET_METADATA_LABELS=]
--species-aliases <SPECIES_ALIASES>
Extra scientific-name aliases for the occurrence filter: a tab-separated `legacy<TAB>canonical` file.
The geomodel and the classifier were frozen at different points in a moving taxonomy and do not always spell a species the same way. Most of those the station reconciles by itself, on the common name and the specific epithet; this file is for the pairs that share neither.
Blank lines and `#` comments are skipped, and a malformed line is skipped rather than fatal — a typo here must not take the occurrence filter off a running station.
[env: BIRDNET_SPECIES_ALIASES=]
--sf-thresh <SF_THRESH>
Species frequency threshold for the metadata model filter (0.0-1.0).
Species with occurrence probability below this threshold are filtered out. Lower values allow more species through; higher values are more restrictive.
[env: BIRDNET_SF_THRESH=]
[default: 0.03]
--privacy-threshold <PRIVACY_THRESHOLD>
Privacy filter threshold for human voice detection (0.0 = disabled).
The model's confidence for its human classes (speech, whistling, other human sounds) at or above which an analysis window is suppressed, along with the windows either side of it. Read from the model's output before the detection threshold applies, so this value binds on its own: lower suppresses more. Typical values: 0.01-0.03.
[env: BIRDNET_PRIVACY_THRESHOLD=]
[default: 0.0]
--privacy-rank <PRIVACY_RANK>
BirdNET-Pi's privacy rule: suppress an analysis window, and the windows either side of it, when a human class (speech, whistling, other human sounds) is among the model's this-many highest-scoring classes (0 = off).
Read from the model's output before the detection threshold applies, as BirdNET-Pi reads it. The default, 10, is BirdNET-Pi's own at its own default (`PRIVACY_THRESHOLD=0` gives `human_cutoff = max(10, …)`), so a station nobody has customised does not keep a clip of a conversation near its microphone. For a BirdNET-Pi `PRIVACY_THRESHOLD` of `p`, set this to `max(10, 60 × p)`. It works beside `--privacy-threshold`; either rule suppresses a window. Config key: `PRIVACY_RANK`.
[env: BIRDNET_PRIVACY_RANK=]
[default: 10]
--noise-threshold <NOISE_THRESHOLD>
Confidence at which a barking dog suppresses its audio chunk (0.0 = disabled).
`BirdNET`'s label set carries non-bird classes, and a bark near the microphone is broadband enough that the classifier also produces confident-looking scores for whatever species it most resembles. When a watched class scores at or above this, every detection in that chunk is discarded. Typical values: 0.5-0.8. Config key: `NOISE_THRESHOLD`.
[env: BIRDNET_NOISE_THRESHOLD=]
[default: 0.0]
--noise-classes <NOISE_CLASSES>
Non-bird label names the noise filter watches, comma-separated.
Defaults to `Dog`. Other classes in the label set are `Siren`, `Engine`, `Power tools`, `Fireworks`, `Gun`, `Environmental` and `Noise` — the last two score highly on ordinary quiet recordings, so watching them will suppress a great deal. Names are matched exactly (case-insensitively) against a label's common or scientific name. Config key: `NOISE_CLASSES`.
[env: BIRDNET_NOISE_CLASSES=]
--decrypt-backup <FILE>
Decrypt an offsite backup file and exit.
The counterpart to `--offsite-backup`: takes a `.bnb` file downloaded from the bucket or the SSH server and writes the plain database to `--out`. The result is an ordinary `SQLite` file — stop the service and put it in place of `birds.db`, as with any snapshot.
The passphrase comes from `OFFSITE_PASSPHRASE` (config or `BIRDNET_OFFSITE_PASSPHRASE`), never from a flag: an argument on a command line is visible in `ps` to every user on the machine. There is no recovery without it.
--out <PATH>
Where `--decrypt-backup` writes the plain database.
Refused if the file already exists: a restore that silently overwrote the database you were about to compare against would be worse than one that failed.
--offsite-backup <OFFSITE_BACKUP>
Send each weekly backup to an offsite destination: `off`, `s3` or `sftp`.
A station's rolling backups live beside its database, on the same card. That covers a corrupt page and a bad VACUUM; it does not cover the card wearing out, the enclosure flooding, or the Pi being stolen.
Everything is encrypted on the station before it leaves, and that is not configurable — so this needs `OFFSITE_PASSPHRASE`, plus the destination's own settings. **Those are config-file or environment keys only, never flags**: an argument on a command line is visible in `ps` to every user on the box and is copied into the journal by systemd. `--doctor` lists what is missing. Config key: `OFFSITE_BACKUP`.
[env: BIRDNET_OFFSITE_BACKUP=]
--confirmation-level <CONFIRMATION_LEVEL>
How much agreement from neighbouring analysis windows a species needs before it is recorded: `off`, `lenient`, `moderate`, `balanced` or `strict`.
A real bird sings across more than one window; a classifier artefact usually fires once. Each level names the fraction of the windows within six seconds that must carry the same species — 20%, 30%, 50%, 70% — rounded up, and never less than the detection itself.
**This does nothing without `--overlap`.** With no overlap a six-second neighbourhood holds two 3-second windows, and 20% or 30% of two rounds to one, which every detection already satisfies. `--doctor` reports the overlap each level needs, and the daemon warns at startup when the level it was given cannot reject anything.
Off by default: it removes rows, so switching it on puts a visible step in every chart. Config key: `CONFIRMATION_LEVEL`.
[env: BIRDNET_CONFIRMATION_LEVEL=]
[default: off]
--duplicate-interval-secs <DUPLICATE_INTERVAL_SECS>
Suppress a repeat of the same species within this many seconds (0 = disabled).
A 15-second recording is five 3-second chunks, so a bird that sings through the whole of it is recorded five times — and every count in the application is a row count, so a species that sings in long phrases outscores one that calls in short bursts for no reason but phrasing.
Off by default: switching it on changes how many rows a station records, which puts a visible step in every chart when it happens. Config key: `DUPLICATE_INTERVAL_SECS`.
[env: BIRDNET_DUPLICATE_INTERVAL_SECS=]
[default: 0]
--night-filter
Quarantine day birds heard in the middle of the night.
A blue tit "detected" at 02:30 is almost always the classifier hearing something else. Owls, nightjars, rails and bitterns are exempt — see `birdnet_core::detection::nocturnal` for the genus list, and `--night-extra-nocturnal` to add to it.
Needs station coordinates; without them there is no sunrise to compute and the filter stays off. Detections are quarantined for review, never dropped, because the taxonomy is genus-level and cannot be complete. Config key: `NIGHT_FILTER`.
[env: BIRDNET_NIGHT_FILTER=]
--night-margin-mins <NIGHT_MARGIN_MINS>
Minutes after sunset and before sunrise before the night window opens.
Birds sing through dusk and start again well before sunrise, so a window that began at sunset would quarantine the evening chorus. Config key: `NIGHT_MARGIN_MINS`.
[env: BIRDNET_NIGHT_MARGIN_MINS=]
[default: 60]
--night-extra-nocturnal <NIGHT_EXTRA_NOCTURNAL>
Extra genera or scientific names always allowed at night, comma-separated.
A station recording nocturnal flight calls — migrating thrushes and warblers calling as they pass overhead — should either leave `--night-filter` off or name those genera here. Config key: `NIGHT_EXTRA_NOCTURNAL`.
[env: BIRDNET_NIGHT_EXTRA_NOCTURNAL=]
--overlap <OVERLAP>
Analysis window overlap in seconds (0.0-2.9, default 0.0).
Controls how much consecutive analysis windows overlap (the model's window: 3 s for BirdNET V2.4, 4.5 s for the bundled V3.0). Higher overlap increases sensitivity at the cost of more CPU time. BirdNET-Pi equivalent: OVERLAP config option.
[env: BIRDNET_OVERLAP=]
[default: 0.0]
--site-name <SITE_NAME>
Custom site name displayed in page titles and header.
Replaces the default "BirdNet-Behavior" branding in the web UI.
[env: BIRDNET_SITENAME=]
--lang <LANG>
Language to show species common names in (e.g. `de`, `fr`, `ja`, `zh_CN`).
Only what is shown changes: the names stored with detections stay the classifier's. 35 languages are built in (`/api/v2/languages` lists them with their coverage of the installed classifier); species a language has no name for keep the classifier's. Default: "en" (English).
[env: BIRDNET_LANG=]
[default: en]
--labels-dir <LABELS_DIR>
Directory of optional species-name files that replace the built-in names.
Files are named `de_labels.txt`, `fr_labels.txt` and so on (or sit in a `labels_l18n/` subdirectory), one `Scientific name_Common name` per line. Not needed for the built-in languages.
[env: BIRDNET_LABELS_DIR=]
--info-site <INFO_SITE>
eBird/AllAboutBirds species info links: "ebird", "allaboutbirds", or "none"
[env: BIRDNET_INFO_SITE=]
[default: ebird]
--audio-format <AUDIO_FORMAT>
Audio format for extracted detection clips: "wav", "mp3", "flac", or "ogg".
Non-WAV formats require ffmpeg or sox to be installed. BirdNET-Pi equivalent: AUDIOFMT config option.
[env: BIRDNET_AUDIO_FORMAT=]
[default: wav]
--max-files-per-species <MAX_FILES_PER_SPECIES>
Maximum number of extracted recordings kept per species (0 = unlimited).
When set, the oldest files beyond this limit are deleted automatically. `BirdNET-Pi` equivalent: `MAX_FILES_SPECIES` config option.
[env: BIRDNET_MAX_FILES_PER_SPECIES=]
[default: 0]
--disk-exclude <DISK_EXCLUDE>
Comma-separated paths to exclude from disk usage monitoring.
Files under these paths are never auto-purged.
[env: BIRDNET_DISK_EXCLUDE=]
--clip-retention-days <CLIP_RETENTION_DAYS>
Reclaim the audio of detections older than this many days (0 = keep audio forever, the default).
The age-based half of retention, alongside `--max-files-per-species` and the disk-full purge. Locked clips are exempt, and the detection records themselves are always kept — counts, species lists, trends and exports are unaffected; only the audio is reclaimed.
[env: BIRDNET_CLIP_RETENTION_DAYS=]
[default: 0]
--disk-purge-threshold <DISK_PURGE_THRESHOLD>
Disk-usage percentage at which recordings start being purged.
The safety net that keeps a 24/7 station from filling its card: once the data disk crosses this, clips are deleted from the most-recorded species first (lowest confidence, then oldest, within it), no species is taken below `--purge-species-floor`, and locked clips are never touched. `BirdNET-Pi` equivalent: `PURGE_THRESHOLD`. What happens at the threshold is the config key `FULL_DISK` (`purge`, the default, or `keep`, which stops recording and deletes nothing), as in BirdNET-Pi.
`0` leaves the resolution to the config file / admin settings, then the 95 % default — the flag is only "set" when given, so it never silently overrides a value chosen in the UI.
[env: BIRDNET_DISK_PURGE_THRESHOLD=]
[default: 0]
--purge-species-floor <PURGE_SPECIES_FLOOR>
Clips every species keeps when the disk-full purge runs.
The purge takes from the most-recorded species first, so the single clip of the year's rarest bird is the last thing to go; this floor makes it never go: a species with this many clips or fewer is not touched, however full the disk is. `0` removes the floor. Config file: `PURGE_SPECIES_FLOOR`; unset, 5.
[env: BIRDNET_PURGE_SPECIES_FLOOR=]
--stream-retention-secs <STREAM_RETENTION_SECS>
Seconds a raw capture segment is kept in the transient stream directory.
Raw segments are read by the detector and never needed again; draining them is what keeps the RAM-backed stream dir from filling. Far longer than the pipeline needs, so an unprocessed segment is never removed. `0` = resolve from config / admin settings, then the 600 s default.
[env: BIRDNET_STREAM_RETENTION_SECS=]
[default: 0]
--stream-max-mb <STREAM_MAX_MB>
Hard ceiling in mebibytes on the transient stream directory.
Backstop for many-stream or backed-up runs; the oldest segments drop first. `0` = resolve from config / admin settings, then the 512 MiB default.
[env: BIRDNET_STREAM_MAX_MB=]
[default: 0]
--raw-audio-keep-every <RAW_AUDIO_KEEP_EVERY>
Keep one raw capture segment in N on the data disk.
The raw audio is otherwise gone once analysed, so only what already triggered a detection survives and a season can never be re-analysed under a new model. With this set, one aged segment in N (in capture order) is copied to `<recordings>/raw` before the stream directory drains it; `1` keeps every segment. Budget it: a 15 s segment at 48 kHz mono is about 1.4 MB, so `1` is roughly 8 GB a day per source and `10` about 0.8 GB. The disk-full purge takes kept raw audio before any clip. Config file: `RAW_AUDIO_KEEP_EVERY`; unset or 0, nothing is kept.
[env: BIRDNET_RAW_AUDIO_KEEP_EVERY=]
--custom-image-dir <CUSTOM_IMAGE_DIR>
Directory containing custom species images (checked before Wikipedia cache).
Files should be named `{lowercase_sci_name_with_underscores}.jpg`, e.g. `turdus_merula.jpg`. There is no `BirdNET-Pi` equivalent: its `CUSTOM_IMAGE` is one picture for the Overview page, which is the `dashboard_image` setting here.
[env: BIRDNET_CUSTOM_IMAGE_DIR=]
--apprise-config <APPRISE_CONFIG>
Path to Apprise config file (alternative/addition to --apprise-url).
When set, uses the `apprise` CLI tool with `-c <file>` for notifications. `BirdNET-Pi` equivalent: its `apprise.txt`, which importing a BirdNET-Pi `birdnet.conf` brings across (Station, Data, or `--import-birdnet-pi-conf`).
[env: BIRDNET_APPRISE_CONFIG=]
--weekly-report-schedule <WEEKLY_REPORT_SCHEDULE>
Weekly report notification schedule.
Send a weekly summary via Apprise on a fixed weekday. Values: "monday", "tuesday", ..., "sunday", or "disabled". `BirdNET-Pi` equivalent: `weekly_report` cron job.
[env: BIRDNET_WEEKLY_REPORT_SCHEDULE=]
[default: monday]
--freq-shift-hz <FREQ_SHIFT_HZ>
Frequency shift applied to extracted audio clips in Hz (0 = disabled).
Shifts every frequency in a saved detection clip by the given number of Hz (a shift, not a ratio: an 8 kHz song at -3000 is heard at 5 kHz), and leaves the clip's length alone. Positive raises, negative lowers.
For age-related high-frequency hearing loss the useful direction is **negative**: it brings warbler and kinglet song, which lives above 8 kHz, down into a band that is still audible. Typical value: -3000. (Earlier releases documented this backwards — see `birdnet_core::audio::extraction::ACCESSIBILITY_SHIFT_HZ`.)
Requires ffmpeg 4.3 or newer (its `afreqshift` filter); without one the clip is saved unshifted and a warning says so. `BirdNET-Pi` equivalent: its `FREQSHIFT_*` options, which scale by a ratio instead.
[env: BIRDNET_FREQ_SHIFT_HZ=]
[default: 0]
--mqtt-host <MQTT_HOST>
MQTT broker hostname or IP address for `IoT` detection events.
When set, each bird detection is published as a JSON payload to the configured broker. Compatible with Home Assistant, Node-RED, Mosquitto, and any MQTT 3.1.1-compatible broker. Example: `--mqtt-host mqtt.local`
[env: BIRDNET_MQTT_HOST=]
--mqtt-port <MQTT_PORT>
MQTT broker port.
Left unset it is 1883, or 8883 when `--mqtt-tls` is given.
[env: BIRDNET_MQTT_PORT=]
--mqtt-tls
Connect to the MQTT broker over TLS (`mqtts`).
The certificate is verified against the platform trust store plus anything in `--mqtt-ca-file`. There is no way to skip verification: an unverified TLS connection carrying the broker credentials is worse than a plaintext one, because it looks safe.
[env: BIRDNET_MQTT_TLS=]
--mqtt-ca-file <MQTT_CA_FILE>
PEM file of extra certificates to trust for `--mqtt-tls`.
For a broker behind a private CA — or a self-signed one, whose own certificate works here as a trust anchor. Config key: `MQTT_CA_FILE`.
[env: BIRDNET_MQTT_CA_FILE=]
--mqtt-client-cert <MQTT_CLIENT_CERT>
PEM file with this station's client certificate, for a broker that requires mutual TLS. Needs `--mqtt-client-key`; implies `--mqtt-tls`. Config key: `MQTT_CLIENT_CERT`
[env: BIRDNET_MQTT_CLIENT_CERT=]
--mqtt-client-key <MQTT_CLIENT_KEY>
PEM file with the private key for `--mqtt-client-cert`. Config key: `MQTT_CLIENT_KEY`
[env: BIRDNET_MQTT_CLIENT_KEY=]
--mqtt-tls-server-name <MQTT_TLS_SERVER_NAME>
Hostname to validate the broker's certificate against.
Defaults to `--mqtt-host`. Set it when connecting to a broker by IP whose certificate names a hostname — the usual shape on a home LAN with no internal DNS. Config key: `MQTT_TLS_SERVER_NAME`.
[env: BIRDNET_MQTT_TLS_SERVER_NAME=]
--mqtt-client-id <MQTT_CLIENT_ID>
MQTT client identifier published in CONNECT packets.
Must be unique per active connection on the broker.
[env: BIRDNET_MQTT_CLIENT_ID=]
[default: birdnet-behavior]
--mqtt-username <MQTT_USERNAME>
MQTT broker username for authentication
[env: BIRDNET_MQTT_USERNAME=]
--mqtt-password <MQTT_PASSWORD>
MQTT broker password for authentication
[env: BIRDNET_MQTT_PASSWORD=]
--mqtt-topic-prefix <MQTT_TOPIC_PREFIX>
MQTT topic prefix for published messages (default: "birdnet").
Detections are published to `{prefix}/detection/{species}`. Status heartbeat is published to `{prefix}/status`.
[env: BIRDNET_MQTT_TOPIC_PREFIX=]
[default: birdnet]
--mqtt-retain
Set the RETAIN flag on MQTT detection messages.
When set, the broker stores the last detection per topic and delivers it immediately to new subscribers. Useful for Home Assistant sensors.
[env: BIRDNET_MQTT_RETAIN=]
--mqtt-ha-discovery
Publish Home Assistant MQTT auto-discovery messages at startup.
When enabled, BirdNet-Behavior publishes MQTT discovery payloads to `homeassistant/<component>/<uid>/config` so Home Assistant automatically creates sensors without manual `configuration.yaml` entries.
Requires `--mqtt-host` to be set. Harmless if HA is not running.
[env: BIRDNET_MQTT_HA_DISCOVERY=]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
Keeping this page current: the
.env.exampleblock updates itself on every docs build. Regenerate the CLI block after changing any flag with:./scripts/gen-cli-help.shCI fails if the committed
cli-help.txtis stale, so this can’t silently drift.