Ansible playbooks and roles to manage the Pyronear Raspberry Pi fleet (wildfire detection engines) and the backing servers (alert API, platform, mediamtx, openvpn, annotation, temporal).
This repository is a template: it holds the playbooks, roles and tooling but
no inventory and no secrets. The inventory, host_vars/, group_vars/ and
the vault password live in a sister repo (pi-manager-X), glued in through
REPO_PATH. You should not need to modify this repository to onboard a new site
— per-site work happens in the sister repo.
The services this repo deploys, and where their code lives:
| Service | What it does | Repo |
|---|---|---|
| Engine | Runs on each Raspberry Pi. Analyzes the camera streams, detects smoke and pushes alerts to the alert API. Ships with pyro-camera-api, a local API to control the cameras. |
pyro-engine |
| Alert API | Central FastAPI + PostgreSQL service. Stores organizations, cameras, detections and alerts; engines authenticate against it to post detections. | pyro-api |
| Platform | React web app used to monitor and acknowledge alerts (docker image pyronear/pyro-platform-react). |
new-pyro-platform |
| MediaMTX | Media server relaying the live camera streams; each engine registers a stream path so streams are viewable from the platform. | bluenviron/mediamtx (config managed here) |
| OpenVPN | VPN server; every engine and API server gets a client, so the whole fleet is reachable remotely over the VPN. | ansible-role-openvpn |
| Traefik | Reverse proxy and TLS termination in front of the services on the combined server. | role in this repo |
| Reverse SSH | Fallback access: each engine keeps an SSH tunnel open to a bastion server, used when the VPN is down. | role in this repo |
| Service | What it does | Repo |
|---|---|---|
| Temporal API | Serves the temporal smoke-detection model (docker image pyronear/temporal-model-api), used to confirm detections over time. |
temporal-model |
| Annotation | Annotation API + web app to label detection sequences and build training datasets. | pyro-annotator |
Ansible runs inside a Docker container (pyro-ansible). The sister repo's
inventory, host_vars and group_vars are bind-mounted straight into the
container (see docker-compose.yml), so there is no copy step to run.
- Clone the sister
pi-manager-Xrepo next to this one. - Create a
.envat the repo root from.env.templateand setREPO_PATH,VAULT_PASSWORD_FILEandSSH_PRIVATE_KEY_FILE(see.env.template). - To seed the API database for a new site, fill
init_script/.env(copyinit_script/.env.ex).
Build the image and drop into a shell inside the container:
make ansible-upFrom that shell, run any target below. The operational targets only work inside
the container; running them on the host fails fast with a reminder. When you're
done, exit the shell and stop the container from the host with make ansible-stop.
make ping # ping all hosts in inventory/hosts_prod
make check-engines # verify engine containers are running + healthy
make deploy-all-engines # deploy-engines.yml on every engine
make deploy-one-engine SITE=<host> # deploy-engines.yml on a single host
make deploy-one-engine-preprod SITE=<host> # same play, against inventory/hosts_preprod
make init-one-engine SITE=<host> # rpi-init.yml on a single host (+ mediamtx)
make init-pi-zero SITE=<host> # rpi-init-pi-zero.yml on a single Pi Zero
make up / make down # start / stop engine docker-compose stacks fleet-wide
make sync-ssh-keys # push authorized SSH keys to all hostsWhat the main targets actually do:
init-one-engine— one-time OS setup of a new Pi: hostname, wifi, docker, OpenVPN client, reverse SSH tunnel, Grafana Alloy monitoring and static IP. Also registers the site's stream path on the mediamtx server.deploy-one-engine/deploy-all-engines— (re)deploys the engine application: pulls thepyro-engineandpyro-camera-apiimages, fetches a fresh API token per camera, writescredentials.jsonand.env, then restarts the docker-compose stack. Also installs the daily-reboot and watchdog cron jobs. Run it again to ship an engine update.check-engines— verifies the engine containers are up and healthy on every engine; use it after a deploy or when a site looks silent.init-pi-zero— sets up a Pi Zero (wifi, static IP, watchdog scripts). Engines and Pi Zeros watchdog each other through power relays — see watchdog-setup.
- Seed the API database from
init_script/(create_orga.py,create_user.py,create_cameras.py) and write the returned IDs into the site'shost_varsin the sister repo (site_config/app.pycan generate it). make init-one-engine SITE=<host>— base system, VPN, static IP.make deploy-one-engine SITE=<host>— deploy the detection stack.make check-engines— confirm the containers are running and healthy.
The full walkthrough lives in How to configure a new raspberry.
make install-openvpn # deploy-servers.yml -l openvpn
make install-mediamtx # deploy-servers.yml -l mediamtx_server
make update-mediamtx-conf # refresh mediamtx streams config only
make install-annotation-server
make install-platform-react-server
make install-platform-react-preprod-server # same plays, against inventory/hosts_preprod
make install-alert-api-server
make install-alert-api-preprod-server # same plays, against inventory/hosts_preprod
make install-temporal-server
make install-temporal-preprod-server # same play, against inventory/hosts_preprod
make install-combined-server # deploy-combined.yml: traefik + mediamtx + platform + alert API on one VMinstall-openvpn sources init_script/.env before running.
To redeploy a single service on the combined server (e.g. ship an API update) without touching the others:
make update-combined-api # alert API only
make update-combined-platform # platform only
make update-combined-mediamtx # mediamtx only
make update-combined-traefik # traefik onlyinstall-combined-server deliberately does not install an OpenVPN client
(no reboot on a routine deploy). If the alert API on the combined VM must reach
VPN-only engines, install the client with the opt-in vpn tag:
ansible-playbook playbooks/deploy-combined.yml -i inventory/hosts_prod \
-l combined_server --tags vpnThis installs the client (cert generated on the OpenVPN server), routes the
192.168.255.0/24 engine subnet over the tunnel, applies the OpenVPN 2.6+ DCO
fix, enables the service on boot, and reboots the VM. It requires
openvpn_client_password in the combined host's host_vars vault (the same
value the engines use).
To try a PR branch of a service before it is merged, build its docker image(s) from that branch and push them to Docker Hub tagged with the branch name, then point the target host at that tag and redeploy:
make build-push COMPONENT=api BRANCH=<branch> # pyro-api -> pyronear/alert-api:<tag> (amd64)
make build-push COMPONENT=engine BRANCH=<branch> # pyro-engine -> pyro-engine + pyro-camera-api :<tag> (arm64)
make build-push COMPONENT=platform BRANCH=<branch> # new-pyro-platform -> pyronear/pyro-platform-react:<tag> (amd64)The tag is the branch name without its type prefix, sanitized for docker
(feat/my-fix -> my-fix); the script prints it. Tags that could overwrite
a release (latest, main, version numbers like 1.0.11) are refused.
Unlike the deploy targets, this runs on the host: it needs the docker
daemon (with buildx) and a Docker Hub login with push rights on the
pyronear organization.
Cross-architecture builds need QEMU/binfmt (Docker Desktop ships it; on bare
Linux install qemu-user-static).
To deploy it, set the image tag in the sister repo to the pushed tag:
platform_react_docker_version / alert_api_docker_version in
inventory/group_vars/envpreprod/vars.yml, pyro_engine_docker_tag in the
engine's host_vars (host-level, engine_servers outranks envpreprod).
# 1. On the host: pass the full branch name, as shown on GitHub.
# Pushes pyronear/pyro-platform-react:my-specific-branch
make build-push COMPONENT=platform BRANCH=feat/my-specific-branch
# 2. In the sister repo, inventory/group_vars/envpreprod/vars.yml:
# platform_react_docker_version: my-specific-branch
# 3. In the container: redeploy (compose pulls the tag and recreates)
make install-platform-react-preprod-serverThe API works the same with COMPONENT=api, the alert_server group and
make install-alert-api-preprod-server. For engines, set
pyro_engine_docker_tag then make deploy-one-engine SITE=<host> — note
this target uses inventory/hosts_prod; for a host that only exists in the
preprod inventory, run the playbook directly:
ansible-playbook playbooks/deploy-engines.yml -i inventory/hosts_preprod -l <host>.
When you push the same branch again after new commits, the tag doesn't change, so compose keeps the stale local image (it only pulls missing images). Force the pull before redeploying — engines don't need this, the engine role always force-pulls:
ansible platform_react_server -i inventory/hosts_preprod -b \
-m community.docker.docker_image \
-a "name=pyronear/pyro-platform-react:<tag> source=pull force_source=true"Note for the platform: /config/app-config.js served to the browser is
templated by ansible (roles/platform_react), not taken from the image — a
branch that adds config keys needs them added to the template too, otherwise
the app falls back to its built-in defaults.
See How to configure a new raspberry.
- playbooks/ — playbooks per task (engine deploy, server deploy, checks).
- roles/ — custom and vendored roles.
- init_script/ — Python helpers to seed the API DB before onboarding a site.
- docker/ — Dockerfile, entrypoint and container-side
ansible.cfg/Makefile.
Sensitive data is stored vault-encrypted in the sister repo.