How to Build NomadNet Nodes on a Raspberry Pi: A Complete Tutorial

A step-by-step guide to building Reticulum mesh network nodes that serve pages, RSS feeds, guestbooks, and emergency alerts over radio and TCP. Written for someone who can follow commands in a terminal but doesn't necessarily know what every command does yet.
By the end of this tutorial you'll have:
A working Reticulum mesh network on a Raspberry Pi
At least one NomadNet node serving pages
Dynamic pages with forms and user-submitted content
Automated RSS feed fetching
Multiple independent nodes running on the same Pi
Automatic backups to a USB drive
A framework for adding more nodes and pages
This tutorial can be given to Claude (or any capable LLM assistant) as context. If you get stuck, paste this document in alongside your question and the assistant will have enough background to help.
Part 1: Understanding What You're Building
What is Reticulum?
Reticulum is a decentralised networking stack. Think of it like the internet, but built from the ground up to run over anything: long-range radio (LoRa), short-range radio, WiFi, I2P tunnels, plain old TCP. Every node on the network has a cryptographic identity. Every message is encrypted end-to-end by default.
You don't need an ISP for Reticulum to work. Two devices with LoRa radios can talk to each other directly over kilometres of distance, and if a third device is in range of both, it can act as a relay. The network keeps working even if the internet is down.
What is NomadNet?
NomadNet is a piece of software that runs on top of Reticulum. It does two things:
Lets you send encrypted messages to other Reticulum users
Serves pages from your device that other Reticulum users can browse
Think of the page server like a tiny dark-web site, except it runs over Reticulum instead of Tor. People don't type in a domain name. They browse to your node's cryptographic address (a short string like 4710b86ba18c42d1d5f30f74fe509286) and see whatever pages you're hosting.
What is MeshChat?
MeshChat is a browser-based front-end for Reticulum. Instead of a terminal UI, you get a clean web interface where you can send messages, see who else is on the network, and browse other people's NomadNet nodes. You'll use MeshChat to browse your own pages and confirm they look right.
Why build multiple nodes?
One Pi can run multiple NomadNet instances, each with its own identity and purpose. In this tutorial you'll build three:
A main node (for a guestbook, visitor count, tutorial pages)
A news node (aggregates RSS feeds from Canadian news sources)
An emergency warnings node (aggregates official alert feeds)
Each node announces itself separately on the network, so someone browsing from another city would see three distinct nodes, not one big one.
Part 2: What You Need
Hardware
A Raspberry Pi. A Pi 3B+ or newer works fine. You want at least 1GB of RAM.
A microSD card, 32GB or larger. Get a decent brand (SanDisk, Samsung). Cheap SD cards fail.
A power supply for the Pi.
Network access. Ethernet cable or WiFi both work.
(Optional but recommended) A USB stick for backups. 8GB is plenty.
(Optional) A LoRa radio device like a LilyGo LoRa32 or Heltec LoRa32 if you want radio connectivity. This tutorial works without one, using TCP over the internet only.
Software skills
You should be comfortable with:
Opening a terminal
Running commands over SSH
Editing text files (using
nanois fine, no need for vim)Copying files between your laptop and the Pi using
scp
If you've never SSH'd into a computer, spend an hour learning that first. The rest of this tutorial assumes you have it down.
Time
First-time setup takes 4 to 6 hours if nothing breaks. Budget a full weekend for your first attempt. It gets much faster the second time.
Part 3: Flashing the SD Card
Step 1: Install Raspberry Pi Imager
Download Raspberry Pi Imager from raspberrypi.com. Install it on your laptop.
Step 2: Configure the Image
Plug your SD card into your laptop. Open Raspberry Pi Imager and:
Choose device: your Pi model
Choose OS: Raspberry Pi OS Lite (64-bit). "Lite" means no desktop, which is what you want for a server.
Choose storage: your SD card
Before you click Write, click the Settings gear icon. You need to pre-configure a few things:
Set hostname: pick something memorable. You'll use this to SSH in. Examples:
my-node,mesh-pi,nodeserver. Avoid dots, spaces, special characters. In this tutorial we'll usemynodeas the example hostname.Set username and password: pick a username you'll remember. In this tutorial we'll use
useras the example. Use a strong password.Configure wireless LAN: if using WiFi, enter your network SSID and password.
Enable SSH: turn this on. Password authentication is fine for a home network.
Set locale: pick your timezone.
Click Save, then Write. This takes about 5-10 minutes.
Step 3: Boot the Pi
When writing is done, eject the card, put it in the Pi, plug in network cable (or just power for WiFi), plug in power. Wait 3-5 minutes for first boot.
Step 4: Connect via SSH
From your laptop's terminal:
Replace user with whatever username you set, and mynode with your hostname. Type yes to accept the SSH key, then enter your password.
If it says "connection refused", wait another 2 minutes and try again. First boot does a bunch of setup work that can temporarily block SSH.
If it says "could not resolve hostname", your router may not be doing mDNS properly. Find the Pi's IP address from your router admin page and SSH to that IP directly instead.
Part 4: Installing Reticulum
Step 1: Update the system
First things first, update everything:
This takes about 10 minutes on a Pi 3. Let it finish.
Step 2: Install Python tools and dependencies
Step 3: Add swap space (recommended for Pi 3)
The Pi 3B+ only has 1GB of RAM. Running multiple Python services can push it into out-of-memory territory. Add 512MB of swap as a safety net:
This creates a 512MB file at /var/swap, turns it into swap space, and adds it to /etc/fstab so it's automatically enabled on boot.
Step 4: Install Reticulum itself
The --break-system-packages flag is needed on recent Raspberry Pi OS versions because Debian tries to prevent pip from installing packages globally. For our purposes, it's fine.
Step 5: Fix the PATH
Newly installed Python scripts go into ~/.local/bin/ but that's not in your shell's search path by default. Fix that:
Test it:
Should return something like /home/user/.local/bin/rnsd. If it says "not found", the PATH setup didn't take effect.
Step 6: Configure Reticulum
Now configure Reticulum to connect to the global mesh network. Create the config file:
Paste this in:
Save with Ctrl+O, Enter, then Ctrl+X.
What this does: The three TCP interfaces connect you to public community nodes that bridge between isolated Reticulum mesh segments. Without these, your Pi can only talk to other devices on your local network.
Step 7: Run rnsd as a service
rnsd (Reticulum Network Stack Daemon) is the background process that keeps the network running. Run it as a systemd service so it starts automatically at boot:
Important: replace user with your actual username in both the User= line and the ExecStart= path. If you're not sure, run whoami to check.
Enable and start the service:
Check it's running:
You should see active (running) in green.
Step 8: Verify the network is live
Wait 30 seconds for connections to establish. You should see all three TCP interfaces with Status: Up. If any are Down, your ISP might be blocking that port (some ISPs block non-standard ports). One out of three working is enough to stay connected.
Part 5: Installing MeshChat
MeshChat is a web-based browser for Reticulum. You'll use it to browse your NomadNet nodes as you build them.
Step 1: Clone and install
Step 2: Set up as a service
Replace user with your username in three places.
Step 3: Access MeshChat
From your laptop's browser, go to:
Replace mynode with your Pi's hostname. If that doesn't work, try the Pi's IP address instead. You should see the MeshChat interface.
Part 6: Your First NomadNet Node
Step 1: Install NomadNet
Step 2: Generate the default config
Run NomadNet once to create its config directory. Let it start, then kill it:
This creates ~/.nomadnetwork/ with default config files.
Step 3: Enable node hosting
Open the config:
Find the [node] section (use Ctrl+W to search). Change these settings:
What each setting means:
enable_node: makes your device actually serve pages. Without this, NomadNet runs but doesn't host.node_name: what other people see when your node announces itself. Can include emoji.announce_interval: how often (in minutes) your node broadcasts its existence. Lower numbers mean new people find you faster, but use more airtime.announce_at_start: send an announce the moment the service starts, don't wait for the first interval.
Save and exit.
Step 4: Run as a service
Replace user with your actual username. Should show active (running).
Step 5: Find your node in MeshChat
In MeshChat, click the Announces tab (looks like a broadcast tower icon). Wait up to 2 minutes for your node to show up. You should see "My First Node" (or whatever you called it) appear.
Click it, and you'll see... probably nothing, because you haven't made any pages yet. That's what the next section is about.
Part 7: Understanding Micron Markup
Pages in NomadNet are written in a markup language called Micron. It's designed to work over extremely low-bandwidth links, so it's minimal.
Static vs Dynamic Pages
There are two kinds of pages:
Static pages: plain text files with Micron markup. Just text. The server sends them as-is.
Dynamic pages: executable scripts (usually Python) that print Micron markup to standard output. The server runs them and sends whatever they print.
The difference comes down to whether the file has the executable permission bit set. Static pages should NOT be executable. Dynamic pages MUST be executable.
This is important and has bitten me more than once. If you accidentally make a static page executable, NomadNet tries to run it as a script, fails, and the page hangs forever with no error message.
Micron Formatting Codes
Every formatting code starts with a backtick (`):
Code | Effect |
|---|---|
| Center align |
| Left align |
| Toggle bold |
| Toggle italic |
| Toggle underline |
| Foreground colour, 3-digit hex (e.g. |
| Background colour, 3-digit hex |
| Reset foreground |
| Reset background |
`` (two backticks) | Reset everything |
| Heading (starts with |
| Horizontal divider |
Important: bold is `! not `*. The asterisk is italic. Using asterisks inside link labels can break your page because the parser interprets them as formatting commands.
Links
The path must include /page/ for internal pages. Example:
Simple Index Page Example
The double backtick `` after the red-banner heading resets all formatting so the rest of the page isn't still red and on a red background.
Part 8: Writing Static Pages
Pages live in ~/.nomadnetwork/storage/pages/. The main page is index.mu.
Step 1: Create the pages directory
Step 2: Create your index page
Paste the example index page from the previous section. Save and exit.
Step 3: Make sure it's NOT executable
The 644 permission means readable by everyone, writable only by you, not executable. This is correct for static pages.
Step 4: Reload NomadNet
NomadNet scans the pages directory on startup. Restart it to pick up new pages:
Step 5: View your page
In MeshChat, go to your node again. You should now see your index page rendered.
Adding more static pages
Just create more .mu files in the same directory. Link to them from index.mu using the `[Label`:/page/filename.mu] syntax. Give them all 644 permissions.
Example: create about.mu:
Then:
No NomadNet restart needed for individual pages. Just refresh in MeshChat.
Part 9: Writing Dynamic Pages
Dynamic pages are Python scripts. They do whatever you want, then print Micron markup to stdout. Common uses:
A visitor counter that increments on each page load
A guestbook where people leave messages
An RSS feed reader that displays news headlines
A form that accepts input and stores it
The Basic Structure
Every dynamic page looks like this:
Two important lines at the top:
#!/usr/bin/env python3is the shebang line. This tells Linux "run this file using Python 3". Every executable Python script needs this.#!c=0is a NomadNet directive that disables caching. Without this, dynamic pages might return stale content because NomadNet remembers their output. Always include this for dynamic pages.
Example: Visitor Counter
Create counter.mu:
Save, then make it executable:
The 755 permission means executable by everyone, writable only by you. Required for dynamic pages.
Test it from the command line:
You should see the Micron markup printed. Each run increments the counter.
Add a link to it from your index.mu:
Browse to it in MeshChat. Refresh the page a few times. The count goes up.
Part 10: Forms and Guestbooks
This is where NomadNet gets interesting. You can accept input from visitors.
How Forms Work
NomadNet passes form field values to your script as environment variables. If you have a field called username, its value shows up as an environment variable ending in username (the exact prefix varies by client).
The trick is that MeshChat and NomadNet's own TUI use slightly different prefixes. A robust script scans all environment variables looking for any that end with your field name.
The Guestbook Script
Make it executable:
Understanding the Form Syntax
Look at these three lines:
The `<username`> creates a text input field named username.
The `<40|message`> creates a text input 40 characters wide named message.
The submit button syntax is:
The pipe-separated list after the second backtick tells NomadNet which form fields to submit.
Sanitisation
The script does three important things for security:
Strip backticks:
`is the Micron markup character. If a visitor types a backtick into their message, it could break the page rendering. Remove them.Strip newlines: messages should be one line. Newlines could break the log format.
Truncate length: cap nickname at 20 characters, message at 120. Prevents someone pasting in a novel.
Add a link to it from index.mu and browse from MeshChat. You should be able to type a nickname and message, hit Submit, and see your message appear in the list.
Part 11: Running Multiple Nodes
One Pi can run multiple NomadNet instances, each with its own identity, name, and pages. This is where things get powerful.
Each instance needs:
Its own config directory (like
~/.nomadnetwork_news/instead of~/.nomadnetwork/)Its own systemd service with a different name
The
--configflag on the nomadnet command line pointing at the right directory
Step 1: Create a new config directory
Let's build a "News Node":
Step 2: Generate the config
Run NomadNet once with the new config path to create a default config:
Replace user with your actual username.
Step 3: Edit the new config
Find the [node] section and change:
Step 4: Create the new systemd service
Step 5: Verify both are running
Should print active twice.
In MeshChat, you should now see two nodes in your Announces tab: your original node and the News Node.
Add pages to the news node the same way as before, but in the new directory:
Part 12: Aggregating RSS Feeds
Now let's do something useful with the news node: pull in real RSS feeds from external sources, cache them locally, and display them.
Why cache?
Every time someone browses your news page, we could have the script fetch the RSS feed live. But that would:
Be slow (waits on internet)
Consume bandwidth unnecessarily
Break if the internet was down (defeating the point of mesh networking)
Instead, a separate script runs every 30 minutes on a schedule. It fetches all feeds and writes them to cache files. The page script just reads the cache. The page loads instantly even if the internet is down.
Step 1: Write the fetcher
Step 2: Run it manually to test
You should see [OK] lines for each feed. Any [ERR] means that specific feed couldn't be reached.
Step 3: Create the feed display page
Make it executable:
Step 4: Update the news index
Note: create feed_globemail.mu using the feed_cbc.mu template, just changing CACHE_FILE and FEED_NAME.
Step 5: Automate fetching with cron
Add:
Replace user with your username. This runs the fetcher every 30 minutes and logs output to a file.
Check the cron job is installed:
Part 13: Backups
If your SD card dies, you lose everything. Even if you haven't run for long, back up early and often. Use a USB drive plugged into the Pi.
Step 1: Prepare the USB drive
Plug in a USB drive. Find its device name:
Look for something like sda with a partition sda1. That's your USB drive (as opposed to mmcblk0 which is the SD card).
Check what filesystem it has:
If it's FAT32 or exFAT, we'll reformat to ext4 for cleaner Linux permissions. If there's anything you want to keep on the drive, copy it off first.
Step 2: Format to ext4
Type y to confirm. This takes about 30 seconds.
Step 3: Set up persistent mount
Get the UUID:
Copy the UUID value. It looks like d64d7bf2-b1e7-4369-a37c-002a54d22f29.
Create the mount point:
Add to fstab so it mounts automatically on boot:
Replace YOUR-UUID-HERE with your actual UUID.
What nofail does: if the USB drive isn't plugged in at boot, the Pi still boots normally. Without this, a missing USB drive can drop the Pi into recovery mode. Include it.
Mount it:
Replace user with your username.
Important detail: chown must run AFTER mounting. If you chown the empty mount point before mounting the drive, the drive's own root filesystem gets mounted on top and your ownership change is hidden. This has bitten me.
Test it:
Step 4: Write the backup script
Replace user with your username everywhere. Make it executable:
Test it:
You should see a backup file appear.
Step 5: Schedule daily
Add:
Now backups run automatically at 3 AM every day.
Part 14: Lessons Learned
These are the gotchas that cost me time. Pay attention.
Lesson 1: Executable bit matters
The executable bit on a .mu file tells NomadNet whether to run it as a script or serve it as static text.
Static page:
chmod 644 file.mu(not executable)Dynamic script:
chmod 755 file.mu(executable)
If you make a static file executable, NomadNet tries to run it as a script, fails on the Micron markup, and the page hangs. If you make a dynamic script not executable, NomadNet serves the Python source code as text instead of running it.
How to tell which kind a file is: look at the first line. If it starts with #!/usr/bin/env python3, it's a script (needs +x). If it starts with Micron markup (backticks), it's static (no +x).
Lesson 2: scp sometimes changes permissions
When you scp a file from a Mac to the Pi, the file might arrive with unusual permissions like -rwx--x--x (711) instead of -rwxr-xr-x (755). The 711 mode removes read permissions for everyone except the owner. Your script still runs when you invoke it manually, but NomadNet (which reads the file differently) may fail.
After every scp, double-check:
If any file has weird permissions, fix them:
Lesson 3: Multiple services means multiple restarts
If you have three nodes running (nomadnet, nomadnet_news, nomadnet_warnings), each is a separate service with its own config and pages directory.
When you add or change pages on the news node, you have to restart nomadnet_news, not nomadnet. Restarting the wrong service does nothing useful.
Keep a note of which service owns which config:
Lesson 4: MeshChat caches aggressively
After deploying a new page or updating an existing one, MeshChat may still show the old version. Fixes, in order of increasing force:
Click the refresh icon (circular arrow) in MeshChat's node browser
Navigate home and back into the node
Restart the NomadNet service
Lesson 5: #!c=0 for dynamic pages
NomadNet caches page output by default. For static pages that's fine. For dynamic pages (which generate new content every time), it's a bug. Always include #!c=0 on the second line of dynamic scripts:
Lesson 6: Chown AFTER mount, not before
When setting up a USB drive mount point:
Lesson 7: Form field detection is fragile
Different NomadNet clients (MeshChat, the TUI, mobile apps) pass form fields with slightly different environment variable prefixes. The safe way to read form input is to loop through all environment variables looking for ones that end with your field name:
This works across all clients.
Lesson 8: Announce interval trade-offs
Your node announces itself to the network at intervals (default 6 hours, or 360 minutes). Faster announces make you easier to find, but consume more LoRa airtime if you're on a radio link. For testing, 30 minutes is fine. For production radio deployment, consider going back to 360 to conserve airtime.
Lesson 9: Service naming matters
Don't name services generic things like myservice.service. Use names that tell you what they're for:
Six weeks from now you won't remember which is which. Descriptive names save you from restarting the wrong thing.
Lesson 10: Back up early and often
The first thing to do after getting a node working is set up backups. Don't wait. SD cards fail. Power goes out mid-write. You'll accidentally rm -rf something. Backups are cheap insurance.
Lesson 11: Each node has its own cryptographic identity
When you create a new NomadNet instance with --config ~/.nomadnetwork_news, it generates a new identity file. That identity is tied to that config directory.
If you delete the config directory, you lose the identity. If you lose the identity, your node gets a NEW address on the network. Anyone who had favourited your old address can't find you anymore.
This is why backups matter: they preserve identity files, so even after a total system rebuild, your nodes come back with the same addresses.
Lesson 12: Check both tar and ls after every backup
A silent failure where tar runs but produces an empty archive is rare but possible. After running a backup, verify:
If the file exists, has reasonable size (few MB at minimum), and listing its contents shows expected files, you're good.
Part 15: Where to Go From Here
You now have a working mesh node with pages, a guestbook, RSS feeds, multiple independent instances, and backups. Some directions to explore:
Add a LoRa radio. A LilyGo LoRa32 or Heltec LoRa32 board flashed with RNode firmware plugs into your Pi via USB and adds long-range radio connectivity. Now your node can talk to other Reticulum nodes within several kilometres with no internet required.
Build more node types. Weather station data, local bus schedules, community announcements, a bulletin board. Anything you can express as a cache-and-display pattern works.
Contribute a fork. Projects like LXMF_messageboard, nomadForum, and RetiBBS are all open source. Pick one, add a feature that annoys you about it, submit a pull request.
Set up remote backups. USB backup protects against SD card failure. For true disaster recovery, add a second backup target offsite. A weekly scp to your laptop, or rclone to a cloud provider, survives the physical destruction of the Pi.
Connect it all to a Maker Faire or similar event. Your node is most interesting when strangers interact with it. Bring a screen, a keyboard, and let people leave messages in your guestbook.
Part 16: Handing This Tutorial to Claude
If you're using Claude (or another capable LLM) to help you through this, paste this tutorial as context and then ask questions. Claude can walk you through each step, help troubleshoot when things break, and adapt the tutorial to your specific situation.
Some useful ways to ask:
"I'm stuck at Part 6 Step 4 and my service won't start. Here's the output of
systemctl status...""Walk me through Part 11 step by step, pausing after each command so I can confirm it worked."
"I want to add a third node for local weather. Adapt Part 11 for that use case."
"My guestbook form isn't saving messages. Here's what I see when I run the script manually..."
Paste command output when things go wrong. Paste config files when asking about configuration. The more context the assistant has, the more useful its answers.
Appendix: Quick Reference
Commonly needed commands
Check services:
Restart a service:
View recent logs for a service:
Check Reticulum network status:
List cron jobs:
Edit cron jobs:
Check disk usage:
File locations summary
Location | Purpose |
|---|---|
| Reticulum identity and interface config |
| Main NomadNet node config and pages |
| News NomadNet node config and pages |
| Where your |
| MeshChat installation |
| Your systemd service files |
| USB backup drive mount point |
Micron syntax cheat sheet
Service file template
Replace NAME_HERE, USERNAME_HERE, and INSTANCE_HERE for each new node.
Dynamic page template
Save as .mu, chmod 755 to make executable, put in ~/.nomadnetwork_XXX/storage/pages/.

