The build instructions in the documentation are tested for a native Linux
Machine. For MacOS or Windows consider creating a docker container build. One
of the developers uses the following devcontainer.json build environment:
Both components run in a container. The Leshan gateway (Leshan 2.0.0-M18)
runs in an eclipse-temurin:25-jre container and the Django server runs in
a python:3.14-slim container. This allows for an easy and reproducible setup
of the server.
Both components running in one machine using Podman Compose#
The following diagram shows the Container Environment. The file
compose.yml defines the services and their configuration. The file
leshan/Containerfile defines the Leshan container, Dockerfile.django
the Django container and Dockerfile.redis the Redis container. Compose runs
the images without source mounts, as in production.
The container can be built and started with the following commands:
makeimage-smoke-test starts the three images on a private network and
checks the admin login, static files, Celery through Redis, and the Leshan
health route from Django. CI runs the same test before it publishes the images.
GitHub Actions deploys flownexus to one Debian server with an Ansible playbook
(deploy/ansible). The server runs the CI-built images from GHCR as rootless
Podman containers with systemd Quadlets, behind nginx with a Let’s Encrypt
certificate. The server does not build images and needs no git checkout.
nginx serves up to four sites. Each site has its own host name, set by a
GitHub variable. No host name is fixed, and the host names need not share a
domain.
Variable
Default
Site
FLOWNEXUS_DASHBOARD_HOST
required
Django dashboard and API (reverse proxy to 127.0.0.1:8000)
FLOWNEXUS_FW_HOST
required
Firmware downloads from /binaries/ (IoT-safe TLS)
FLOWNEXUS_LANDING_HOST
off
flownexus landing page (utils/landing_page)
FLOWNEXUS_DOCS_HOST
off
This documentation (Sphinx HTML)
A customer deployment usually sets only the dashboard and firmware hosts. The
landing page and documentation are the flownexus project sites; when their
variable is empty, the playbook does not render their nginx site, and the
workflow does not build the documentation.
Devices download firmware from https://<FLOWNEXUS_FW_HOST>/binaries/....
The server sends only the path; the device firmware contains the host name. A
change of FLOWNEXUS_FW_HOST therefore needs a firmware that uses the new
host.
LwM2M CoAP (5683/udp) goes directly to the Leshan container and bypasses nginx.
The firmware download server (FLOWNEXUS_FW_HOST) is configured with
ssl_buffer_size1370 in nginx. This limits TLS record sizes to fit within a
single TCP segment (1500 byte MTU - IP/TCP headers - TLS overhead), which is
the industry standard for IoT-compatible HTTPS.
Constrained devices like the Nordic nRF9160 have a modem-offloaded TLS
engine with a maximum receive buffer of 2048 bytes for TLS records. Standard
web servers (including Go-based servers like Caddy) use dynamic TLS record
sizing that can send records up to 16KB, causing the modem to reject the
download with NRF_EMSGSIZE (errno 122). The ssl_buffer_size directive
in nginx is the server-side fix for this limitation.
HTTP/2 is intentionally disabled on the firmware server block because HTTP/2
framing adds overhead that can push effective TLS record sizes above device
limits. Gzip compression is also disabled since firmware binaries are not
compressible.
A push to main runs “System Tests”. It builds the Django, Redis and
Leshan images, runs the gateway contract check, the image smoke test and
the E2E scenarios, and pushes the images as
ghcr.io/<owner>/flownexus-<service>:sha-<shortsha>.
When that run succeeds, the “Deploy” workflow starts for the same commit.
It skips the deployment if FLOWNEXUS_DASHBOARD_HOST is not set, so forks
without a server do not fail.
The workflow checks that the three images exist and lists their digests in
the job summary. Then it runs deploy/ansible/site.yml over SSH.
The playbook configures the host, nginx and TLS, syncs the optional sites,
pulls the images and writes quadlets that reference the image digests. It
stops Django and Celery, backs up the database, runs the migrations once,
and restarts all services. It fails if Django or the Celery worker do not
answer.
The workflow checks each enabled site over HTTPS from the runner.
Deployments do not overlap. A manual run of “Deploy” (workflow_dispatch)
deploys the commit given in its sha input; this is also the rollback path
(see Rollback and Restore).
The playbook changes nginx only through a checked step: it renders the site,
runs nginx-t, and reloads nginx only if the test passes. If the test
fails, it restores the previous site file and stops the deployment, so the
running nginx keeps its configuration.
flownexus-django: web application (web command of the Django image)
flownexus-celery: Celery worker (worker command of the same image)
The migrations run in a one-off container (migrate command) before the
services restart. Into an empty database, migrate also loads the initial
data, which includes the admin user with a default password. Change that
password after the first deployment.
Set these in the repository under Settings → Secrets and variables →
Actions. The variables must be repository variables, because the workflow
reads FLOWNEXUS_DASHBOARD_HOST before it selects the production
environment. Secrets can be repository or production environment secrets.
Optional. If not set, the first deployment generates a key on the
server and keeps it.
The workflow pulls the images with its own GITHUB_TOKEN, so the GHCR
packages can stay private. Get the SSH_KNOWN_HOSTS value from a trusted
connection to the server, for example:
Generate a key pair without a passphrase for GitHub Actions. Add the public
key to ~flownexus/.ssh/authorized_keys (mode 0600, directory
0700) and the private key as the SSH_PRIVATE_KEY secret:
Set the GitHub variables and secrets, then run the “Deploy” workflow
manually or push to main.
The playbook enables ufw and allows 22/tcp, 80/tcp, 443/tcp and 5683/udp.
Set flownexus_manage_firewall:false in a manual inventory to keep an
existing firewall.
After the first deployment:
Change the admin password in the Django admin.
Check that https://<FLOWNEXUS_DASHBOARD_HOST>/ shows no debug pages.
makedeploy runs the same playbook from a workstation. Copy
deploy/ansible/inventory.example.yml to deploy/ansible/inventory.yml
(ignored by git), set the host names and the image prefix, then:
Add --check--diff--tagshost,nginx to the ansible-playbook command to
preview the host and nginx changes without applying them. The stack tag
cannot run in check mode, because it needs the pulled images.
makedeploy-test lints the playbook and tests the rendered nginx site in a
container for a customer setup (dashboard and firmware only) and for all four
sites. CI runs it on each pull request.
To roll back, run the “Deploy” workflow manually with the full 40-character
sha of an older commit. Its images must still exist in GHCR. The playbook
does not reverse migrations. If the older release cannot read the database,
also restore the backup that was made before the newer release.
If the backup or the migration fails, the deployment stops with Django and
Celery stopped, and the quadlets already reference the new images. Roll back
as above, and restore the backup if the migration changed the database.
Before each migration, the playbook writes
~flownexus/backups/db_<time>_<tag>.sqlite3 with sqlite3.backup and
keeps the newest 30 backups. To restore one:
Each deployment writes the deployed tag and image digests to
~flownexus/.local/state/flownexus/release.json and appends them to
releases.log in the same directory.
Servers that ran deploy/deploy-flownexus need these changes once:
Replace the sudoers rule for the script with the rule from Server Setup.
Set the GitHub variables. For flownexus.org, set
FLOWNEXUS_TLS_CERT_NAME=flownexus.org to keep the existing certificate.
On the first run, the playbook copies
~flownexus/flownexus/server/data/db.sqlite3 to ~flownexus/data if no
database exists there. It does not change the old copy.
After a successful deployment, ~flownexus/flownexus and the old
localhost/flownexus-* images are no longer used.