Build and Deploy#
Build Setup#
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:
{
"name": "Ubuntu",
"image": "mcr.microsoft.com/devcontainers/base:jammy",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"runArgs": [
"--cap-add=NET_ADMIN",
"--cap-add=MKNOD",
"--device=/dev/net/tun",
"--sysctl=net.ipv6.conf.all.disable_ipv6=0",
"--sysctl=net.ipv6.conf.default.disable_ipv6=0"
],
"postCreateCommand": "apt-get update && apt-get install -y iproute2 && echo 'IPv6 is enabled.'",
"remoteUser": "root"
}
Before you we start with any development here are a few things you should get configured:
Get the Zephyr SDK downloaded and configured in your root directory. You can find the instructions here.
Setup a virtual environment for the project.
host:~$ sudo apt update && sudo apt upgrade
host:~$ sudo apt install python3-pip python3.10-venv
host:~$ python3.10 -m venv venv
host:~$ source venv/bin/activate
host:~$ pip install --upgrade pip && pip install west
host:~$ mkdir workspace && cd workspace
host:~/workspace$ west init -m https://github.com/flownexus-lwm2m/flownexus --mr main
host:~/workspace$ west update
Container 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.
Warning
Make sure to change the password to the admin console as well as other settings like SECRET_KEY, DEBUG flag in a production environment!
The container can be built and started with the following commands:
host:~/workspace/flownexus$ make server-build
host:~/workspace/flownexus$ podman-compose -f server/compose.yml up
Production Deployment#
flownexus can be deployed to a virtual server using Caddy as a reverse proxy. This deployment supports four subdomains:
flownexus.org - Main landing page (static HTML)
docs.flownexus.org - Documentation (Sphinx HTML)
fw.flownexus.org - Firmware download server
dashboard.flownexus.org - Dynamic dashboard (Django application)
Architecture Overview#
The deployment uses a split traffic routing approach:
HTTP/HTTPS (Ports 80/443): Handled by Caddy reverse proxy
UDP Traffic (Ports 5683/5684): Directly bound to host, bypassing Caddy for LwM2M
Internet
│
├──→ Caddy (Ports 80/443)
│ ├──→ flownexus.org (static)
│ ├──→ docs.flownexus.org (static)
│ ├──→ fw.flownexus.org (static + browse)
│ └──→ dashboard.flownexus.org (reverse proxy → localhost:8000)
│
└──→ LwM2M UDP (Ports 5683/5684) → Direct to Leshan container
├──→ 5683/udp - CoAP (unencrypted)
└──→ 5684/udp - DTLS/CoAPS (encrypted)
This architecture provides:
Caddy - Reverse proxy with automatic HTTPS
Podman - Container runtime (rootless)
Podman Compose - Container orchestration
GitHub Actions - CI/CD pipeline
Server Requirements#
Linux server (tested on Debian 13)
Domain name with DNS A/AAAA records pointing to server
Minimum: 1 vCPU
Initial Server Setup#
This section contains the required one-time manual bootstrap steps for a new server. After these steps are complete, normal application deployments are handled by GitHub Actions.
Install required packages:
vserver:~$ sudo apt update && sudo apt upgrade -y
vserver:~$ sudo apt install -y podman podman-compose caddy git curl rsync ufw
Create a non-root user:
Important: Complete this step while logged in as root (or your initial admin user).
Create a dedicated user for running the application instead of using root:
vserver:~$ sudo useradd -m -s /bin/bash flownexus
vserver:~$ sudo passwd flownexus
vserver:~$ sudo usermod -aG sudo flownexus
Note: Log out and log back in for the sudo group membership to take effect.
Allow the GitHub Actions deploy key to run the required deployment commands without an interactive password prompt:
vserver:~$ sudo tee /etc/sudoers.d/flownexus-deploy > /dev/null <<'EOF'
flownexus ALL=(root) NOPASSWD: /usr/bin/rsync
flownexus ALL=(root) NOPASSWD: /usr/bin/mkdir
flownexus ALL=(root) NOPASSWD: /usr/bin/chown
flownexus ALL=(root) NOPASSWD: /usr/bin/chmod
flownexus ALL=(root) NOPASSWD: /usr/bin/mv
flownexus ALL=(root) NOPASSWD: /usr/bin/systemctl reload caddy
flownexus ALL=(root) NOPASSWD: /usr/bin/systemctl restart caddy
flownexus ALL=(root) NOPASSWD: /usr/bin/systemctl is-active caddy
flownexus ALL=(root) NOPASSWD: /usr/bin/caddy validate --config /tmp/Caddyfile.new
EOF
Validate the sudoers file before continuing:
vserver:~$ sudo visudo -cf /etc/sudoers.d/flownexus-deploy
Set up SSH access for the new user. Since the user is new, add your SSH key manually:
# Create .ssh directory and add your public key
vserver:~$ sudo mkdir -p ~flownexus/.ssh
vserver:~$ echo "YOUR_PUBLIC_KEY_HERE" | sudo tee ~flownexus/.ssh/authorized_keys
# Fix permissions (important!)
vserver:~$ sudo chmod 700 ~flownexus/.ssh
vserver:~$ sudo chmod 600 ~flownexus/.ssh/authorized_keys
vserver:~$ sudo chown -R flownexus:flownexus ~flownexus/.ssh
Replace YOUR_PUBLIC_KEY_HERE with your actual public key content from
~/.ssh/id_ed25519.pub on your local machine.
Now you can SSH as the flownexus user:
local:~$ ssh flownexus@your-server-ip
Generate SSH Key for GitHub Actions (on your local machine):
Before setting up GitHub Actions, generate a dedicated SSH key pair:
# Generate a new SSH key (do NOT add a passphrase)
local:~$ ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/flownexus_deploy
# This creates two files:
# ~/.ssh/flownexus_deploy (private key - keep secret!)
# ~/.ssh/flownexus_deploy.pub (public key - copy to server)
# Copy the public key to the server (as root or flownexus user)
local:~$ cat ~/.ssh/flownexus_deploy.pub
# On the server, add this to authorized_keys:
vserver:~$ echo "PASTE_PUBLIC_KEY_HERE" >> ~flownexus/.ssh/authorized_keys
# Add the private key to GitHub Secrets:
# 1. Go to GitHub repo → Settings → Secrets and variables → Actions
# 2. Click "New repository secret"
# 3. Name: SSH_PRIVATE_KEY
# 4. Value: Copy contents of ~/.ssh/flownexus_deploy (private key)
# 5. Click "Add secret"
Clone the repository:
vserver:~$ cd ~flownexus
vserver:~flownexus$ git clone https://github.com/flownexus-lwm2m/flownexus.git
Create directory structure:
vserver:~$ sudo mkdir -p /var/www/flownexus/{landing,docs,binaries}
vserver:~$ sudo mkdir -p ~flownexus/flownexus/server/data
vserver:~$ sudo chown -R caddy:caddy /var/www/flownexus
vserver:~$ sudo chown -R flownexus:flownexus ~flownexus/flownexus/server/data
vserver:~$ sudo mkdir -p /var/log/caddy
Configure firewall:
vserver:~$ sudo ufw enable
vserver:~$ sudo ufw allow 22/tcp # SSH
vserver:~$ sudo ufw allow 80/tcp # HTTP
vserver:~$ sudo ufw allow 443/tcp # HTTPS
vserver:~$ sudo ufw allow 5683/udp # LwM2M CoAP (unencrypted)
vserver:~$ sudo ufw allow 5684/udp # LwM2M DTLS/CoAPS (encrypted)
Optional Manual Backend Start#
This section is not required for normal deployments. Use it only for the first local smoke test on a fresh server or for manual recovery/debugging.
Start the containers:
vserver:~flownexus/flownexus$ export APP_VERSION=$(git describe --always --dirty --tags)
vserver:~flownexus/flownexus$ podman-compose -f server/compose.yml up -d --build
Verify services are running:
vserver:~flownexus/flownexus$ podman ps
vserver:~flownexus/flownexus$ curl http://localhost:8000/admin/login/
vserver:~flownexus/flownexus$ curl http://localhost/admin/login/
Automated CI/CD#
This is the normal deployment path after the initial server bootstrap. When code is pushed to the main branch:
Build static assets locally (landing page, Sphinx documentation)
Deploy static files via rsync to
/var/www/flownexus/Deploy Caddyfile and reload Caddy configuration
SSH to server and run
git pull origin mainto update backend codeBuild and restart containers with
podman-compose -f server/compose.yml up -d --build
The backend code is pulled directly from the repository on the server, ensuring the deployed code matches the git commit exactly.
The Django container stores its SQLite database in
~flownexus/flownexus/server/data/db.sqlite3 and stores uploaded
firmware directly in /var/www/flownexus/binaries so the same files are
immediately available through the firmware download host. These paths are
configured via DJANGO_DB_HOST_PATH and FIRMWARE_STORAGE_HOST_PATH in
the deployment environment; local development falls back to ./data and
./firmware inside server/.
Required GitHub Secrets:
SSH_PRIVATE_KEY- Private key for the deploy user
The workflow uses sudo -n on the server for rsync, Caddy validation,
directory ownership fixes, and Caddy reload/health checks. The deploy user must
therefore have the matching passwordless sudo rules configured as shown above.
To deploy to a different domain or server layout, update deploy/Caddyfile
and the environment values in .github/workflows/deploy.yml.
Optional Certificate Maintenance#
Caddy automatically manages Let’s Encrypt certificates. No manual intervention
is required. Certificates are stored in /var/lib/caddy/.
Use the following only if you need to troubleshoot or force a renewal:
vserver:~$ sudo caddy reload --config /etc/caddy/Caddyfile
Troubleshooting#
Everything in this section is optional and only needed for debugging a broken deployment or validating a suspicious state.
Caddy Issues#
Check Caddy logs:
vserver:~$ sudo journalctl -u caddy -f
Validate configuration:
vserver:~$ sudo caddy validate --config /etc/caddy/Caddyfile
Test Caddy locally:
vserver:~$ sudo caddy run --config /etc/caddy/Caddyfile
Container Issues#
Check container status:
vserver:~$ podman ps -a
View logs:
vserver:~$ podman logs flownexus-django
vserver:~$ podman logs flownexus-leshan
vserver:~$ podman logs flownexus-redis
Restart specific service:
vserver:~$ podman restart flownexus-django
SSL Certificate Issues#
Caddy handles SSL automatically. If certificates fail:
# Force certificate renewal
vserver:~$ sudo caddy reload --config /etc/caddy/Caddyfile
# Check certificate status
vserver:~$ sudo caddy list-modules | grep tls
Security Considerations#
This section is guidance to review before exposing the stack publicly. It is not an extra deployment procedure, but these items still matter for a real internet-facing setup.
Required open ports:
Port 22, TCP: SSH access
Port 80, TCP: HTTP (redirects to HTTPS)
Port 443, TCP: HTTPS
Port 5683, UDP: LwM2M CoAP (unencrypted)
Port 5684, UDP: LwM2M DTLS/CoAPS (encrypted)
Security Best Practices:
SSH Keys: Use ed25519 keys for deployment:
ssh-keygen -t ed25519 -a 100File Permissions: Ensure
/var/www/flownexusis owned bycaddy:caddyFirewall: Only open required ports (22, 80, 443, 5683/udp, 5684/udp)
Updates: Regularly update Caddy and container base images
Secrets: Never commit secrets to the repository
Before deploying to production:
Change the Django
SECRET_KEYin production settingsDisable Django
DEBUGmodeUse strong passwords for admin accounts
Configure firewall rules
Set up automated backups
Customizing for Your Deployment#
This section is only needed if you want to change the default domains, server paths, or deploy user.
To deploy on a different server or domain, update these files:
deploy/Caddyfile#
Change the configured domains and the Let’s Encrypt email address:
webmaster@flownexus.org
flownexus.org
docs.flownexus.org
fw.flownexus.org
dashboard.flownexus.org
.github/workflows/deploy.yml#
Adjust the deployment environment values near the top of the file:
env:
DEPLOY_USER: flownexus
SERVER_HOST: flownexus.org
APP_ROOT: ~flownexus/flownexus
DJANGO_DB_HOST_PATH: ~flownexus/flownexus/server/data
FIRMWARE_STORAGE_HOST_PATH: /var/www/flownexus/binaries
Also update any username or home-directory references in the server setup commands throughout this chapter.
When following the server setup instructions, replace:
flownexuswith your desired username~flownexuswith your user’s home directoryServer IP addresses in SSH commands
After making these changes, commit and push to trigger deployment.
File Locations#
File |
Location |
Purpose |
|---|---|---|
Static website |
|
Landing page |
Documentation |
|
Sphinx HTML |
Firmware |
|
Firmware downloads |
SQLite database |
|
Persistent Django data |
Caddy config |
|
Reverse proxy |
Caddy data |
|
SSL certificates |
Container config |
|
Podman services |
Logs |
|
Access logs |
Maintenance#
Everything in this section is optional day-2 operations guidance.
Updating Containers#
Usually GitHub Actions performs updates for you. Use these commands only when you intentionally want to update or recover the server manually.
vserver:~flownexus/flownexus$ git pull
vserver:~flownexus/flownexus$ export APP_VERSION=$(git describe --always --dirty --tags)
vserver:~flownexus/flownexus$ podman-compose -f server/compose.yml up -d --build
Cleaning Up#
Use these commands only for manual housekeeping.
# Remove old container images
vserver:~$ podman image prune -a
# Clean up volumes
vserver:~$ podman volume prune
Warning
flownexus is not production ready. This server setup is only intended for testing purposes. Review and harden the configuration before using in production.