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 server is running in a
openjdk:17-slim container and the Django server is running in a
python:3.11-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
Dockerfile.leshan defines the Leshan container and the file
Dockerfile.django defines the Django container.
The container can be built and started with the following commands:
flownexus can be deployed to a virtual server using nginx as a reverse proxy
with certbot for automatic TLS certificates. The default configuration supports
four subdomains (using flownexus.org as the example domain – substitute
your own throughout):
flownexus.org - Main landing page (static HTML)
docs.flownexus.org - Documentation (Sphinx HTML)
fw.flownexus.org - Firmware download server (IoT-safe TLS)
The firmware download server (fw.<domain>) 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.
The production deployment uses systemd Quadlet instead of Podman Compose. Quadlet
provides native systemd integration for rootless containers, offering several advantages:
Automatic startup - Containers start on boot via systemd
Health checks - systemd monitors container health automatically
Logging - Centralized logging via journalctl
Crash recovery - Automatic restart on failure
SSH session independence - Containers persist after logout (via loginctlenable-linger)
Quadlet files are located in deploy/quadlet/ and define three services:
flownexus-redis - Redis cache and message broker
flownexus-leshan - LwM2M server with CoAP/DTLS endpoints
flownexus-django - Django application with Celery worker
Each service is defined as a .container file that systemd converts to a service unit.
Logs are captured by systemd journal, viewable with journalctl--user-uflownexus-<service>-f
(see Troubleshooting for details).
For local development and testing, use makerun-mock or
podman-compose-fserver/compose.ymlup which provides the same container
configuration but without systemd integration.
This section contains the required one-time manual bootstrap steps for a new
server. After that, normal deployments are handled by GitHub Actions via
deploy/deploy-flownexus.
Obtain a single SAN certificate covering all domains. Certbot will temporarily
use nginx to complete the ACME challenge, so nginx must be running and DNS must
already resolve to this server:
This creates one certificate lineage at
/etc/letsencrypt/live/flownexus.org/ with all four domains as SANs. The
deployment script renders nginx against that lineage path by default. Certbot
installs a systemd timer that renews it automatically before expiry.
Important
Pass --cert-nameflownexus.org exactly as shown above. Without an
explicit lineage name, certbot may reuse a different existing certificate
name such as dashboard.flownexus.org, which leaves the certificate valid
but causes deployment to fail because nginx is rendered against the expected
lineage path.
Note
If any domain uses a manually provisioned certificate (e.g. a private
CA for IoT device trust), skip it from the certbot invocation above and
instead copy the certificate files to /etc/nginx/ssl/ before deploying:
The deploy/nginx.conf for that server block must then reference
/etc/nginx/ssl/fw.crt and /etc/nginx/ssl/fw.key directly instead
of the Let’s Encrypt paths. This is done in the deployment-specific branch.
Add the deploy user to the www-data group so it can write files that nginx
serves:
Django reads its configuration from environment variables at runtime. This
separates deployment-specific settings from the application code, so the same
container image runs in development (with safe defaults) or production (with
hardened settings) without rebuilding.
The Django secret key
Django uses a secret key to cryptographically sign sessions, CSRF tokens, and
password reset links. If an attacker obtains the key, they can forge session
cookies and impersonate any user. The development default key (prefixed
django-insecure-) is public knowledge because it lives in the repository
– it must never be used on an internet-facing server.
The deploy script handles this automatically. On first run it:
Generates a cryptographically random 50-character key using Python’s
secrets module
Writes it to ~flownexus/.config/flownexus/env with permissions 0600
(readable only by the deploy user)
Skips generation on all subsequent deploys so the key is stable across
redeploys
The file is never committed to the repository or passed through GitHub Actions.
Verify it was created after the first deploy:
Never copy, share, or log this key. If it leaks, delete the file and
redeploy to rotate it. All existing sessions will be invalidated.
Other production environment variables
The remaining variables are set inline in
deploy/quadlet/flownexus-django.container. These are deployment config,
not secrets, so keeping them in version control is fine:
Variable
Production value
Purpose
DJANGO_DEBUG
False
Disables debug mode and detailed error pages
DJANGO_ALLOWED_HOSTS
dashboard.flownexus.org,localhost,127.0.0.1
Restricts Host headers Django will respond to
DJANGO_CSRF_ORIGINS
https://dashboard.flownexus.org
Trusted origins for CSRF checks
For local development (makerun-mock, tests), no environment variables are
needed – settings.py defaults are used automatically.
When deploying to a different domain, update these values in the quadlet file
on the relevant deployment branch.
Add deploy user to www-data group: sudousermod-aGwww-dataflownexus
Add your personal SSH public key to ~flownexus/.ssh/authorized_keys
Generate a dedicated SSH key pair for GitHub Actions (no passphrase):
ssh-keygen-ted25519-C"github-actions-deploy"
Add the Actions public key to ~flownexus/.ssh/authorized_keys
Add the Actions private key to GitHub: Settings → Secrets → SSH_PRIVATE_KEY
Configure passwordless sudo for the deploy script in
/etc/sudoers.d/flownexus-deploy and validate with visudo-cf
Open firewall: ports 22/tcp, 80/tcp, 443/tcp, 5683/udp, 5684/udp
Clone the repository into ~flownexus/flownexus
Remove the default nginx site: sudorm-f/etc/nginx/sites-enabled/default
DNS
Create an A record for each subdomain (dashboard, fw, docs,
<rootdomain>) pointing to the server IP
Wait for DNS propagation before running certbot (ACME challenge requires
valid DNS)
TLS certificates
Run certbot for a single SAN cert covering all domains and pin the lineage
name:
sudocertbotcertonly--nginx--cert-name<domain>-d<domain>-ddocs.<domain>-dfw.<domain>-ddashboard.<domain>
For manually provisioned certs (e.g. private CA for IoT), copy them to
/etc/nginx/ssl/ and reference them in the deployment-specific nginx config
First deployment
Run the deploy script: sudo~flownexus/flownexus/deploy/deploy-flownexus
(or push to the tracked branch to trigger GitHub Actions)
Deploy static files via rsync to /var/www/flownexus/
Update the backend checkout and run ``deploy/deploy-flownexus`` to refresh code, nginx, and containers
The workflow updates the backend checkout, then runs the deploy script on the
server. The script creates the required directories, installs the nginx
configuration, and restarts the stack. The Django container stores its SQLite
database in ~flownexus/flownexus/server/data/db.sqlite3 and stores uploaded
firmware in /var/www/flownexus/binaries.
Required GitHub Secrets:
SSH_PRIVATE_KEY - Private key for the deploy user
The workflow uses sudo-n only to invoke deploy/deploy-flownexus.
That script performs the privileged server setup and restart steps in one place.
To deploy to a different domain or server layout, update deploy/nginx.conf
and the environment values in .github/workflows/deploy.yml.
To deploy on a different domain, update the domain names in
deploy/nginx.conf, the environment variables (DJANGO_ALLOWED_HOSTS,
DJANGO_CSRF_ORIGINS) in deploy/quadlet/flownexus-django.container, and
the target server in .github/workflows/deploy.yml.