<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Netbox on dKade's notes</title><link>https://dkade.com/tags/netbox/</link><description>Recent content in Netbox on dKade's notes</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>dkade@dkade.com (Daniel Loureiro)</managingEditor><webMaster>dkade@dkade.com (Daniel Loureiro)</webMaster><lastBuildDate>Wed, 12 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://dkade.com/tags/netbox/index.xml" rel="self" type="application/rss+xml"/><item><title>Running NetBox in a FreeBSD Jail with BastilleBSD</title><link>https://dkade.com/posts/netbox_freebsd_jail/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><author>dkade@dkade.com (Daniel Loureiro)</author><guid>https://dkade.com/posts/netbox_freebsd_jail/</guid><description>A complete guide to running NetBox natively inside a FreeBSD 15.1 VNET jail managed with BastilleBSD — no Docker and no Linux VM</description><content:encoded><![CDATA[<p>I wanted a simple source of truth for my home network: physical hosts, virtual machines, VLANs, prefixes, IP addresses, interfaces and switch connections. I was already keeping some of this information in NocoDB, but once the relationships between devices, interfaces and IPs become important, NetBox is a much better fit.</p>
<p>The official installation documentation targets Linux, but NetBox itself is a Python/Django application backed by PostgreSQL and Redis. With a few FreeBSD-specific adjustments it runs cleanly inside a jail.</p>
<h2 id="architecture">Architecture</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FreeBSD 15.1-RELEASE host
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">└── netbox VNET jail
</span></span><span class="line"><span class="cl">    │
</span></span><span class="line"><span class="cl">    ├── nginx          :80
</span></span><span class="line"><span class="cl">    │    └── Gunicorn  127.0.0.1:8001
</span></span><span class="line"><span class="cl">    │         └── NetBox
</span></span><span class="line"><span class="cl">    │
</span></span><span class="line"><span class="cl">    ├── PostgreSQL 17
</span></span><span class="line"><span class="cl">    ├── Redis
</span></span><span class="line"><span class="cl">    └── NetBox RQ worker
</span></span></code></pre></div><p>Everything runs natively in one jail using normal FreeBSD services.</p>
<h2 id="why-a-jail">Why a jail?</h2>
<ul>
<li>Native FreeBSD deployment without Docker</li>
<li>No Linux VM just for NetBox</li>
<li>PostgreSQL, Redis and nginx come from normal FreeBSD packages</li>
<li>ZFS snapshots make rollback easy</li>
<li>VNET gives the jail its own LAN address</li>
<li>The application can be managed with standard service commands</li>
</ul>
<h2 id="creating-the-jail">Creating the jail</h2>
<p>Create a VNET jail on the FreeBSD host. Adjust the address and interface for your network:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille create -V netbox 15.1-RELEASE 192.168.2.55/24 vtnet0
</span></span></code></pre></div><p>Before starting PostgreSQL, give the jail private System V IPC namespaces:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvmsg new
</span></span><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvsem new
</span></span><span class="line"><span class="cl">bastille config netbox <span class="nb">set</span> sysvshm new
</span></span><span class="line"><span class="cl">bastille restart netbox
</span></span></code></pre></div><p>Without these settings PostgreSQL initdb fails with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FATAL: could not create shared memory segment: Function not implemented
</span></span><span class="line"><span class="cl">DETAIL: Failed system call was shmget(...)
</span></span></code></pre></div><p>Enter the jail:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">bastille console netbox
</span></span></code></pre></div><h2 id="installing-netbox">Installing NetBox</h2>
<p>The bootstrap script installs the full stack:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">fetch https://codeberg.org/dkade/BSD/raw/branch/main/FreeBSD/netbox/install_netbox_freebsd.sh
</span></span><span class="line"><span class="cl">chmod +x install_netbox_freebsd.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">NETBOX_HOST</span><span class="o">=</span>netbox.home.lan <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">NETBOX_IP</span><span class="o">=</span>192.168.2.55 <span class="se">\
</span></span></span><span class="line"><span class="cl">./install_netbox_freebsd.sh
</span></span></code></pre></div><p>The current script is pinned to NetBox 4.6.1 and installs:</p>
<ul>
<li>Python 3.12</li>
<li>PostgreSQL 17</li>
<li>Redis</li>
<li>Gunicorn</li>
<li>nginx</li>
</ul>
<p>NetBox itself lives under:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/netbox/
</span></span><span class="line"><span class="cl">├── src/
</span></span><span class="line"><span class="cl">├── venv/
</span></span><span class="line"><span class="cl">└── gunicorn.py
</span></span></code></pre></div><p>The installer creates native FreeBSD services for:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">netbox
</span></span><span class="line"><span class="cl">netbox_rq
</span></span></code></pre></div><p>and enables the standard services:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">postgresql
</span></span><span class="line"><span class="cl">redis
</span></span><span class="line"><span class="cl">nginx
</span></span></code></pre></div><h2 id="freebsd-service-handling">FreeBSD service handling</h2>
<p>This was the main FreeBSD-specific part.</p>
<p>A first implementation used:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">daemon
</span></span><span class="line"><span class="cl">└── su
</span></span><span class="line"><span class="cl">    └── gunicorn
</span></span></code></pre></div><p>It started successfully, but stop and restart could hang because the FreeBSD service framework was tracking a supervisor process instead of the actual Gunicorn master.</p>
<p>The working version lets Gunicorn daemonize itself and maintain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/run/netbox/netbox.pid
</span></span></code></pre></div><p>The rc.d script then signals the real Gunicorn master directly.</p>
<p>The RQ worker uses the same principle and stores its real PID at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/var/run/netbox/netbox-rq.pid
</span></span></code></pre></div><p>This gives normal FreeBSD behaviour:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox start
</span></span><span class="line"><span class="cl">service netbox stop
</span></span><span class="line"><span class="cl">service netbox restart
</span></span><span class="line"><span class="cl">service netbox status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">service netbox_rq start
</span></span><span class="line"><span class="cl">service netbox_rq restart
</span></span><span class="line"><span class="cl">service netbox_rq status
</span></span></code></pre></div><h2 id="nginx">nginx</h2>
<p>nginx listens on port 80 and proxies to Gunicorn on localhost:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">client
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">nginx :80
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">Gunicorn 127.0.0.1:8001
</span></span><span class="line"><span class="cl">  |
</span></span><span class="line"><span class="cl">  v
</span></span><span class="line"><span class="cl">NetBox
</span></span></code></pre></div><p>The generated configuration lives at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/local/etc/nginx/conf.d/netbox.conf
</span></span></code></pre></div><h2 id="creating-the-administrator">Creating the administrator</h2>
<p>After installation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/local/netbox/src/netbox
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">su -m netbox -c <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s1">&#39;/usr/local/netbox/venv/bin/python manage.py createsuperuser&#39;</span>
</span></span></code></pre></div><p>Then open the hostname or jail IP in a browser.</p>
<h3 id="allowed_hosts-and-csrf">ALLOWED_HOSTS and CSRF</h3>
<p>Django validates both request hosts and POST origins.</p>
<p>The installer adds the configured hostname and optional jail IP to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;netbox.home.lan&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;192.168.2.55&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;localhost&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;127.0.0.1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">CSRF_TRUSTED_ORIGINS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;http://netbox.home.lan&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;http://192.168.2.55&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>If the origin is missing, the UI can load normally but login fails with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Forbidden (403)
</span></span><span class="line"><span class="cl">CSRF verification failed. Request aborted.
</span></span></code></pre></div><p>If HTTPS is added later, add the matching <code>https://</code> origin as well.</p>
<h2 id="checking-the-stack">Checking the stack</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox status
</span></span><span class="line"><span class="cl">service netbox_rq status
</span></span><span class="line"><span class="cl">service postgresql status
</span></span><span class="line"><span class="cl">service redis status
</span></span><span class="line"><span class="cl">service nginx status
</span></span></code></pre></div><p>Gunicorn should only listen on localhost:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sockstat -4 -l <span class="p">|</span> grep <span class="m">8001</span>
</span></span></code></pre></div><p>nginx should listen on port 80:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sockstat -4 -l <span class="p">|</span> grep <span class="s1">&#39;:80&#39;</span>
</span></span></code></pre></div><h2 id="upgrading">Upgrading</h2>
<p>Before upgrading I would take a ZFS snapshot and dump PostgreSQL:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pg_dump -U netbox -h 127.0.0.1 netbox &gt; /root/netbox.sql
</span></span></code></pre></div><p>Then stop the application:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">service netbox_rq stop
</span></span><span class="line"><span class="cl">service netbox stop
</span></span></code></pre></div><p>For now the installer intentionally stays pinned to a known working NetBox release rather than automatically following the latest tag. A separate upgrade script can later handle release download, Python dependency updates, migrations, static file collection and service restart.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>System V IPC</strong>: PostgreSQL needs <code>sysvmsg=new</code>, <code>sysvsem=new</code> and <code>sysvshm=new</code> in the Bastille jail.</li>
<li><strong>Service user shell</strong>: <code>/bin/sh</code> avoids the <code>su</code> and user-environment problems we hit with <code>/usr/sbin/nologin</code>.</li>
<li><strong>Gunicorn 26 runtime path</strong>: set <code>XDG_RUNTIME_DIR=/var/run/netbox</code> so its control socket is created in a directory owned by the NetBox user.</li>
<li><strong>Avoid <code>daemon -&gt; su -&gt; gunicorn</code></strong>: startup works, but signal propagation makes stop/restart unreliable.</li>
<li><strong>Track the real RQ worker PID</strong>: the same supervisor problem applies to the background worker.</li>
<li><strong>CSRF matters</strong>: any hostname or IP used in the browser must be represented in the Django host/origin configuration.</li>
<li><strong>Static docs directory</strong>: release archives may omit <code>project-static/docs</code>; creating it avoids Django&rsquo;s static-files warning.</li>
<li><strong>FreeBSD is not NetBox&rsquo;s officially documented installation target</strong>: future upgrades may need extra ports/packages when Python dependencies change.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>NetBox runs well natively inside a Bastille VNET jail once the service supervision and PostgreSQL IPC details are handled.</p>
<p>The final stack is simple:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FreeBSD jail
</span></span><span class="line"><span class="cl">├── nginx
</span></span><span class="line"><span class="cl">├── NetBox / Gunicorn
</span></span><span class="line"><span class="cl">├── NetBox RQ
</span></span><span class="line"><span class="cl">├── PostgreSQL
</span></span><span class="line"><span class="cl">└── Redis
</span></span></code></pre></div><p>No Docker, no Linux VM, and the whole application behaves like a normal FreeBSD service.</p>
<hr>
<p><em>Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.</em></p>
]]></content:encoded></item></channel></rss>