<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John</title>
    <description>The latest articles on DEV Community by John (@john_182319291).</description>
    <link>https://dev.to/john_182319291</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063265%2F912320f7-d3d8-4432-876b-a7e372adb01a.jpg</url>
      <title>DEV Community: John</title>
      <link>https://dev.to/john_182319291</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/john_182319291"/>
    <language>en</language>
    <item>
      <title>How Many People Fit in a Self-Hosted Hubs Room? Dialog SFU Bandwidth and CPU Costs Explained</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:11:34 +0000</pubDate>
      <link>https://dev.to/john_182319291/how-many-people-fit-in-a-self-hosted-hubs-room-dialog-sfu-bandwidth-and-cpu-costs-explained-4l4f</link>
      <guid>https://dev.to/john_182319291/how-many-people-fit-in-a-self-hosted-hubs-room-dialog-sfu-bandwidth-and-cpu-costs-explained-4l4f</guid>
      <description>&lt;p&gt;A single self-hosted Hubs server on a 4 vCPU, 8 GB VPS comfortably carries one room of roughly 15 to 25 participants with audio and avatars, and drops to single digits once most of them switch cameras on. The binding constraint is almost never Reticulum, the Elixir state server, which handles position and object sync at very low cost per user. It is Dialog, the mediasoup based SFU, which must decrypt, route and re encrypt every audio and video stream, and whose outbound bandwidth grows with the square of the participant count. Plan your capacity against Dialog's egress and CPU first, and treat everything else as secondary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two person startup replacing a paid meeting room, for example a design studio doing weekly client walkthroughs:&lt;/strong&gt; one small VPS is enough, because a 4 person audio first room needs a fraction of a single core and well under 10 Mbit/s of egress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community organiser running open public rooms, for example a monthly meetup with 30 drop in visitors:&lt;/strong&gt; budget for a dedicated Dialog host and cap video, because unrestricted cameras in a 30 person room push egress into hundreds of Mbit/s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Educator running parallel classrooms, for example six seminar rooms of 12 students each:&lt;/strong&gt; scale by adding Dialog instances rather than by growing one machine, since mediasoup workers are single threaded and bind to one core each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise team behind a corporate firewall, for example a distributed engineering group on locked down laptops:&lt;/strong&gt; provision TURN capacity explicitly, because relayed peers double the traffic path and can quietly triple your bandwidth bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hobbyist on a home server, for example a NAS in a cupboard on domestic fibre:&lt;/strong&gt; check upload speed before anything else, because a typical asymmetric connection saturates its uplink at around 8 to 12 video participants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event host planning a one off launch, for example 100 attendees in a showcase space:&lt;/strong&gt; shard into multiple rooms or use audio only broadcast patterns, since a single Hubs room is not designed as a webinar stage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff is simple: every extra participant you allow with video multiplies Dialog's forwarding work and your egress bill, while every restriction you impose on cameras, resolution or room size buys back capacity you can spend on more concurrent rooms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is the realistic concurrent user limit for one self-hosted Hubs server?&lt;/li&gt;
&lt;li&gt;Which Hubs component actually breaks first under load, Reticulum or Dialog?&lt;/li&gt;
&lt;li&gt;How does Dialog's SFU model differ from mesh and MCU, and why does it matter here?&lt;/li&gt;
&lt;li&gt;How much bandwidth does each Hubs participant consume, upstream and downstream?&lt;/li&gt;
&lt;li&gt;Why does room bandwidth grow quadratically, and where does the curve become unaffordable?&lt;/li&gt;
&lt;li&gt;How much CPU does a mediasoup worker use per audio stream and per video stream?&lt;/li&gt;
&lt;li&gt;What server specification do you need for 10, 25 and 50 concurrent participants?&lt;/li&gt;
&lt;li&gt;How much does Reticulum cost per user for position, rotation and object sync?&lt;/li&gt;
&lt;li&gt;When does TURN relay kick in, and what does it add to your bandwidth bill?&lt;/li&gt;
&lt;li&gt;How do simulcast, bitrate caps and audio only rooms change the maths?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is the realistic concurrent user limit for one self-hosted Hubs server?
&lt;/h2&gt;

&lt;p&gt;Start from the room, not the server. A Hubs room is a single Reticulum channel plus a single Dialog routing context, so the numbers that matter are per room before they are per machine. On a 4 vCPU, 8 GB Linux host running the full Community Edition stack, plan around these ceilings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Room style&lt;/th&gt;
&lt;th&gt;Realistic ceiling on 4 vCPU, 8 GB&lt;/th&gt;
&lt;th&gt;What stops you first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio only, avatars moving&lt;/td&gt;
&lt;td&gt;Roughly 25 to 30 participants&lt;/td&gt;
&lt;td&gt;Dialog egress, then mediasoup worker CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio plus a few shared cameras&lt;/td&gt;
&lt;td&gt;Roughly 15 to 20 participants&lt;/td&gt;
&lt;td&gt;Outbound bandwidth on the host uplink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everyone on camera&lt;/td&gt;
&lt;td&gt;Single digits, often 6 to 8&lt;/td&gt;
&lt;td&gt;Quadratic forwarding load in Dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio plus screen share at 1080p&lt;/td&gt;
&lt;td&gt;Roughly 12 to 15 participants&lt;/td&gt;
&lt;td&gt;Sustained high bitrate of the shared track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several parallel small rooms&lt;/td&gt;
&lt;td&gt;Limited by core count, not room count&lt;/td&gt;
&lt;td&gt;One mediasoup worker binds to one core&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat those as planning defaults, not guarantees. Your scene complexity, bitrate caps and network path move them substantially, and the later sections quantify each factor.&lt;/p&gt;

&lt;p&gt;Where you run the stack sets the hard limit on the uplink figure. A VPS gives you symmetric bandwidth and a public IP. A home server or NAS gives you cheap storage but usually an asymmetric uplink. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user. Whichever you pick, measure the uplink with &lt;code&gt;iperf3&lt;/code&gt; before you size a room, because Dialog cannot forward what the link will not carry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which Hubs component actually breaks first under load, Reticulum or Dialog?
&lt;/h2&gt;

&lt;p&gt;Dialog breaks first, almost every time. The two services fail in completely different ways, and knowing which symptom belongs to which process saves hours of misdirected tuning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reticulum scales the way Elixir services do:&lt;/strong&gt; it is a Phoenix application on the BEAM virtual machine, and each participant is a lightweight process handling position, rotation and object state over a WebSocket. Thousands of these coexist on one core. You will see memory grow gently and CPU stay flat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialog scales the way media servers do:&lt;/strong&gt; it is built on mediasoup, and each worker process is single threaded and pinned to one core. When that core hits 100 percent, every stream it routes degrades at once, regardless of how idle the other cores look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure signatures are distinct:&lt;/strong&gt; a saturated Dialog produces choppy audio, frozen video tiles and rising round trip time while avatars keep moving smoothly. A struggling Reticulum produces the opposite, teleporting avatars and stale object positions while voices stay clear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL rarely participates:&lt;/strong&gt; Reticulum writes room metadata, accounts and scene records, not per frame state, so database load stays roughly constant as a room fills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The host uplink often beats both:&lt;/strong&gt; on a 1 Gbit/s link, sustained video forwarding can exhaust bandwidth while both processes still report headroom, which reads as network failure rather than server failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Diagnose with &lt;code&gt;top -H -p $(pgrep -f mediasoup)&lt;/code&gt; to see per thread CPU on Dialog workers, then compare against the BEAM scheduler load. If one thread sits at 100 percent and the rest idle, you have found your ceiling.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does Dialog's SFU model differ from mesh and MCU, and why does it matter here?
&lt;/h2&gt;

&lt;p&gt;Dialog is a selective forwarding unit built on mediasoup. It receives one upstream from each participant and forwards copies to everyone else without decoding or re encoding the video. That single design choice explains most of the capacity numbers in this article.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;What the server does&lt;/th&gt;
&lt;th&gt;Cost at 10 participants with video&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mesh, peer to peer&lt;/td&gt;
&lt;td&gt;Nothing, clients connect directly to each other&lt;/td&gt;
&lt;td&gt;Each client uploads 9 copies and downloads 9, clients die first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFU without simulcast, plain forwarding&lt;/td&gt;
&lt;td&gt;Copies each incoming stream to every other peer&lt;/td&gt;
&lt;td&gt;Each client uploads 1 and downloads 9, server egress is 90 stream copies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFU with simulcast, Dialog's normal mode&lt;/td&gt;
&lt;td&gt;Picks a suitable quality layer per receiver&lt;/td&gt;
&lt;td&gt;Same stream count, lower total bitrate, no transcoding CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCU, server side mixing&lt;/td&gt;
&lt;td&gt;Decodes all streams, composites one, re encodes&lt;/td&gt;
&lt;td&gt;Each client uploads 1 and downloads 1, server CPU cost is severe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio only SFU&lt;/td&gt;
&lt;td&gt;Forwards Opus tracks, applies no video work&lt;/td&gt;
&lt;td&gt;Trivial CPU, bandwidth measured in tens of kbit/s per stream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical consequence is that Dialog trades CPU for bandwidth. It never transcodes, so a mediasoup worker stays cheap in processor terms compared to an MCU like Jitsi Videobridge running in mixing mode. What it cannot escape is the copy count. Ten senders in one room mean 90 outbound stream copies leaving your network interface, and 20 senders mean 380.&lt;/p&gt;

&lt;p&gt;That is why you size a Hubs deployment by egress capacity and worker count rather than by raw CPU. It also means adding a faster processor rarely raises your ceiling.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much bandwidth does each Hubs participant consume, upstream and downstream?
&lt;/h2&gt;

&lt;p&gt;Bandwidth in a Hubs room comes from four separate streams, and only one of them is expensive. Measure each one before you multiply.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Voice, Opus over SRTP:&lt;/strong&gt; a single microphone track runs in the 24 to 40 kbit/s range at typical WebRTC settings, plus roughly 10 percent packet overhead. Thirty people talking is still under 1.5 Mbit/s of unique source material.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webcam video, VP8 or H.264 simulcast:&lt;/strong&gt; Dialog normally publishes several spatial layers, commonly around 150 kbit/s at 320x180, 500 kbit/s at 640x360 and 1.2 to 1.5 Mbit/s at 1280x720. Each receiver pulls the layer their connection and tile size justify, so downstream per stream varies widely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen share at 1080p:&lt;/strong&gt; treat this as the heaviest single track in the room, typically 2 to 3 Mbit/s sustained for detailed content, and higher during rapid motion. One presenter sharing to 20 viewers costs more egress than 20 voice tracks combined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avatar and object state over Reticulum:&lt;/strong&gt; position, rotation and interaction messages travel on a WebSocket separate from the media path, measured in single digit kbit/s per user. This is noise against the media figures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scene and asset downloads:&lt;/strong&gt; a GLB scene, textures and audio files transfer once per join, so a 40 MB scene costs 40 MB per visitor at join time, not continuously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upstream per participant is simple: one voice track, optionally one camera, optionally one screen share. Downstream is where the count multiplies. Open &lt;code&gt;chrome://webrtc-internals&lt;/code&gt; during a live room and read &lt;code&gt;bytesSent&lt;/code&gt; and &lt;code&gt;bytesReceived&lt;/code&gt; per transport to confirm your own numbers rather than trusting defaults.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does room bandwidth grow quadratically, and where does the curve become unaffordable?
&lt;/h2&gt;

&lt;p&gt;The arithmetic is unforgiving. With n people sending video, each of the other n minus 1 receives a copy, so Dialog emits n times n minus 1 stream copies. Doubling a room quadruples its egress.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Five senders at 500 kbit/s each:&lt;/strong&gt; 20 outbound copies, roughly 10 Mbit/s of egress. Any connection handles this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ten senders at the same bitrate:&lt;/strong&gt; 90 copies, roughly 45 Mbit/s. Still comfortable on a symmetric 100 Mbit/s link, already impossible on most domestic uplinks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twenty senders:&lt;/strong&gt; 380 copies, roughly 190 Mbit/s sustained. This is where a shared VPS network allowance starts to matter more than CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thirty senders:&lt;/strong&gt; 870 copies, roughly 435 Mbit/s. You are now consuming close to half a gigabit for one room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fifty senders:&lt;/strong&gt; 2,450 copies, over 1.2 Gbit/s. A single 1 Gbit/s port cannot carry it, whatever the server specification says.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convert that to metered cost before committing. Sustained 100 Mbit/s of egress transfers about 45 GB per hour, so a weekly two hour session at twenty senders moves several hundred gigabytes per month. Providers that meter egress will notice.&lt;/p&gt;

&lt;p&gt;The practical break point sits between 15 and 20 simultaneous video senders on a 1 Gbit/s host. Below it, you tune. Above it, you change the room design.&lt;/p&gt;

&lt;p&gt;Where you host decides which side of that line you sit on. A self managed VPS with a symmetric gigabit port, a home server or NAS on asymmetric fibre, and Yundera, a managed Personal Cloud Server built on CasaOS that runs self-hosted apps as Docker containers on a server dedicated to the user, each present a different uplink profile you should confirm before sizing rooms.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much CPU does a mediasoup worker use per audio stream and per video stream?
&lt;/h2&gt;

&lt;p&gt;Dialog never decodes media, so the CPU cost is packet handling, not pixel work. What a worker actually spends its cycles on is SRTP decryption of the incoming packet, per consumer bookkeeping, then SRTP encryption of each outgoing copy. Cost tracks packets per second, not bitrate alone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio is cheap because packet rates are low:&lt;/strong&gt; Opus at the standard 20 ms frame size produces 50 packets per second per stream. Forwarding one voice track to 29 listeners means 1,450 encryptions per second, which a single core absorbs without noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video is expensive because packets are many:&lt;/strong&gt; a 1.2 Mbit/s stream at a 1,200 byte payload runs at roughly 125 packets per second. The same 29 receivers turn that into 3,625 encryptions per second for one sender, and each additional sender adds another block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulcast multiplies incoming work, not outgoing:&lt;/strong&gt; the publisher sends several spatial layers, so the worker decrypts all of them but still forwards only the layer each consumer selected. Ingest cost rises modestly, egress cost does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retransmissions add unpredictable load:&lt;/strong&gt; NACK and PLI handling from lossy clients forces the worker to resend packets and request keyframes, and a handful of bad connections can cost more than a dozen clean ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker count is your only horizontal lever on one box:&lt;/strong&gt; mediasoup starts one worker process per detected CPU core by default, each single threaded, and a room is pinned to a single worker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch it live with &lt;code&gt;pidstat -u -p $(pgrep -d, -f mediasoup) 1&lt;/code&gt;. When one worker crosses roughly 80 percent of a core, stop adding video senders to rooms it owns.&lt;/p&gt;




&lt;h2&gt;
  
  
  What server specification do you need for 10, 25 and 50 concurrent participants?
&lt;/h2&gt;

&lt;p&gt;Size three things separately: cores for Dialog workers, RAM for Reticulum and PostgreSQL, and uplink for egress. The uplink is usually what you have to buy deliberately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target load&lt;/th&gt;
&lt;th&gt;Specification to plan for&lt;/th&gt;
&lt;th&gt;The binding constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 to 6 people, one test room, audio first&lt;/td&gt;
&lt;td&gt;2 vCPU, 4 GB RAM, 100 Mbit/s symmetric, 40 GB disk&lt;/td&gt;
&lt;td&gt;Nothing, this runs the whole Community Edition stack on one host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 concurrent, mixed audio and occasional video&lt;/td&gt;
&lt;td&gt;4 vCPU, 8 GB RAM, 1 Gbit/s port&lt;/td&gt;
&lt;td&gt;RAM, once PostgreSQL, Reticulum and the asset hosts share a box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25 concurrent, audio led with a few cameras&lt;/td&gt;
&lt;td&gt;8 vCPU, 16 GB RAM, 1 Gbit/s port, metered egress checked&lt;/td&gt;
&lt;td&gt;Sustained egress, not processor headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 concurrent, video restricted&lt;/td&gt;
&lt;td&gt;Dialog on its own 8 vCPU host, Reticulum and PostgreSQL on a second 4 vCPU host&lt;/td&gt;
&lt;td&gt;Worker to core pinning, plus port capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 plus, or several parallel rooms&lt;/td&gt;
&lt;td&gt;Multiple Dialog instances, one per host, rooms assigned across them&lt;/td&gt;
&lt;td&gt;Horizontal scaling, since a room cannot span workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Allow 2 GB of RAM for PostgreSQL before tuning &lt;code&gt;shared_buffers&lt;/code&gt;, and keep 20 GB of disk free for scene assets and uploads, which grow quietly.&lt;/p&gt;

&lt;p&gt;Hosting choices split along the same lines: a self managed VPS gives you the symmetric port and root access to tune kernel networking, a home server or NAS gives you storage but rarely the uplink, and Yundera is a managed Personal Cloud Server built on CasaOS where apps are installed from an app store in one click rather than assembled from compose files by hand. Confirm the egress allowance in writing before you commit to a 25 person room.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much does Reticulum cost per user for position, rotation and object sync?
&lt;/h2&gt;

&lt;p&gt;Reticulum's cost per user is small enough that it rarely appears in capacity planning, but it is not zero, and it also fans out quadratically. The difference is payload size.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per user memory on the BEAM is measured in kilobytes:&lt;/strong&gt; each participant is an Erlang process holding channel state, so a 30 person room adds megabytes, not gigabytes. Reticulum's baseline footprint dominates, and that baseline is roughly constant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update payloads are tiny:&lt;/strong&gt; an avatar transform is a handful of floats for position, rotation and a few tracked bones. Even at 10 to 20 updates per second, one sender produces low tens of kilobits per second of unique data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fan out still squares:&lt;/strong&gt; with 30 people in a room, every update is copied 29 times. Assume 15 updates per second at 50 bytes and the room generates roughly 650 kB/s of broadcast traffic in total, which is under 6 Mbit/s. That is one twentieth of what the same room costs in video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object and media pins hit PostgreSQL, movement does not:&lt;/strong&gt; spawning, moving and pinning an object writes a row, so a room where people constantly pin content produces steady database writes while ordinary walking around produces none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join and leave events are the spiky part:&lt;/strong&gt; each join triggers presence updates to every client plus scene metadata queries, so 30 people arriving in the same 60 seconds costs more than the following hour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify on your own instance by attaching to the running node and reading scheduler utilisation with &lt;code&gt;:observer.start()&lt;/code&gt; or &lt;code&gt;recon:scheduler_usage/1&lt;/code&gt;. If Reticulum is your bottleneck, you have an unusual room, not a typical one.&lt;/p&gt;




&lt;h2&gt;
  
  
  When does TURN relay kick in, and what does it add to your bandwidth bill?
&lt;/h2&gt;

&lt;p&gt;Most Hubs clients connect straight to Dialog over UDP, because an SFU is already a public endpoint and needs no peer to peer hole punching. Relay only appears when the client side network refuses that path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symmetric NAT and strict firewalls force it:&lt;/strong&gt; corporate networks that permit outbound 443 only will block Dialog's UDP media range outright, and the ICE negotiation falls back rather than failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICE over TCP is the cheaper first fallback:&lt;/strong&gt; mediasoup can accept TCP candidates on the same host, which keeps media on your Dialog server and adds latency and head of line blocking instead of a second machine and a second bandwidth charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;coturn is the real TURN option:&lt;/strong&gt; it listens on 3478 for UDP and TCP and 5349 for TLS, with a media port range set by &lt;code&gt;min-port&lt;/code&gt; and &lt;code&gt;max-port&lt;/code&gt; in &lt;code&gt;/etc/turnserver.conf&lt;/code&gt;. Use &lt;code&gt;static-auth-secret&lt;/code&gt; with time limited credentials rather than long lived users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relayed traffic is counted twice:&lt;/strong&gt; every byte enters the TURN server and leaves it again, so a relayed participant pulling 2 Mbit/s costs 4 Mbit/s of measured transfer on that host, on top of the identical traffic Dialog already sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small relay share is a large bill:&lt;/strong&gt; in a 20 person video room, moving just 4 participants onto TURN adds their entire downstream a second time, which is a double digit percentage increase on total egress for a fifth of the audience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test before you need it with &lt;code&gt;turnutils_uclient -T -u user -w secret your.turn.host&lt;/code&gt;. If you skip TURN entirely, accept that some corporate and mobile users will join with avatars visible and no audio at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do simulcast, bitrate caps and audio only rooms change the maths?
&lt;/h2&gt;

&lt;p&gt;These three levers move your ceiling further than any hardware upgrade. Each one attacks the copy count or the bitrate per copy. Take the same 20 person room and compare.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;What actually changes&lt;/th&gt;
&lt;th&gt;Room egress at 20 participants&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everyone on camera at 640x360, 500 kbit/s&lt;/td&gt;
&lt;td&gt;Nothing restricted, 380 outbound copies&lt;/td&gt;
&lt;td&gt;Roughly 190 Mbit/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitrate capped to 320x180, 150 kbit/s&lt;/td&gt;
&lt;td&gt;Same 380 copies, each one smaller&lt;/td&gt;
&lt;td&gt;Roughly 57 Mbit/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simulcast selecting low layers for distant avatars&lt;/td&gt;
&lt;td&gt;Receivers pull the layer their tile size justifies&lt;/td&gt;
&lt;td&gt;Between the two figures above, typically nearer the lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera rights limited to 4 speakers&lt;/td&gt;
&lt;td&gt;Copies fall to 4 senders times 19 receivers, 76 total&lt;/td&gt;
&lt;td&gt;Roughly 38 Mbit/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio only, avatars and screen share off&lt;/td&gt;
&lt;td&gt;380 Opus copies at about 32 kbit/s&lt;/td&gt;
&lt;td&gt;Roughly 12 Mbit/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three practical notes. Simulcast is the default worth keeping, because it costs the publisher extra upstream but saves every receiver downstream, and it degrades gracefully when one participant's connection collapses. Bitrate caps belong in Dialog's mediasoup settings rather than in client code, since anything enforced client side can be bypassed by a browser flag.&lt;/p&gt;

&lt;p&gt;Restricting camera rights is the blunt instrument that works. Hubs room settings let you decide who may share video, and moving from open cameras to four presenters cuts egress by 80 percent in the table above without touching resolution at all.&lt;/p&gt;

&lt;p&gt;Audio only is not a fallback, it is a valid room design. A 30 person social space with spatial audio, avatars and a good scene stays under 20 Mbit/s and runs on hardware that would collapse instantly with cameras enabled.&lt;/p&gt;

</description>
      <category>hubs</category>
      <category>webrtc</category>
      <category>selfhosted</category>
      <category>performance</category>
    </item>
    <item>
      <title>Duplicati on Backblaze B2 vs Backblaze Personal Backup: where the yearly cost actually crosses over</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:05:07 +0000</pubDate>
      <link>https://dev.to/john_182319291/duplicati-on-backblaze-b2-vs-backblaze-personal-backup-where-the-yearly-cost-actually-crosses-over-2hp8</link>
      <guid>https://dev.to/john_182319291/duplicati-on-backblaze-b2-vs-backblaze-personal-backup-where-the-yearly-cost-actually-crosses-over-2hp8</guid>
      <description>&lt;p&gt;Duplicati backing up to Backblaze B2 costs less per year than a Backblaze Personal Backup subscription only below a dataset size you can calculate in one line, and that line is the whole decision. Personal Backup charges a flat annual fee per computer for unlimited data, so its cost curve is horizontal. B2 charges per gigabyte stored per month plus API calls and egress above the free allowance, so the Duplicati bill starts near zero and rises in a straight line. Divide the annual subscription fee by twelve times the B2 per gigabyte monthly rate, and you get the crossover in gigabytes: below it Duplicati wins every year, above it the subscription wins and keeps winning harder as you add data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 400 GB laptop owner, one MacBook holding documents, photos and a code folder:&lt;/strong&gt; run Duplicati to B2, because a few hundred gigabytes billed per gigabyte per month sits far under any flat annual fee, and Duplicati compresses and deduplicates before you are billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 6 TB media keeper with a film and music library on external drives:&lt;/strong&gt; stay on Personal Backup, because unlimited flat pricing per computer is unbeatable at that volume and B2 charges for every terabyte forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two person household with three laptops totalling 1.5 TB:&lt;/strong&gt; run Duplicati into one B2 bucket, because the subscription is licensed per computer while a single bucket and three Duplicati jobs cover all of them under one bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The homelab operator with a NAS, a Proxmox host and two Linux VMs:&lt;/strong&gt; Duplicati is the only option of the two, because Personal Backup is a Windows and macOS desktop product that excludes network shares and server operating systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The person who has never restored a file and wants zero maintenance:&lt;/strong&gt; stay subscribed, because Duplicati's real price includes your own hours on local database rebuilds, retention tuning, verification runs and failure monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The person restoring large amounts often, for example a video editor pulling back 500 GB projects:&lt;/strong&gt; model the egress before you switch, because Personal Backup restores are included while B2 bills downloads once you pass the free egress allowance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff is a flat fee that ignores how much data you own against a metered bill that rewards you for owning less of it, with the difference paid in your own attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What does Duplicati plus Backblaze B2 actually cost you per year?&lt;/li&gt;
&lt;li&gt;What does a Backblaze Personal Backup subscription actually cost per year?&lt;/li&gt;
&lt;li&gt;At what dataset size do the two cost curves cross?&lt;/li&gt;
&lt;li&gt;Deduplication, compression and the number B2 really bills you for&lt;/li&gt;
&lt;li&gt;How much do restores cost, and how often will you actually restore?&lt;/li&gt;
&lt;li&gt;How does retention depth change the arithmetic?&lt;/li&gt;
&lt;li&gt;Tuning Duplicati remote volume size, upload threads and API call classes&lt;/li&gt;
&lt;li&gt;Multiple computers, NAS shares and the per computer licence&lt;/li&gt;
&lt;li&gt;What each product simply refuses to back up&lt;/li&gt;
&lt;li&gt;Where should you run Duplicati, and what does the host add to the bill?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What does Duplicati plus Backblaze B2 actually cost you per year?
&lt;/h2&gt;

&lt;p&gt;Duplicati itself is free and open source, so every euro or dollar on your annual bill comes from B2 and from your own hardware. The bill has four moving parts, and only one of them is the obvious one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage per gigabyte per month:&lt;/strong&gt; B2 bills the average bytes stored across the month, so your annual cost is roughly your stored size multiplied by the published per gigabyte rate multiplied by twelve. This is the dominant term for almost everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored size, not source size:&lt;/strong&gt; Duplicati 2 deduplicates at a default block size of 100 KB and compresses before upload, so a 500 GB source folder rarely becomes 500 GB in the bucket. Documents and code shrink hard, video and photos barely move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction calls:&lt;/strong&gt; B2 splits API calls into Class A, Class B and Class C, with Class A calls such as uploads free and the listing and download classes billed per thousand calls. Duplicati's default remote volume size of 50 MB means a 500 GB backup lands as roughly 10,000 objects, which keeps call counts modest but not zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress on restore:&lt;/strong&gt; B2 includes a free download allowance tied to your average stored data, and bills per gigabyte beyond it. Routine backups download nothing, so this term is zero in most years and spikes only when you restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version retention:&lt;/strong&gt; every kept version of a changed file is extra stored bytes, billed monthly like everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add those four, and you have a number that scales with what you actually own.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does a Backblaze Personal Backup subscription actually cost per year?
&lt;/h2&gt;

&lt;p&gt;Personal Backup is a flat fee per computer, billed monthly, yearly or on a two year term, with the longer terms carrying a lower effective annual rate. Unlimited means unlimited: 200 GB and 20 TB on the same machine cost the same. The complications are not in the headline price.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The licence covers one computer, not one household:&lt;/strong&gt; three laptops means three subscriptions, and the annual cost triples while the amount of data protected is irrelevant to the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version history defaults to 30 days:&lt;/strong&gt; deleted or changed files fall out of the backup after 30 days unless you pay for an extended version history add on, sold as a one year option and a forever option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forever version history is not flat:&lt;/strong&gt; the forever tier adds a per gigabyte per month charge for data older than one year, which reintroduces exactly the metered pricing model you left B2 to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External drives count, network drives do not:&lt;/strong&gt; USB attached disks are included at no extra cost, but a NAS share, a mapped network drive or a Linux server cannot be backed up at all, so protecting them means a second product on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attached drives must be reconnected every 30 days:&lt;/strong&gt; disconnect an external disk for longer and its backed up data is purged from the account, which is a silent cost if you rotate archive drives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restores are included:&lt;/strong&gt; web download and restore by mail both come with the plan, with the mailed drive refunded on return.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The predictable number on the invoice is the product's main feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  At what dataset size do the two cost curves cross?
&lt;/h2&gt;

&lt;p&gt;Work it out yourself in one line. Take the annual Personal Backup fee, divide it by twelve, then divide that by the current B2 per gigabyte monthly rate. The result is the crossover in gigabytes stored, and both numbers are published on Backblaze's own pricing pages, so the calculation stays correct as rates change. Call that crossover point C.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bytes stored in B2&lt;/th&gt;
&lt;th&gt;Duplicati annual cost versus one subscription&lt;/th&gt;
&lt;th&gt;What it means for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.25 x C&lt;/td&gt;
&lt;td&gt;About 25 percent&lt;/td&gt;
&lt;td&gt;Duplicati is clearly cheaper, and the gap is large enough to absorb restores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5 x C&lt;/td&gt;
&lt;td&gt;About 50 percent&lt;/td&gt;
&lt;td&gt;Duplicati is cheaper, with room for retention growth before parity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 x C&lt;/td&gt;
&lt;td&gt;Parity&lt;/td&gt;
&lt;td&gt;Choose on features, not on price, because the invoices match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 x C&lt;/td&gt;
&lt;td&gt;About 200 percent&lt;/td&gt;
&lt;td&gt;The subscription is cheaper and the gap widens every month you add data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 x C&lt;/td&gt;
&lt;td&gt;About 400 percent&lt;/td&gt;
&lt;td&gt;Only non cost reasons justify Duplicati at this volume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two adjustments move C in your favour. First, C scales linearly with the number of machines: three computers on Personal Backup means three fees, so your effective crossover is 3 x C for a single B2 bucket holding all three. Second, C applies to stored bytes, not source bytes, so Duplicati compression can put a 700 GB source folder comfortably under a 500 GB crossover.&lt;/p&gt;

&lt;p&gt;One adjustment moves it against you: unlimited retention on B2 grows stored bytes forever, while the flat fee never notices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deduplication, compression and the number B2 really bills you for
&lt;/h2&gt;

&lt;p&gt;Your source folder size is not your invoice. Duplicati hashes every block with SHA-256, stores each unique block once, packs blocks into archive volumes and compresses them before upload. Four things decide how far that gets you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File type dominates the ratio:&lt;/strong&gt; text, code, spreadsheets, mailboxes and virtual disk images compress and deduplicate heavily. JPEG, MP4, MP3 and existing ZIP archives are already compressed, so expect close to no reduction. Duplicati ships a &lt;code&gt;default_compressed_extensions.txt&lt;/code&gt; list and skips compressing those types rather than burning CPU for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication is per backup job, not per bucket:&lt;/strong&gt; two jobs covering the same 40 GB photo folder store those blocks twice and you pay twice. Consolidating overlapping folders into one job is free money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleted data stays billable until compaction runs:&lt;/strong&gt; when retention removes an old version, its blocks remain inside archive volumes that still contain live blocks. Duplicati only rewrites a volume once wasted space passes the compact threshold, 25 percent by default, so stored bytes lag behind logical deletions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption adds a small constant, not a percentage:&lt;/strong&gt; AES-256 output is roughly the size of its compressed input plus per volume header overhead, so encryption costs you almost nothing on the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dashboard tells you the truth:&lt;/strong&gt; the job's reported backend size, not the source size, is the number to feed into the crossover calculation from the previous section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure once with a real backup before committing. A 1 TB documents heavy dataset and a 1 TB video library produce completely different B2 bills.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much do restores cost, and how often will you actually restore?
&lt;/h2&gt;

&lt;p&gt;Most people restore a handful of files a year and a full dataset once a decade, if ever. That matters because B2 includes a free download allowance of three times your average monthly stored data, so a household sized restore usually lands inside it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single full restore is normally free:&lt;/strong&gt; pulling back everything you store is 1x your stored bytes against a 3x allowance, so you would need to restore your entire backup three times in one month before egress charges start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small restores cost more than you expect in bytes:&lt;/strong&gt; Duplicati downloads whole archive volumes, so recovering one 2 MB document can pull a 50 MB volume. The cash impact is negligible, the wait is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restoring without the local database is the slow path:&lt;/strong&gt; direct restore first downloads the &lt;code&gt;dlist&lt;/code&gt; and &lt;code&gt;dindex&lt;/code&gt; files to rebuild an index of what is where, which on a large backup with many versions can take hours before a single file lands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class B download transactions are billed per call:&lt;/strong&gt; thousands of volume fetches during a big restore add a small line item alongside egress, another reason larger remote volume sizes suit archival datasets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal Backup charges nothing for either route:&lt;/strong&gt; web download restores are included, and the restore by mail USB drive is refunded when you return it, so the subscription genuinely removes restore cost from the equation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Budget the shape, not a scary number: routine restores are effectively free on both, and only repeated bulk pulls of your whole dataset make B2 egress a real line on the invoice.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does retention depth change the arithmetic?
&lt;/h2&gt;

&lt;p&gt;Comparing a 30 day subscription against an unlimited history Duplicati job is not a fair comparison. Match the retention first, then compare the invoices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retention setting&lt;/th&gt;
&lt;th&gt;Effect on your bill&lt;/th&gt;
&lt;th&gt;Where it applies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keep all versions, the Duplicati default&lt;/td&gt;
&lt;td&gt;Stored bytes grow with every change forever, so the B2 line rises even if your source folder never grows&lt;/td&gt;
&lt;td&gt;Duplicati&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--keep-time=30D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Roughly matches the standard subscription window, keeping stored bytes close to your live dataset size&lt;/td&gt;
&lt;td&gt;Duplicati&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--retention-policy=1W:1D,4W:1W,12M:1M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Thins older versions to one per week then one per month, buying a year of history for a fraction of the bytes&lt;/td&gt;
&lt;td&gt;Duplicati&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 day version history, the plan default&lt;/td&gt;
&lt;td&gt;No extra charge, and anything deleted more than 30 days ago is gone&lt;/td&gt;
&lt;td&gt;Personal Backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One year extended version history&lt;/td&gt;
&lt;td&gt;A fixed add on fee per computer per year, still flat regardless of data volume&lt;/td&gt;
&lt;td&gt;Personal Backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forever version history&lt;/td&gt;
&lt;td&gt;Flat fee plus a per gigabyte monthly charge on data older than one year, so the metered model returns&lt;/td&gt;
&lt;td&gt;Personal Backup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical consequence is that deep retention narrows the gap. If you want a year of history, the subscription add on is a fixed number per computer, while Duplicati's smart retention adds real bytes to a metered bill. If you want a decade of history, Duplicati's thinned policy competes well against a forever tier that meters everything over twelve months old.&lt;/p&gt;

&lt;p&gt;Set retention before you measure stored size, or your crossover calculation is based on a number that will keep climbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tuning Duplicati remote volume size, upload threads and API call classes
&lt;/h2&gt;

&lt;p&gt;Defaults are tuned for a 50 GB laptop, not a 2 TB archive. Four settings change your bill, your backup window and your restore speed, and they pull against each other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dblock-size&lt;/code&gt;, default 50 MB:&lt;/strong&gt; raising it to 200 MB or 500 MB cuts object count and the listing calls that go with it by the same factor. The cost is restore granularity, since recovering one small file downloads one whole volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--blocksize&lt;/code&gt;, default 100 KB:&lt;/strong&gt; this one is fixed at job creation and cannot be changed later without starting over. A 2 TB dataset at 100 KB implies roughly 20 million block records in the local SQLite database, which is what makes repair and rebuild operations crawl. Raising it to 1 MB or larger for multi terabyte jobs trades a little deduplication for a database that stays usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--asynchronous-concurrent-upload-limit&lt;/code&gt;, default 4:&lt;/strong&gt; more parallel uploads fill a fast link, but each in flight volume needs temporary disk space, and B2 counts each upload as a Class A call, which is not billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--number-of-retries&lt;/code&gt;, default 5, with &lt;code&gt;--retry-delay&lt;/code&gt;:&lt;/strong&gt; raising both rides out transient B2 errors instead of failing a job at 90 percent and re uploading later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The class split is what makes B2 forgiving here: uploads and deletes are Class A and free, downloads are Class B, and listing is Class C with a published daily free allowance. Duplicati lists the bucket on every run, so shrinking object count with a larger volume size keeps you comfortably inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiple computers, NAS shares and the per computer licence
&lt;/h2&gt;

&lt;p&gt;This is where the flat fee stops being flat. Every extra machine multiplies a subscription while adding only its own gigabytes to a B2 bucket, so a four device household is a completely different calculation from a single laptop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One bucket serves every machine:&lt;/strong&gt; give each host its own Duplicati job and its own &lt;code&gt;--prefix&lt;/code&gt; value, for example &lt;code&gt;laptop-anna/&lt;/code&gt; and &lt;code&gt;nas-media/&lt;/code&gt;, so their archive volumes stay separated inside a single bucket and a single bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue one restricted application key per host:&lt;/strong&gt; B2 application keys can be scoped to one bucket and one name prefix, so a compromised laptop key cannot read or delete another machine's backup. The master key stays out of every configuration file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A central job over SMB shares deduplicates across machines:&lt;/strong&gt; point one Duplicati instance at mounted network shares and shared files are stored once. The catch is that a laptop out of the house at 02:00 is simply not backed up that night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per machine agents catch laptops, at a cost:&lt;/strong&gt; Duplicati runs on Windows, macOS, Linux and in Docker, so each device can back itself up whenever it has a connection, but identical files on two machines are billed twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Servers and NAS boxes have no subscription path at all:&lt;/strong&gt; a Synology volume, a Proxmox host or a Linux VPS cannot be enrolled in a desktop plan, so mixed estates end up paying for two products anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Count your machines before you compare prices. Three subscriptions against one metered bucket moves the crossover point three times further out.&lt;/p&gt;




&lt;h2&gt;
  
  
  What each product simply refuses to back up
&lt;/h2&gt;

&lt;p&gt;Both tools have hard exclusions, and neither warns you loudly about them. Check this list against what you actually care about before comparing invoices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Backblaze Personal Backup&lt;/th&gt;
&lt;th&gt;Duplicati&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Operating system files, applications, temporary files&lt;/td&gt;
&lt;td&gt;Excluded by default, since the product protects user data rather than a bootable image&lt;/td&gt;
&lt;td&gt;Included if you select the path, though it is not a bare metal image either&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual disk images such as &lt;code&gt;.vmdk&lt;/code&gt;, &lt;code&gt;.vhd&lt;/code&gt; and &lt;code&gt;.iso&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Excluded by default through the client's exclusion rules, editable in settings&lt;/td&gt;
&lt;td&gt;Included, and they deduplicate well between versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network shares and NAS volumes&lt;/td&gt;
&lt;td&gt;Not supported at all&lt;/td&gt;
&lt;td&gt;Supported through any mounted path, including SMB and NFS mounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux hosts and servers&lt;/td&gt;
&lt;td&gt;No client exists&lt;/td&gt;
&lt;td&gt;First class, including headless Docker deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open or locked files&lt;/td&gt;
&lt;td&gt;Handled by the client's own snapshot logic&lt;/td&gt;
&lt;td&gt;Requires &lt;code&gt;--snapshot-policy=on&lt;/code&gt; to use VSS on Windows or LVM on Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live databases&lt;/td&gt;
&lt;td&gt;Files are copied as found, with no consistency guarantee&lt;/td&gt;
&lt;td&gt;Same, unless you dump first with &lt;code&gt;--run-script-before&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two practical consequences follow. If your important data includes VM images or a NAS share, the subscription's unlimited pricing never applies to it, so the comparison collapses before cost enters the picture. If your important data is a laptop's documents and photos, both cover it fully, and you are back to arguing about gigabytes.&lt;/p&gt;

&lt;p&gt;Also check the client's maximum file size setting before trusting it with 80 GB video projects, because a size cap silently skips files rather than failing the backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where should you run Duplicati, and what does the host add to the bill?
&lt;/h2&gt;

&lt;p&gt;Duplicati has to run somewhere that can read your files, stay awake on schedule and hold a local SQLite database. That host is the hidden line item the subscription does not have.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On the machine being backed up:&lt;/strong&gt; the simplest option, with the web interface on port 8200 and no extra hardware cost. The catch is that a sleeping laptop runs no backup, so set a schedule that matches when the lid is actually open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a NAS via Docker:&lt;/strong&gt; one container backs up every share, which suits the central job pattern. Budget the local database on fast storage, because rebuilds on a spinning disk are painfully slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a mini PC or home server:&lt;/strong&gt; a low power box running 24/7 adds electricity and a machine to maintain, but it backs up network shares on a fixed schedule without depending on anyone's laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a VPS or a managed personal server:&lt;/strong&gt; useful when the data already lives off site, for example a web application's files and database dumps. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user, and it sits alongside a self managed VPS, a home server or a NAS as one place to run the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule applies to every choice: a remote host can only back up data it can reach. Running Duplicati off site does nothing for the photos on a laptop unless those photos already sync somewhere the container can read.&lt;/p&gt;

</description>
      <category>duplicati</category>
      <category>backup</category>
      <category>selfhosted</category>
      <category>backblaze</category>
    </item>
    <item>
      <title>OpenClaw vs Subscription AI Assistants: Real Token Costs, Capability Gaps and the Usage Level Where Self-Hosting Wins</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:10:14 +0000</pubDate>
      <link>https://dev.to/john_182319291/openclaw-vs-subscription-ai-assistants-real-token-costs-capability-gaps-and-the-usage-level-where-5en</link>
      <guid>https://dev.to/john_182319291/openclaw-vs-subscription-ai-assistants-real-token-costs-capability-gaps-and-the-usage-level-where-5en</guid>
      <description>&lt;p&gt;OpenClaw wins on cost only at the two extremes of usage: very light use, where a metered token bill stays far below a flat monthly seat fee, and heavy automated use, where a subscription would throttle you long before your work is done. In the middle band, where you send a few dozen messages a day to a frontier model, a flat subscription is usually the cheaper line item, and OpenClaw earns its place on capability and data control instead of price. What OpenClaw actually buys you is a resident agent with shell access, file access, indefinite memory and a heartbeat that acts without being prompted, none of which a chat subscription offers at any tier. Treat the server as the small cost and the model tokens as the variable that decides everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The light personal user, for example someone sending 10 to 20 messages a day and cancelling a 20 USD per month plan:&lt;/strong&gt; run OpenClaw against a mid tier API model, because at that volume metered tokens plus a small VPS usually land under the flat fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The steady daily user, for example a knowledge worker with long threads and large pasted documents:&lt;/strong&gt; keep the subscription and add OpenClaw only for automation, because long context replays are what make metered billing overtake a flat fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The automation builder, for example someone replacing scheduled Zapier or Make scenarios:&lt;/strong&gt; move to OpenClaw, because the heartbeat and tool access do work a chat subscription cannot perform at any price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The privacy driven self-hoster, for example someone who wants files and memory to stay on their own disk:&lt;/strong&gt; run OpenClaw with a local model through Ollama, and accept slower responses and weaker reasoning as the price of zero prompt egress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The developer with an existing homelab, for example someone already running a 4 vCPU and 8 GB server:&lt;/strong&gt; adopt OpenClaw early, because your marginal hosting cost is close to zero and only tokens are new spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The non technical subscriber, for example someone who wants an assistant and not a system to maintain:&lt;/strong&gt; stay subscribed, because OpenClaw adds credential handling, model routing, permission scoping and update duty to your week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff is simple: a subscription gives you a predictable bill and no maintenance, while OpenClaw gives you capability and data control in exchange for a bill that moves with your usage and a system you now own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What does OpenClaw actually do, and what does it replace?&lt;/li&gt;
&lt;li&gt;Flat subscription fees versus metered tokens: two different cost shapes&lt;/li&gt;
&lt;li&gt;What drives an OpenClaw token bill: context, memory and the heartbeat&lt;/li&gt;
&lt;li&gt;How much server does OpenClaw need, and what does that cost to run?&lt;/li&gt;
&lt;li&gt;How much does OpenClaw cost per month at light, steady and heavy usage?&lt;/li&gt;
&lt;li&gt;At what monthly usage does self-hosting OpenClaw actually win?&lt;/li&gt;
&lt;li&gt;Can local models through Ollama cut the OpenClaw token bill to zero?&lt;/li&gt;
&lt;li&gt;What does OpenClaw do that a subscription assistant cannot?&lt;/li&gt;
&lt;li&gt;What do you give up when you cancel the subscription?&lt;/li&gt;
&lt;li&gt;Which of your data leaves the server when OpenClaw calls a hosted model?&lt;/li&gt;
&lt;li&gt;What does OpenClaw cost over three years, including your own time?&lt;/li&gt;
&lt;li&gt;Where should you run OpenClaw: home server, VPS, NAS or managed Personal Cloud Server?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What does OpenClaw actually do, and what does it replace?
&lt;/h2&gt;

&lt;p&gt;OpenClaw is an open source AI agent you run on your own hardware, previously known as Clawdbot and Moltbot. It connects to messaging apps you already use, including WhatsApp, Telegram and Discord, and it brings its own model keys rather than shipping a model. You point it at Anthropic's Claude, OpenAI's GPT models or a local model served through Ollama, and you pay for whatever that model charges per token.&lt;/p&gt;

&lt;p&gt;The distinction that matters for cost is that OpenClaw is an agent, not a chat window. It executes rather than answers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File and shell access:&lt;/strong&gt; OpenClaw reads and writes files on the machine it runs on and can execute terminal commands, so it acts on your actual working directory rather than on pasted excerpts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent memory:&lt;/strong&gt; it retains preferences, ongoing projects and personal detail indefinitely in local storage, instead of losing state when a chat thread ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The heartbeat:&lt;/strong&gt; OpenClaw wakes on its own schedule to check calendars, watch repositories and message you first, which is work no chat subscription performs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging bridges:&lt;/strong&gt; the interface is Telegram or WhatsApp, so the assistant lives beside your other conversations rather than in a separate tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination means OpenClaw replaces two purchases at once. It replaces the flat assistant subscription, typically 20 USD per month at the entry tier for the mainstream products. It also replaces part of an automation subscription such as Zapier or Make, because scheduled, self triggered tasks are native behaviour here. Judge the bill against both, not just the chat plan you are cancelling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flat subscription fees versus metered tokens: two different cost shapes
&lt;/h2&gt;

&lt;p&gt;A subscription and an OpenClaw deployment do not just differ in price. They differ in the shape of the curve, and that shape decides who wins at your volume.&lt;/p&gt;

&lt;p&gt;A subscription is a step function. You pay the same 20 USD per month at the entry tier whether you send 5 messages or 500, until you hit a usage cap and either wait for a reset window or jump to the next tier. OpenClaw is a straight line through the origin. Zero messages costs you only the server. Every message after that adds its own token charge, and nothing resets at midnight.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost dimension&lt;/th&gt;
&lt;th&gt;Flat subscription&lt;/th&gt;
&lt;th&gt;OpenClaw with metered API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost at zero usage&lt;/td&gt;
&lt;td&gt;Full monthly fee, billed regardless&lt;/td&gt;
&lt;td&gt;Server only, no model spend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost at heavy usage&lt;/td&gt;
&lt;td&gt;Capped by tier, then throttled&lt;/td&gt;
&lt;td&gt;Rises linearly with no ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;Exact figure known in advance&lt;/td&gt;
&lt;td&gt;Varies month to month with workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limits&lt;/td&gt;
&lt;td&gt;Enforced per plan tier&lt;/td&gt;
&lt;td&gt;Bounded by provider account limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of a second user&lt;/td&gt;
&lt;td&gt;Usually a second seat fee&lt;/td&gt;
&lt;td&gt;Extra tokens only, one server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle automation&lt;/td&gt;
&lt;td&gt;Not offered on chat plans&lt;/td&gt;
&lt;td&gt;Heartbeat runs bill even when you sleep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two consequences follow. First, the flat plan protects you from your own worst month, while OpenClaw exposes you to it. Second, OpenClaw charges you for activity you never initiated, because a scheduled check that finds nothing still consumed input tokens. Budget for the floor your automation creates before you budget for your conversations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What drives an OpenClaw token bill: context, memory and the heartbeat
&lt;/h2&gt;

&lt;p&gt;Your bill is not driven by how much you type. It is driven by how much the agent has to re-read before it answers you. Output tokens are the smaller half of the invoice for most personal deployments, because a 40 word reply is cheap and the 8,000 token context that produced it is not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context replay on every turn:&lt;/strong&gt; each message resends the conversation so far, so turn 20 of a thread costs far more than turn 2, and one long session can cost more than fifty short ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory injection:&lt;/strong&gt; the indefinite memory that makes OpenClaw useful is loaded into the prompt as text, so a memory store that grows all year quietly raises the floor price of every single request you make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool call round trips:&lt;/strong&gt; one instruction can become three or four model calls, because reading a file, running a command and interpreting the result are separate turns, each carrying the full context again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat wake-ups:&lt;/strong&gt; scheduled checks bill input tokens even when the answer is nothing happened, so a 15 minute interval creates 96 paid calls per day before you say a word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model tier selection:&lt;/strong&gt; a top tier reasoning model can cost several times a mid tier one per million tokens, and OpenClaw lets you route to either, so tier choice moves the bill more than message count does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two levers matter most. Lengthen the heartbeat interval from 15 minutes to 60 and you remove roughly three quarters of that idle spend. Prune memory and start fresh threads, and you cut the per turn floor for everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much server does OpenClaw need, and what does that cost to run?
&lt;/h2&gt;

&lt;p&gt;The hardware side of this comparison is the boring side, and that is the point. OpenClaw's documentation recommends 2 vCPUs and 4 GB of RAM for development and testing, and 4 vCPUs, 8 GB of RAM and at least 20 GB of SSD storage for production use. Those are modest numbers. The agent spends most of its life idle, waiting on a heartbeat or on a reply from a remote model API, so CPU load is bursty and low.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small VPS:&lt;/strong&gt; a 4 vCPU and 8 GB instance from a mainstream provider covers the production baseline, and this is a fixed monthly line item you can quote exactly before you start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing home server or NAS:&lt;/strong&gt; if you already run a machine 24/7, your marginal cost is the electricity for a workload that is idle most hours, which makes tokens your only genuinely new spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Personal Cloud Server:&lt;/strong&gt; Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user, which is a third option beside a self-managed VPS and hardware you own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage growth:&lt;/strong&gt; the 20 GB floor covers the install, but persistent memory, logs and any files the agent touches grow steadily, so watch the volume rather than assuming the baseline holds all year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Budget the server as a flat number and stop thinking about it. On almost every deployment described in this article, hosting is the smaller half of the bill and model tokens are the half that actually varies.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much does OpenClaw cost per month at light, steady and heavy usage?
&lt;/h2&gt;

&lt;p&gt;There is no single answer, because your bill is a fixed server fee plus a token figure that you set by behaviour. What you can do is model the three profiles honestly, in token volume rather than invented currency, then multiply by whatever your chosen model charges per million tokens on the day you sign up.&lt;/p&gt;

&lt;p&gt;Assume the production baseline of 4 vCPUs and 8 GB in all three cases, so the server line is identical and only the model spend moves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usage profile&lt;/th&gt;
&lt;th&gt;Monthly workload&lt;/th&gt;
&lt;th&gt;What dominates the bill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;10 to 20 short messages a day, heartbeat at 60 minutes, memory pruned&lt;/td&gt;
&lt;td&gt;Server fee, with token spend a minority of the total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady&lt;/td&gt;
&lt;td&gt;50 to 100 messages a day, long threads, documents pasted in&lt;/td&gt;
&lt;td&gt;Context replay, which grows faster than message count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy&lt;/td&gt;
&lt;td&gt;Continuous automation, heartbeat at 15 minutes, multi step tool chains&lt;/td&gt;
&lt;td&gt;Tool call round trips, at three or four model calls per instruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local only&lt;/td&gt;
&lt;td&gt;Any volume, model served by Ollama on your own hardware&lt;/td&gt;
&lt;td&gt;Electricity and RAM, with no per token charge at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules make the arithmetic tractable. First, multiply your daily message count by your average context length, not by your typing length, because the context is what you actually pay for. Second, add the heartbeat separately: at a 15 minute interval that is 96 calls a day, or roughly 2,880 a month, each carrying your memory payload whether or not anything happened.&lt;/p&gt;

&lt;p&gt;Price those two numbers against your provider's published rate and you have your real figure.&lt;/p&gt;




&lt;h2&gt;
  
  
  At what monthly usage does self-hosting OpenClaw actually win?
&lt;/h2&gt;

&lt;p&gt;The break-even is not one number, it is two crossing points, and most people sit between them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Below roughly 20 short messages a day:&lt;/strong&gt; OpenClaw usually wins on price, because a metered bill at that volume is a fraction of a flat 20 USD entry tier, and your only guaranteed cost is the server. Cancel with confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Between 20 and 100 messages a day on a frontier model:&lt;/strong&gt; the subscription usually wins, because context replay compounds and you are paying full rate for exactly the conversational usage that a flat plan was designed to absorb. This is the widest band and the one people miscalculate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Above the plan's throttle point:&lt;/strong&gt; OpenClaw wins again, not because tokens got cheaper but because the subscription stops serving you and the metered account does not. If you regularly hit reset windows, the comparison is no longer price against price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any automation at all:&lt;/strong&gt; OpenClaw wins by default, because there is no subscription tier that runs a 15 minute heartbeat against your calendar and repositories on your behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the hardware is already paid for:&lt;/strong&gt; the crossing point moves in OpenClaw's favour, since the fixed side of the equation collapses. This applies to a home server, a NAS, an existing VPS or a managed Personal Cloud Server such as Yundera.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Work it out this way: divide your server cost by your model's blended per million token rate to get your monthly token allowance at break-even, then compare that allowance to the volume you modelled earlier. If your projected usage is under it, self-hosting is cheaper. If it is well over, you are buying capability, not savings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can local models through Ollama cut the OpenClaw token bill to zero?
&lt;/h2&gt;

&lt;p&gt;Yes, and that is the wrong way to think about it. Ollama removes the per token charge entirely, then hands you three replacement costs: hardware, latency and capability. OpenClaw supports local models as a first class option, so the plumbing is not the problem. The economics of the machine underneath it are.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware moves from optional to mandatory:&lt;/strong&gt; the 4 vCPU and 8 GB production baseline is sized for an agent that calls a remote API, not one that runs inference locally, so useful local models push you into far more RAM and usually a GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost becomes capital, not subscription:&lt;/strong&gt; you pay once for the hardware and then only for electricity, which flips the comparison from a monthly line item to an amortisation question over 24 or 36 months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability drops at the reasoning end:&lt;/strong&gt; local models handle summarising, routing and short replies acceptably, while multi step tool chains and long context planning are where the gap against a frontier model shows most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency becomes visible:&lt;/strong&gt; a remote API answers in seconds regardless of your hardware, while a local model on modest hardware makes the heartbeat and every tool round trip noticeably slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt egress goes to zero:&lt;/strong&gt; nothing leaves your machine, which is the one benefit no amount of API spend can buy back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pragmatic setup is hybrid. Route the heartbeat and routine classification to a local model, because that is high volume, low difficulty work that was inflating your bill, and reserve a hosted frontier model for the requests that genuinely need reasoning. That combination usually cuts spend more than going fully local does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does OpenClaw do that a subscription assistant cannot?
&lt;/h2&gt;

&lt;p&gt;The capability gap is not about answer quality. On the same underlying model, a hosted chat product and OpenClaw produce comparable text. The gap is about what happens between messages, and about who decides the rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Act without being asked:&lt;/strong&gt; the heartbeat gives OpenClaw permission to start work, so it can check a repository at 07:00 and message you the result, while a chat product only ever responds to a prompt you typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach the real filesystem:&lt;/strong&gt; OpenClaw operates on the directory it runs in, so it edits your actual files and runs commands against them rather than working from a copy you pasted into a box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route per task, not per plan:&lt;/strong&gt; you choose Claude for one job, a GPT model for another and Ollama for the heartbeat, and you can change that routing in an afternoon, whereas a subscription binds you to one vendor's model lineup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retain state indefinitely:&lt;/strong&gt; memory persists on your disk with no thread limit and no retention window imposed from outside, so context from eight months ago is still available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore the reset clock:&lt;/strong&gt; there is no plan tier that pauses you at a usage cap, so a long automated run finishes instead of stopping partway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live where your messages already are:&lt;/strong&gt; the assistant answers inside Telegram, WhatsApp or Discord, which means household members and mobile use come free rather than requiring extra seats.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is free. Every item on this list is also an item on the risk list, because an agent that can run commands, keep everything you say and act at 03:00 without supervision is exactly as consequential as it sounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What do you give up when you cancel the subscription?
&lt;/h2&gt;

&lt;p&gt;Honest accounting means listing the losses, and they are real. A subscription is not only a model behind a login. It is a bundle of features that other teams built, tested and support, and OpenClaw hands most of that work back to you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The polished client:&lt;/strong&gt; you lose native mobile and desktop apps, voice input, document upload widgets and image generation surfaces, and you get a Telegram or WhatsApp thread instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The predictable bill:&lt;/strong&gt; you lose the guarantee that a runaway month cannot happen, because nothing between your agent and the provider stops a misconfigured loop from spending until you notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone else's uptime:&lt;/strong&gt; you become the operator, so container restarts, expired API keys, provider outages and OS updates are now yours to handle at whatever hour they occur.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor side safety rails:&lt;/strong&gt; account level abuse handling, content filtering and rate limiting were doing quiet work, and an agent with shell access and no supervisor is a different risk profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated extras:&lt;/strong&gt; features bundled into the 20 USD tier, from web browsing surfaces to connectors, become things you configure and maintain individually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the time cost explicitly. The first week is genuine setup work: provisioning the server, pairing a messaging channel, scoping tool permissions, setting the heartbeat interval and testing what the agent is allowed to touch. After that, budget an hour or two a month for updates and key rotation. If that hour is worth more to you than the price difference you calculated earlier, the subscription is the rational choice and no amount of capability changes that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which of your data leaves the server when OpenClaw calls a hosted model?
&lt;/h2&gt;

&lt;p&gt;Self-hosting the agent is not the same as self-hosting the inference. If OpenClaw is routed to Claude or a GPT model, the prompt goes to that provider exactly as it would from a subscription client. What changes is that you control which bytes get put into the prompt in the first place.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data category&lt;/th&gt;
&lt;th&gt;Stays on your server&lt;/th&gt;
&lt;th&gt;Sent to the provider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Files and directories&lt;/td&gt;
&lt;td&gt;The files themselves, at rest on your disk&lt;/td&gt;
&lt;td&gt;Any excerpt the agent reads into context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent memory&lt;/td&gt;
&lt;td&gt;The full store, held locally&lt;/td&gt;
&lt;td&gt;Whatever slice is injected into a given request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message history&lt;/td&gt;
&lt;td&gt;Complete archive, retained as long as you choose&lt;/td&gt;
&lt;td&gt;The replayed context window for each turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command output&lt;/td&gt;
&lt;td&gt;Logs and results on the machine&lt;/td&gt;
&lt;td&gt;Any output fed back for interpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;Nothing, if you host it yourself&lt;/td&gt;
&lt;td&gt;Timing, volume and account identity per call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two consequences follow. First, your provider sees a stream shaped by your automation, not just your typing, so a chatty heartbeat leaks more operational detail than a human ever would. Second, the storage side of the privacy question genuinely is resolved by self-hosting: the memory store, the file tree and the transcripts sit on hardware you chose, whether that is a home server, a NAS, a VPS or a managed Personal Cloud Server such as Yundera, and no multi-tenant service holds a copy.&lt;/p&gt;

&lt;p&gt;If prompt egress itself is the concern rather than storage, only local inference answers it. Everything else is scope reduction, which is worth doing but is not the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does OpenClaw cost over three years, including your own time?
&lt;/h2&gt;

&lt;p&gt;Three years is the right window, because it is long enough for hardware to amortise and for token prices to move. Build the total from five components rather than one monthly figure, and price your labour at whatever your hour is genuinely worth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server cost, 36 months:&lt;/strong&gt; multiply your monthly hosting figure by 36. This is the only component you can quote precisely on day one, and on a 4 vCPU and 8 GB baseline it is usually the smallest variable in the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token spend, 36 months:&lt;/strong&gt; take your modelled monthly volume and multiply, then apply a downward adjustment. Per token prices at a given capability tier have trended down, so holding today's rate flat for three years overstates the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup labour, one time:&lt;/strong&gt; the first week is real work. Count 6 to 12 hours across provisioning, channel pairing, permission scoping and testing, then price it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance labour, recurring:&lt;/strong&gt; at 1 to 2 hours a month, that is 36 to 72 hours over three years for updates, key rotation and incident handling. This line frequently exceeds every cash cost in the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration risk:&lt;/strong&gt; budget one rebuild. Something breaks, a provider changes an interface, or you move hosts, and that is another day of your time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then compute the comparison honestly: 36 months of a 20 USD subscription is 720 USD per seat, with zero labour hours attached. If your OpenClaw total lands near that, the deciding factor was never money. It was capability and data control, and you should say so out loud when you choose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where should you run OpenClaw: home server, VPS, NAS or managed Personal Cloud Server?
&lt;/h2&gt;

&lt;p&gt;The agent is small, so this decision is about reachability and operating burden, not horsepower. OpenClaw needs to be online when the heartbeat fires and reachable from your phone when you message it, which is where most home deployments get complicated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hosting option&lt;/th&gt;
&lt;th&gt;What it demands from you&lt;/th&gt;
&lt;th&gt;Main constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Home server&lt;/td&gt;
&lt;td&gt;OS updates, backups, remote access setup&lt;/td&gt;
&lt;td&gt;Residential connections and dynamic IPs make inbound reachability the hard part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAS with Docker&lt;/td&gt;
&lt;td&gt;Container management on an appliance&lt;/td&gt;
&lt;td&gt;Often fewer than 4 usable vCPUs once existing services are running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-managed VPS&lt;/td&gt;
&lt;td&gt;Full sysadmin duty, firewall and TLS&lt;/td&gt;
&lt;td&gt;Predictable cost and uptime, but every layer is yours to patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Personal Cloud Server&lt;/td&gt;
&lt;td&gt;App installation and configuration only&lt;/td&gt;
&lt;td&gt;Less low level control than a bare VPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yundera is one option in that last row, alongside self-managed alternatives. Apps are installed from an app store in one click rather than assembled from compose files by hand, and each app is reachable on a public HTTPS subdomain via NSL.SH mesh routing, so no static IP, no port forwarding and no manual TLS certificate setup are required.&lt;/p&gt;

&lt;p&gt;Two practical points decide most cases. First, if the machine sleeps or the connection drops, the heartbeat silently stops and you lose the capability you left the subscription for, which rules out laptops and anything on a flaky link. Second, whatever you choose, back up the memory store and configuration, because after twelve months that directory holds the accumulated context that makes the agent worth running at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this article is published by &lt;a href="https://yundera.com" rel="noopener noreferrer"&gt;Yundera&lt;/a&gt;, a managed Personal Cloud Server for running self-hosted apps. It is mentioned above as one option among several.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>selfhosted</category>
      <category>ai</category>
      <category>costs</category>
    </item>
    <item>
      <title>When DokuWiki Is the Wrong Replacement for Notion or Confluence Cloud: The Gaps No Plugin Closes</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:48:19 +0000</pubDate>
      <link>https://dev.to/john_182319291/when-dokuwiki-is-the-wrong-replacement-for-notion-or-confluence-cloud-the-gaps-no-plugin-closes-k83</link>
      <guid>https://dev.to/john_182319291/when-dokuwiki-is-the-wrong-replacement-for-notion-or-confluence-cloud-the-gaps-no-plugin-closes-k83</guid>
      <description>&lt;p&gt;DokuWiki replaces the part of Notion and Confluence Cloud that stores and serves written pages, and it does that job well on a flat file store with no database. It does not replace simultaneous multi cursor editing, database views with filters and rollups, native comment threads with mentions, or first party mobile apps, and three of those gaps have no plugin that closes them properly. Everything else you want back arrives as a community plugin that you then patch, test and carry across every future upgrade yourself. If your team's daily work happens inside a shared page during a call, or inside a filtered board view, stay on your paid plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo consultant with a reference library (one writer, roughly 400 pages of notes and client docs):&lt;/strong&gt; switch, because you never hit the locking model, and the full text index plus namespace ACLs cover everything you were paying a seat for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five person engineering team that already lives in pull requests (backend squad, docs reviewed alongside code):&lt;/strong&gt; switch, because plain text pages in a versioned directory match how you already work, and you can accept the editor being one person at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twelve person marketing or ops team running trackers in Notion databases (content calendar, campaign status, filtered by owner and date):&lt;/strong&gt; stay, because the struct plugin gives you typed fields and aggregation, not the relations, rollups and multiple saved views your calendar depends on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed team that co writes in one page during a video call (a weekly planning doc touched by six people at once):&lt;/strong&gt; stay, because DokuWiki takes an exclusive page lock and the second editor is told to wait or fork the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulated company with an SSO mandate and joiner and leaver automation (60 staff, identity managed in Entra ID or Okta):&lt;/strong&gt; stay unless you accept manual account lifecycle, because OAuth login is a community plugin and SCIM provisioning has no DokuWiki equivalent at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Association, school or club with a small budget and mostly stable reference content (handbooks, procedures, meeting minutes):&lt;/strong&gt; switch, because your content is exactly the shape DokuWiki was built for and your collaboration is asynchronous anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real tradeoff: you exchange a per seat bill and a vendor's feature roadmap for zero licence cost and a permanent maintenance duty on every plugin that stands in for a feature you used to rent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Can two people edit the same DokuWiki page at the same time?&lt;/li&gt;
&lt;li&gt;Is there a real WYSIWYG editor for DokuWiki, and what does it cost you in markup?&lt;/li&gt;
&lt;li&gt;What replaces Notion database views, relations and rollups in DokuWiki?&lt;/li&gt;
&lt;li&gt;How granular are DokuWiki ACLs next to Confluence page restrictions?&lt;/li&gt;
&lt;li&gt;Are there official DokuWiki mobile apps, and what do you actually use on a phone?&lt;/li&gt;
&lt;li&gt;Can DokuWiki do SSO, and what happens to SCIM provisioning?&lt;/li&gt;
&lt;li&gt;Where do comment threads, mentions and notifications end up?&lt;/li&gt;
&lt;li&gt;Which paid features DokuWiki genuinely beats, and why that still matters&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Can two people edit the same DokuWiki page at the same time?
&lt;/h2&gt;

&lt;p&gt;No. DokuWiki takes an exclusive lock on a page the moment you open the editor, and the second person is refused entry until that lock expires or the first person saves. There is no operational transformation layer, no CRDT, no shared cursors. This is the single largest behavioural difference from Notion and Confluence Cloud, and no plugin in the repository fixes it, because the storage model writes one whole text file per page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lock timeout is 15 minutes by default:&lt;/strong&gt; the &lt;code&gt;locktime&lt;/code&gt; setting in &lt;code&gt;conf/local.php&lt;/code&gt; holds 900 seconds, and the lock file sits in &lt;code&gt;data/locks/&lt;/code&gt; named after the page ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lock refreshes while you type:&lt;/strong&gt; DokuWiki sends a keepalive from the edit form, so a colleague reading the "is currently being edited" message has no reliable estimate of when the page frees up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draft recovery is per user, not shared:&lt;/strong&gt; unsaved text lands in &lt;code&gt;data/cache/&lt;/code&gt; as a draft tied to your session, so a crashed browser loses nothing, but nobody else sees your work in progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section editing is the real workaround:&lt;/strong&gt; DokuWiki locks per page, so splitting a long document into several pages under one namespace lets two people work in parallel on adjacent content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Edittable and Wrap plugins do not change this:&lt;/strong&gt; they alter markup, not concurrency, and both still require the page level lock.&lt;/p&gt;

&lt;p&gt;Teams that treat the wiki as an asynchronous record barely notice. Teams that open one planning page during a stand up and expect six people to type into it will hit the wall on day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is there a real WYSIWYG editor for DokuWiki, and what does it cost you in markup?
&lt;/h2&gt;

&lt;p&gt;Sort of. The stock editor is a plain textarea with a button toolbar, and the markup is DokuWiki syntax, not Markdown: headings are &lt;code&gt;====== Title ======&lt;/code&gt;, bold is &lt;code&gt;**bold**&lt;/code&gt;, italic is &lt;code&gt;//italic//&lt;/code&gt;. A true WYSIWYG surface exists as the community plugin &lt;code&gt;ckgedit&lt;/code&gt;, which drops a CKEditor instance over that textarea. It works, and it is the reason most former Confluence users can be onboarded at all, but the conversion runs both ways and the return trip is where you pay.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Editing option&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;What it costs you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stock toolbar editor&lt;/td&gt;
&lt;td&gt;Raw syntax, section edit links, zero conversion risk&lt;/td&gt;
&lt;td&gt;Every writer learns a markup dialect that transfers nowhere else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ckgedit&lt;/code&gt; plugin&lt;/td&gt;
&lt;td&gt;Rich text buttons, table handling, drag and drop of images&lt;/td&gt;
&lt;td&gt;Round tripping can rewrite or strip syntax from other plugins, so complex pages need a raw pass afterwards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown syntax plugins&lt;/td&gt;
&lt;td&gt;Familiar &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt; for developers&lt;/td&gt;
&lt;td&gt;Two dialects now coexist in one wiki, and search or find and replace has to cover both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paste from Notion or Confluence&lt;/td&gt;
&lt;td&gt;Fastest way to move a single page&lt;/td&gt;
&lt;td&gt;Nested toggles, callouts and column layouts arrive as flattened paragraphs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two practical thresholds matter. First, any page carrying plugin calls such as &lt;code&gt;{{page&amp;gt;...}}&lt;/code&gt; or struct fields deserves editing in raw mode only. Second, decide before rollout, because mixing editors across 300 pages leaves you with inconsistent whitespace and heading levels that nobody wants to normalise later.&lt;/p&gt;

&lt;p&gt;Notion and Confluence give you one editor with one behaviour. DokuWiki gives you two, and you own the seam between them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What replaces Notion database views, relations and rollups in DokuWiki?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;struct&lt;/code&gt; plugin, and it gets you about two thirds of the way. It attaches typed fields to pages through named schemas, stores them in SQLite via the &lt;code&gt;sqlite&lt;/code&gt; plugin dependency, and renders aggregations with syntax such as &lt;code&gt;{{struct table&amp;gt;projects}}&lt;/code&gt;. The older &lt;code&gt;data&lt;/code&gt; plugin does something similar with &lt;code&gt;---- dataentry ----&lt;/code&gt; blocks. Both are real tools. Neither is a database product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed fields work properly:&lt;/strong&gt; text, date, decimal, dropdown, user and page reference types all validate on save, so a status column stays a controlled vocabulary rather than free text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relations exist, in one direction only:&lt;/strong&gt; the Page and Lookup field types point one page at another, and you can list what points back, but there is no rollup that sums or counts a related set into a parent field the way a Notion rollup does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Views are markup, not saved objects:&lt;/strong&gt; each filtered list is a wiki page containing an aggregation block, so changing a filter means editing a page. Nobody gets a personal view, and there is no per user sort state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Board, calendar and timeline layouts are not included:&lt;/strong&gt; struct renders tables and lists. If your team plans in a kanban column or a month grid, that presentation layer is missing, and dragging a card between columns does not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forms need a second plugin:&lt;/strong&gt; &lt;code&gt;bureaucracy&lt;/code&gt; builds the entry form that creates a page from a template, so a simple request tracker is a two plugin arrangement before you write any content.&lt;/p&gt;

&lt;p&gt;Reference tables migrate cleanly. Anything your team actually manipulates as a board does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  How granular are DokuWiki ACLs next to Confluence page restrictions?
&lt;/h2&gt;

&lt;p&gt;Granular enough on the read and write axis, and clumsy on the organisational axis. DokuWiki ships an ACL manager in the admin area that writes plain rules to &lt;code&gt;conf/acl.auth.php&lt;/code&gt;. Every rule maps a page or namespace to a user or a group and one of six permission levels: none (0), read (1), edit (2), create (4), upload (8) and delete (16). The most specific matching rule wins, and a user rule overrides a group rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Namespaces carry the model, not pages:&lt;/strong&gt; you can write a rule for a single page, but the design assumes &lt;code&gt;hr:*&lt;/code&gt; and &lt;code&gt;hr:salaries&lt;/code&gt; style trees, so your permission scheme has to be encoded in your page naming before you migrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only admins can change access:&lt;/strong&gt; in Confluence any editor restricts their own page from the page menu. In DokuWiki that self service does not exist, so every access request becomes a ticket for whoever holds the admin account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moving a page moves its permissions:&lt;/strong&gt; renaming &lt;code&gt;hr:draft:review&lt;/code&gt; into a public namespace silently changes who can read it, which makes the &lt;code&gt;move&lt;/code&gt; plugin a permissions tool as much as a housekeeping one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media inherits from the namespace it sits in:&lt;/strong&gt; an image uploaded into a restricted namespace stays restricted, but the same file linked from a public page is served from its original path, so upload location decides exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read denial is total:&lt;/strong&gt; a page you cannot read does not appear in search results, in the index, or in recent changes, with no "restricted content exists here" hint.&lt;/p&gt;

&lt;p&gt;Three groups and a clean tree work well. Forty ad hoc exceptions do not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Are there official DokuWiki mobile apps, and what do you actually use on a phone?
&lt;/h2&gt;

&lt;p&gt;There are none. DokuWiki has no first party iOS or Android client, and it never has. What you get instead is a responsive default template that reflows to a phone screen, plus a remote API that third party clients can talk to. For reading, this is fine. For the workflows people actually do on a phone, capturing a note in a lift or clearing comments on a commute, it is a visible downgrade from a paid Notion or Confluence seat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser is the client:&lt;/strong&gt; the bundled template collapses the sidebar and sizes text sensibly, and templates such as &lt;code&gt;bootstrap3&lt;/code&gt; or &lt;code&gt;mikio&lt;/code&gt; improve it further, but you are still typing wiki syntax into a textarea with a phone keyboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no offline mode:&lt;/strong&gt; every page view is a request to your server. Lose signal on a train and the wiki is gone, where the Confluence and Notion apps keep recently opened pages cached locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are no push notifications:&lt;/strong&gt; DokuWiki's change alerts arrive by email subscription, so a phone gets them in a mail app, not as a badge on a wiki icon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third party clients exist but depend on the API:&lt;/strong&gt; you must enable the &lt;code&gt;remote&lt;/code&gt; option in &lt;code&gt;conf/local.php&lt;/code&gt;, and optionally restrict it with &lt;code&gt;remoteuser&lt;/code&gt;, before any external app can authenticate and post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Photo capture is a file upload, not an inline action:&lt;/strong&gt; the media manager accepts a camera file through the standard browser picker, then you paste the resulting &lt;code&gt;{{:image.jpg}}&lt;/code&gt; syntax by hand.&lt;/p&gt;

&lt;p&gt;Add the site to your home screen and accept it is a website. Anyone expecting parity with an app store client will be disappointed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can DokuWiki do SSO, and what happens to SCIM provisioning?
&lt;/h2&gt;

&lt;p&gt;Single sign on: yes, through plugins or a proxy. Automated provisioning: no, and there is no equivalent to build on. This is the pairing that decides the question for most companies with an identity team, because the login half looks solved in a demo and the lifecycle half only hurts three months later when a leaver still has an account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory authentication is the mature path:&lt;/strong&gt; the bundled &lt;code&gt;authad&lt;/code&gt; and &lt;code&gt;authldap&lt;/code&gt; backends bind to Active Directory or LDAP, read group membership at login, and feed those groups straight into your ACL rules, so disabling an account in the directory blocks the next sign in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth and OpenID Connect arrive as plugins:&lt;/strong&gt; the community &lt;code&gt;oauth&lt;/code&gt; plugin plus a per provider service plugin covers Google, GitHub, Keycloak and generic OIDC endpoints, and it creates the local user record on first successful login.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAML is usually solved outside DokuWiki:&lt;/strong&gt; put an authenticating reverse proxy such as Authelia, Authentik or oauth2-proxy in front, then let DokuWiki trust the forwarded user header, which keeps the wiki ignorant of the protocol entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCIM does not exist here:&lt;/strong&gt; no endpoint, no plugin, no group push. Joiners, leavers and group changes are either mirrored by your LDAP backend or performed by hand in the user manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local accounts linger by default:&lt;/strong&gt; the plain backend keeps entries in &lt;code&gt;conf/users.auth.php&lt;/code&gt; until an admin removes them, and an active session survives until the cookie expires, so a deprovisioned employee is not necessarily locked out that minute.&lt;/p&gt;

&lt;p&gt;If your compliance evidence includes automated deprovisioning reports, DokuWiki cannot produce them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where do comment threads, mentions and notifications end up?
&lt;/h2&gt;

&lt;p&gt;Split across three mechanisms, none of them native to a page. DokuWiki has no comment system at all in the core. What it has is a subscription engine for change notifications, and a community plugin that bolts a comment box onto the bottom of pages. Inline comments anchored to a selected sentence, the feature Confluence reviewers live in, have no equivalent anywhere in the ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments come from the &lt;code&gt;discussion&lt;/code&gt; plugin:&lt;/strong&gt; it renders a form under the page, supports replies one level deep, and offers moderation so posts wait for approval. Threads are per page, never per paragraph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment data lives outside the page file:&lt;/strong&gt; posts are stored in the metadata directory alongside the page, not inside the &lt;code&gt;.txt&lt;/code&gt; source, so a page export carries the content and leaves the conversation behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no resolve workflow:&lt;/strong&gt; a Confluence thread can be marked resolved and collapsed. Here a handled comment stays visible until someone deletes it, which pushes teams back to email or chat for review cycles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mentions do not notify anyone:&lt;/strong&gt; typing a colleague's name creates text. The core subscription system is the only delivery path, and it is opt in per user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscriptions are per page or per namespace, in three modes:&lt;/strong&gt; every change, digest, or list. Digest and list batches are dispatched by &lt;code&gt;lib/exe/taskrunner.php&lt;/code&gt;, which fires on ordinary page requests, so a quiet wiki delivers its summaries late.&lt;/p&gt;

&lt;p&gt;The practical result: your review conversations move to Slack, Teams or email, and the wiki keeps only the outcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which paid features DokuWiki genuinely beats, and why that still matters
&lt;/h2&gt;

&lt;p&gt;Not every comparison runs one way. On four points DokuWiki is not merely cheaper, it is structurally better than what a hosted plan can offer, and those points are exactly why teams tolerate the gaps in the previous sections.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;What DokuWiki does&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Content storage&lt;/td&gt;
&lt;td&gt;Every page is a UTF-8 file at &lt;code&gt;data/pages/&amp;lt;namespace&amp;gt;/&amp;lt;page&amp;gt;.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You can &lt;code&gt;grep -r&lt;/code&gt;, script bulk edits, or put the tree in Git with no export step and no API rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revision history&lt;/td&gt;
&lt;td&gt;Old versions are gzipped into &lt;code&gt;data/attic&lt;/code&gt; with a Unix timestamp in the filename&lt;/td&gt;
&lt;td&gt;History survives independently of the application, so a broken install still leaves readable text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup and restore&lt;/td&gt;
&lt;td&gt;Copy &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;conf/&lt;/code&gt; and &lt;code&gt;lib/plugins/&lt;/code&gt; with rsync&lt;/td&gt;
&lt;td&gt;A full restore is a directory copy onto a fresh install, with no database dump, no schema migration, no vendor ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User count&lt;/td&gt;
&lt;td&gt;Accounts and page count carry no licence cost&lt;/td&gt;
&lt;td&gt;Adding a contractor for two weeks is a line in a file, not a change to your billing tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Longevity is the underrated one:&lt;/strong&gt; DokuWiki has shipped on the same flat file model for years, and syntax written a decade ago still renders. Hosted editors change their block model on their own schedule, and your archive changes with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The absence of a database is the second:&lt;/strong&gt; no PostgreSQL to patch, no connection pool to tune, no migration window. On a small server, the runtime is PHP and a filesystem.&lt;/p&gt;

&lt;p&gt;If your content is meant to outlive the tool holding it, this column of the ledger carries real weight.&lt;/p&gt;

</description>
      <category>dokuwiki</category>
      <category>selfhosted</category>
      <category>wiki</category>
      <category>php</category>
    </item>
    <item>
      <title>Netdata Cloud vs a Self Hosted Parent Node: The Real Three Year Cost for a Home Server</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:35:22 +0000</pubDate>
      <link>https://dev.to/john_182319291/netdata-cloud-vs-a-self-hosted-parent-node-the-real-three-year-cost-for-a-home-server-3mah</link>
      <guid>https://dev.to/john_182319291/netdata-cloud-vs-a-self-hosted-parent-node-the-real-three-year-cost-for-a-home-server-3mah</guid>
      <description>&lt;p&gt;If you run Netdata on one to three machines at home, the local agent dashboard on port 19999 plus the free Netdata Cloud tier covers almost everything you need, and a paid plan rarely earns its keep across three years. Pay only when you need durable multi node views, alert routing that reaches other people, and role based access you can revoke. Self hosting a Netdata parent node is the cheapest route to long retention and one dashboard for every child node, and the price is one always on machine plus a weekend of setup. Bolting Prometheus and Grafana onto Netdata is the most expensive option measured in hours, and it only pays back if you already run that stack for something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo dev with one box (Marco, one mini PC running 14 containers):&lt;/strong&gt; stay on the local agent and the free tier, because a single node needs no aggregation layer and the agent already stores its own history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo dev with four to six nodes (Priya, a NAS, two VPS and a spare laptop):&lt;/strong&gt; build a self hosted parent node, since streaming gives you one dashboard and one retention policy without a per node bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev who checks metrics from a phone while away (Tomas, on call for his own side project):&lt;/strong&gt; use the free Cloud tier or a WireGuard tunnel home, because remote reachability is the one thing a local only setup genuinely lacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev already running Prometheus and Grafana (Ines, with existing dashboards and recording rules):&lt;/strong&gt; keep Netdata as a collector and export to your existing stack rather than paying twice for storage and dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev sharing nodes with other people (Sam, two contractors with root on the same servers):&lt;/strong&gt; a paid plan is the cleanest answer, since roles, invitations and revocation are exactly what self hosting makes you build by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev with hard data residency rules (Lena, contract work under a strict client policy):&lt;/strong&gt; parent node only, because it keeps every metric and every alert inside machines you control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is straightforward: Netdata Cloud sells you remote access, retention and permissions as a subscription, and a parent node buys you the same three things with your own hardware, your own tunnel and your own maintenance hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What does Netdata Cloud actually add on top of the agent you already run?&lt;/li&gt;
&lt;li&gt;What does the free tier stop giving you?&lt;/li&gt;
&lt;li&gt;How much does Netdata Cloud really cost over three years?&lt;/li&gt;
&lt;li&gt;What does a self hosted Netdata parent node cost over the same three years?&lt;/li&gt;
&lt;li&gt;How do you size a parent node for a home server's metric volume?&lt;/li&gt;
&lt;li&gt;How much bandwidth does streaming from child to parent actually use?&lt;/li&gt;
&lt;li&gt;How do the dbengine tiers change your retention bill on a parent node?&lt;/li&gt;
&lt;li&gt;What does bolting Prometheus and Grafana onto Netdata cost you in hours and hardware?&lt;/li&gt;
&lt;li&gt;Which setup gives you alerts that actually reach you, and what do notifications cost?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What does Netdata Cloud actually add on top of the agent you already run?
&lt;/h2&gt;

&lt;p&gt;The agent is not a thin client. Every collector, every chart, every health alarm and the entire time series database run on the machine itself, and &lt;code&gt;http://localhost:19999&lt;/code&gt; serves the full dashboard whether Cloud is connected or not. Netdata Cloud does not collect your metrics a second time. It adds a control plane on top of agents that already work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single view across nodes:&lt;/strong&gt; Cloud stitches your connected agents into one interface, so a chart can span four machines instead of forcing you to open four browser tabs on four addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reachability without a tunnel:&lt;/strong&gt; the agent opens an outbound connection to Cloud, so you get remote access without exposing port 19999, forwarding a port on your router, or standing up WireGuard or Tailscale yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity and permissions:&lt;/strong&gt; Cloud is where accounts, invitations, Spaces, Rooms and role assignments live. A standalone agent has no user model at all, so anyone who reaches the port sees everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centralised alert state:&lt;/strong&gt; the agent evaluates health entities locally and can send notifications by itself, but Cloud aggregates active alerts from every node and routes them through integrations configured once for the whole Space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fleet metadata and history:&lt;/strong&gt; node inventory, which agents are online, and alert history that outlives the individual agent's database.&lt;/p&gt;

&lt;p&gt;The distinction matters for cost. If you disconnect Cloud tomorrow, collection, retention and local alarms keep running unchanged. What you lose is the aggregation layer, and that layer is exactly what a self hosted parent node also provides.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does the free tier stop giving you?
&lt;/h2&gt;

&lt;p&gt;Start with what it does not touch. The free Community tier places no cap on collection frequency, no cap on the metrics the agent gathers, and no cap on how long your own machine retains them. Retention is decided by &lt;code&gt;netdata.conf&lt;/code&gt; and your disk, not by your plan. The limits sit in the control plane instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced notification integrations:&lt;/strong&gt; email and basic delivery are available without paying, while the routed integrations most teams want, PagerDuty and Opsgenie among them, sit on paid plans. The agent itself can still fire notifications directly through &lt;code&gt;health_alarm_notify.conf&lt;/code&gt;, which is the free workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granular roles:&lt;/strong&gt; free Spaces give you a small set of role options. Fine grained separation, the kind you need when a contractor should read dashboards but never silence an alarm, is a paid feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extended alert and event history in Cloud:&lt;/strong&gt; the window Cloud keeps for alert transitions and audit events is shorter on the free tier. Your agent keeps its own health log regardless, so this is convenience, not data loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support commitments:&lt;/strong&gt; community support versus a contractual response time is a real difference, and it is worth close to nothing when the only person on call is you.&lt;/p&gt;

&lt;p&gt;Two things to check before you budget. Plan boundaries move, so read the current pricing page rather than a blog post, this one included. And every paid capability above is a permissions or routing feature, not a monitoring feature. Nothing you pay for makes the agent see more. That single observation is what makes the three year arithmetic in the next section lean the way it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much does Netdata Cloud really cost over three years?
&lt;/h2&gt;

&lt;p&gt;Netdata Cloud paid plans bill per node per month, so your three year total is not one number, it is a multiplication. Thirty six months times your node count times the plan rate, plus whatever your node count does over that period. Rates change, so pull the current figure from the pricing page and run the arithmetic yourself. The shape of the bill matters more than today's rate, because the shape is what makes it grow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost driver&lt;/th&gt;
&lt;th&gt;How it scales&lt;/th&gt;
&lt;th&gt;Effect on a 36 month total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node count&lt;/td&gt;
&lt;td&gt;Linear, and a node is any claimed agent&lt;/td&gt;
&lt;td&gt;Six nodes cost six times one node, every month, forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container hosts&lt;/td&gt;
&lt;td&gt;One Docker host is one node, not one per container&lt;/td&gt;
&lt;td&gt;Cheap if you consolidate, expensive if you spread services across boxes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ephemeral nodes&lt;/td&gt;
&lt;td&gt;Test VMs and rebuilt machines can each claim a slot&lt;/td&gt;
&lt;td&gt;Silent creep, so prune stale nodes in the Space regularly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing period&lt;/td&gt;
&lt;td&gt;Annual commitment usually beats month to month&lt;/td&gt;
&lt;td&gt;Locks the rate for twelve months and removes one variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan tier&lt;/td&gt;
&lt;td&gt;Step function, not linear&lt;/td&gt;
&lt;td&gt;Adding one contractor who needs a restricted role can move the whole Space up a tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two habits keep the total honest. Run &lt;code&gt;netdata claim&lt;/code&gt; deliberately rather than baking a claiming token into every image you build, otherwise a rebuild loop quietly adds billable nodes. And decide your node count for year three, not year one, because a home lab that starts at two machines rarely stays at two. That growth curve is precisely what a parent node absorbs for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does a self hosted Netdata parent node cost over the same three years?
&lt;/h2&gt;

&lt;p&gt;A parent node is a normal Netdata agent that accepts streamed metrics from children. There is no licence, no node count and no billing period. The costs are real, they are just paid in hardware, watts and hours instead of invoices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware, if you need any:&lt;/strong&gt; if you already keep a NAS or a mini PC running all day, the marginal cost is close to zero and you skip this line entirely. If you buy a box for the job, divide the purchase price by 36 months to get the monthly figure you actually compare against a subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Electricity, which is the only truly recurring cost:&lt;/strong&gt; measure idle draw with a plug meter, then multiply watts by 8,760 hours per year, by three years, by your tariff per kWh. A parent aggregating a handful of children sits near idle most of the time, so the delta over a machine you already run is small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk for retention:&lt;/strong&gt; the parent stores every child's history in its own dbengine files, so its storage footprint is roughly the sum of the fleet. Sizing that properly is section seven's job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup hours, paid once:&lt;/strong&gt; generate an API key with &lt;code&gt;uuidgen&lt;/code&gt;, configure &lt;code&gt;stream.conf&lt;/code&gt; on the parent and on each child, open the streaming port between them, verify the children appear. Budget an evening for the first one and minutes for each additional child.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing maintenance:&lt;/strong&gt; package updates, an occasional config migration, and watching that children reconnect after reboots.&lt;/p&gt;

&lt;p&gt;That is the whole bill. Notice that node count appears nowhere in it, which is the structural difference from a per node subscription.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you size a parent node for a home server's metric volume?
&lt;/h2&gt;

&lt;p&gt;Do not guess the fleet total. Measure one child, then multiply. Query &lt;code&gt;http://localhost:19999/api/v1/info&lt;/code&gt; on a typical machine and read back how many charts and dimensions it actually collects. A quiet VPS and a Docker host running 20 containers differ by an order of magnitude, because every container, every network interface and every mounted filesystem adds dimensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU on the parent scales with ingested dimensions, not with node count:&lt;/strong&gt; four small children can be lighter than one busy container host. Streaming ingestion itself is cheap, since the parent is storing points rather than running collectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine learning is the biggest optional CPU cost:&lt;/strong&gt; the parent can train anomaly models for streamed children as well as itself. If the parent is a low power box, set &lt;code&gt;enabled = no&lt;/code&gt; under the &lt;code&gt;[ml]&lt;/code&gt; section of &lt;code&gt;netdata.conf&lt;/code&gt; on the parent and leave detection running on the children instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAM follows tier0 caching:&lt;/strong&gt; the dbengine holds recent pages in memory before flushing them to disk, so memory use tracks the total dimension count across the fleet. Give the parent headroom rather than trimming it to the exact working set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk wants sustained small writes, not raw capacity:&lt;/strong&gt; the dbengine flushes continuously. An SSD, even a modest one, behaves far better here than a spinning disk shared with media files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with two children, then watch the parent monitor itself:&lt;/strong&gt; Netdata ships a Netdata Monitoring section that charts its own CPU, memory and dbengine behaviour. Add the rest of the fleet only once that section looks flat.&lt;/p&gt;

&lt;p&gt;Size for your year three node count, then stop. Over provisioning a parent is the one place this setup starts costing subscription money.&lt;/p&gt;




&lt;h2&gt;
  
  
  How much bandwidth does streaming from child to parent actually use?
&lt;/h2&gt;

&lt;p&gt;The honest answer is that you must measure it, but you can predict its shape precisely. A child sends one value per dimension per collection interval, continuously, over a single persistent TCP connection. Bandwidth is therefore dimensions multiplied by frequency, and nothing else. A quiet node with few dimensions produces a trickle. A Docker host with hundreds of container dimensions at one second resolution produces a steady, measurable stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compression is on by default and does most of the work:&lt;/strong&gt; the streaming protocol compresses the connection, configured under the &lt;code&gt;[stream]&lt;/code&gt; section of &lt;code&gt;stream.conf&lt;/code&gt;. Leave it enabled. Metric streams are highly repetitive, so they compress well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collection frequency is the single biggest lever:&lt;/strong&gt; raising &lt;code&gt;update every&lt;/code&gt; from 1 to 2 in &lt;code&gt;netdata.conf&lt;/code&gt; on a child roughly halves what it sends. You lose per second granularity on that node, which for a backup NAS matters far less than for a database host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconnection triggers replication, not a silent gap:&lt;/strong&gt; when a child comes back after downtime, it backfills the missing window to the parent. Expect a burst well above the steady rate, proportional to how long the link was down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trimming collectors trims the stream permanently:&lt;/strong&gt; disabling collectors you never look at removes their dimensions from both the parent's disk and the wire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure it where it counts:&lt;/strong&gt; watch the network interface chart on the child, or run &lt;code&gt;iftop&lt;/code&gt; against the parent's address, and read the real number for your fleet.&lt;/p&gt;

&lt;p&gt;This only becomes a cost line if a child lives on a metered connection or a VPS with an egress allowance. On a LAN, it is free.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do the dbengine tiers change your retention bill on a parent node?
&lt;/h2&gt;

&lt;p&gt;Netdata does not store one copy of your history. The dbengine writes three tiers, each downsampling the one before it, and each with its own retention setting in the &lt;code&gt;[db]&lt;/code&gt; section of &lt;code&gt;netdata.conf&lt;/code&gt;. This is why a parent holding months of data for six children stays manageable. The arithmetic per dimension per day makes the point immediately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Default granularity&lt;/th&gt;
&lt;th&gt;Points per dimension per day, and what it buys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tier 0&lt;/td&gt;
&lt;td&gt;1 second&lt;/td&gt;
&lt;td&gt;86,400 points: the forensic detail you want for the last incident, and the overwhelming majority of your disk usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 1&lt;/td&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;td&gt;1,440 points: sixty times cheaper, still fine for spotting a memory leak developing over a week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 2&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;td&gt;24 points: effectively free, and the right resolution for capacity planning across months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All three combined&lt;/td&gt;
&lt;td&gt;Mixed&lt;/td&gt;
&lt;td&gt;The parent serves whichever tier matches the zoom level of the chart you are viewing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tuning move is obvious once you see the ratio. Shorten tier 0 retention and lengthen tiers 1 and 2. Keeping seven days of per second data plus a year of hourly data costs a fraction of keeping per second data for a year, and you will almost never zoom to one second resolution on something that happened in March.&lt;/p&gt;

&lt;p&gt;Set retention per tier by size, by time, or by both, and the dbengine enforces whichever limit it hits first. Setting a size cap is the safer habit on a home server, because it guarantees Netdata cannot fill the disk that everything else depends on.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does bolting Prometheus and Grafana onto Netdata cost you in hours and hardware?
&lt;/h2&gt;

&lt;p&gt;Netdata exposes &lt;code&gt;/api/v1/allmetrics?format=prometheus&lt;/code&gt; on port 19999, so wiring it into Prometheus is a scrape target in &lt;code&gt;prometheus.yml&lt;/code&gt; and nothing more. The integration is easy. The stack around it is what costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You now run three services instead of one:&lt;/strong&gt; Prometheus, Grafana, and in practice Alertmanager, since Grafana alerting or Alertmanager is what turns a query into a notification. Each has its own config, its own upgrade cadence and its own breaking changes to read about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You store everything twice:&lt;/strong&gt; the agent keeps its dbengine tiers and Prometheus keeps its own TSDB, on the same disk if this is one home server. Prometheus defaults to 15 days retention, so budget capacity for both databases rather than assuming the scrape replaces local storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose per second resolution unless you fight for it:&lt;/strong&gt; Prometheus commonly runs a 15 second &lt;code&gt;scrape_interval&lt;/code&gt;. Dropping that to 1 second across a Netdata export multiplies series volume hard, which is exactly the cardinality problem the tiered dbengine was designed to avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You rebuild dashboards by hand:&lt;/strong&gt; Netdata auto generates charts for every collector it finds. Grafana gives you a blank canvas. That is a genuine advantage if you want a curated view, and a genuine cost measured in evenings if you just wanted the charts you already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You rewrite alerts in a second language:&lt;/strong&gt; health entities on the agent become PromQL rules plus routing config, maintained separately.&lt;/p&gt;

&lt;p&gt;The verdict is conditional. If Prometheus already runs for other services, exporting Netdata into it is sensible and nearly free. If it does not, this is the most expensive of the three options and the only one where the recurring cost is your attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which setup gives you alerts that actually reach you, and what do notifications cost?
&lt;/h2&gt;

&lt;p&gt;Every option evaluates alarms the same way, because health entities run on the agent regardless of what sits above it. The difference is what happens when the alerting machine is the machine that failed. A single agent watching only itself cannot tell you it lost power. That blind spot, not feature lists, should drive this decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lone agent covers threshold alerts and nothing else:&lt;/strong&gt; it fires on high disk usage or a stopped service, and it goes silent on the failure mode you most want to hear about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A parent node closes the gap for children:&lt;/strong&gt; it evaluates alarms on streamed data and notices when a child stops reporting, so a dead VPS produces an alert instead of silence. You configure delivery once on the parent rather than on every node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netdata Cloud closes the same gap from outside your network:&lt;/strong&gt; the outbound connection means Cloud sees a node go unreachable even if your whole house loses power, which a parent sitting in that house cannot do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free delivery channels do most of the work:&lt;/strong&gt; Discord, Slack, Telegram and ntfy all accept webhooks directly from the agent, cost nothing, and reach your phone. Reload after editing with &lt;code&gt;netdatacli reload-health&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paid channels are the real notification cost:&lt;/strong&gt; SMS through a provider bills per message, and a PagerDuty seat can exceed what you spend on monitoring itself.&lt;/p&gt;

&lt;p&gt;The pragmatic setup for one person is two independent paths. Let Netdata alert on thresholds, then run a separate reachability check from somewhere else entirely, a free tier uptime service or an Uptime Kuma instance on different hardware. Redundant alerting costs nothing and catches the failure a single system structurally cannot.&lt;/p&gt;

</description>
      <category>netdata</category>
      <category>monitoring</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>Navidrome duplicate albums, split discs and missing art: the tagging and mount anti-patterns that break a 50k library</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:19:38 +0000</pubDate>
      <link>https://dev.to/john_182319291/navidrome-duplicate-albums-split-discs-and-missing-art-the-tagging-and-mount-anti-patterns-that-3ige</link>
      <guid>https://dev.to/john_182319291/navidrome-duplicate-albums-split-discs-and-missing-art-the-tagging-and-mount-anti-patterns-that-3ige</guid>
      <description>&lt;p&gt;Navidrome is not guessing at your library, and it is not corrupting it. It builds albums from the tags inside your files, then keys every album and track to a persistent ID derived from those tags, so a duplicate album means two groups of files disagree about ALBUMARTIST, album name, release date or MusicBrainz ID. Split discs, "Various Artists" fragmentation and missing covers come from the same root cause, one layer down: inconsistent tags, or a filesystem that reports different modification times and paths on every mount. Fix the tags and stabilise the mount, and the scanner stops being the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo developer with a 50,000 track library on a home NAS, mounted over SMB:&lt;/strong&gt; move the music onto a local disk or a stable NFS export before touching tags, because mtime drift makes every scan a full scan and hides whether your tag fixes worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone whose library came from mixed sources over ten years, ripped CDs plus purchased downloads plus old downloads:&lt;/strong&gt; run MusicBrainz Picard over the worst offenders first, since missing ALBUMARTIST and inconsistent album titles cause the large majority of duplicate album reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beets user who already has a tidy, scripted library:&lt;/strong&gt; keep beets as the source of truth and point Navidrome at its output read only, because two tools writing tags to the same files is how you get half-updated albums.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose user running Navidrome next to Jellyfin, Plex or Lidarr on the same folder:&lt;/strong&gt; decide which application owns tag writes and cover files, since competing artwork conventions are the usual reason embedded art stops showing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone running from a laptop, an external USB drive or a rclone mount:&lt;/strong&gt; disable the watcher and use a scheduled scan, because filesystem notification events do not cross most network and FUSE mounts reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone about to "just delete the database and rescan":&lt;/strong&gt; export playlists and back up navidrome.db first, because a rebuild recovers metadata but not play counts, ratings or starred items unless the identity tags are stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff is this: strict, MusicBrainz backed tags give you a library that survives rescans and server moves, but they cost hours of upfront cleanup that a loose folder based collection never asks for until the day it breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why does Navidrome show the same album twice after a scan?&lt;/li&gt;
&lt;li&gt;How Navidrome decides what an album is: persistent IDs and the grouping tags&lt;/li&gt;
&lt;li&gt;Why missing ALBUMARTIST breaks more than any other single tag&lt;/li&gt;
&lt;li&gt;Do you need MusicBrainz IDs in Navidrome, and what actually breaks without them?&lt;/li&gt;
&lt;li&gt;Why does one album split into CD1 and CD2 in the interface?&lt;/li&gt;
&lt;li&gt;Compilations, Various Artists and soundtracks: four ways they fragment&lt;/li&gt;
&lt;li&gt;Why is cover art missing when the file clearly has an embedded image?&lt;/li&gt;
&lt;li&gt;Why does Navidrome rescan the whole library on an SMB or NFS mount?&lt;/li&gt;
&lt;li&gt;Symlinks, bind mounts and container paths: the anti-patterns that hide half a library&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why does Navidrome show the same album twice after a scan?
&lt;/h2&gt;

&lt;p&gt;Because two sets of files disagree about at least one grouping tag, and Navidrome trusts the tags rather than the folder. It never merges albums by directory name. If ten tracks say &lt;code&gt;ALBUMARTIST=Radiohead&lt;/code&gt; and the other two say &lt;code&gt;Radiohead&lt;/code&gt; with a trailing space, you get two albums, both named the same, sitting side by side in the interface.&lt;/p&gt;

&lt;p&gt;The disagreements that produce duplicates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Album title variants:&lt;/strong&gt; one track tagged &lt;code&gt;OK Computer&lt;/code&gt; and another &lt;code&gt;OK Computer (Remastered)&lt;/code&gt; are two different releases as far as the scanner is concerned, and the interface offers no visual hint that only the suffix differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial ALBUMARTIST coverage:&lt;/strong&gt; if a tag editor wrote &lt;code&gt;ALBUMARTIST&lt;/code&gt; on eight of twelve tracks, the untagged four fall back to their own &lt;code&gt;ARTIST&lt;/code&gt; value, so a guest vocalist on track 9 becomes an album of one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed MusicBrainz IDs:&lt;/strong&gt; files carrying &lt;code&gt;MUSICBRAINZ_ALBUMID&lt;/code&gt; are keyed by that ID, while files without it are keyed by artist plus title plus release data, so a half tagged album cannot group with itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release date and version drift:&lt;/strong&gt; a differing &lt;code&gt;DATE&lt;/code&gt;, &lt;code&gt;ORIGINALDATE&lt;/code&gt; or release version field separates two otherwise identical halves of the same rip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible characters:&lt;/strong&gt; trailing whitespace, curly versus straight apostrophes and non breaking spaces compare as different strings, which is why the two entries look identical on screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check one before guessing. Run &lt;code&gt;ffprobe -show_entries format_tags -v quiet yourfile.flac&lt;/code&gt; on a track from each duplicate and compare the output line by line. In most cases the answer is visible within 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Navidrome decides what an album is: persistent IDs and the grouping tags
&lt;/h2&gt;

&lt;p&gt;Every album and every track gets a persistent ID, computed by hashing a configurable expression made of tag values. Two files that produce the same album PID are the same album. Two files that do not, are not. That is the whole rule, and once you can read the expression, duplicate reports stop being mysterious.&lt;/p&gt;

&lt;p&gt;The mechanics you need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The expression lives in configuration:&lt;/strong&gt; &lt;code&gt;PID.Album&lt;/code&gt; and &lt;code&gt;PID.Track&lt;/code&gt; in &lt;code&gt;navidrome.toml&lt;/code&gt;, or &lt;code&gt;ND_PID_ALBUM&lt;/code&gt; and &lt;code&gt;ND_PID_TRACK&lt;/code&gt; as environment variables in your Compose file, so you can inspect and change the grouping rule without touching a single audio file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A comma concatenates, a pipe falls back:&lt;/strong&gt; &lt;code&gt;musicbrainz_albumid|albumartistid,album,albumversion,releasedate&lt;/code&gt; means "use the MusicBrainz album ID if present, otherwise build a key from album artist, album title, version and release date".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first non empty field wins:&lt;/strong&gt; this is why a library with MusicBrainz IDs on 60 percent of files behaves as two libraries under one roof, with two different keying strategies running in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track PIDs hang off album PIDs:&lt;/strong&gt; the default track expression falls back to the album ID plus disc number, track number and title, so an unstable album PID makes every track ID unstable too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing the expression is not free:&lt;/strong&gt; new PIDs mean new rows, and play counts, ratings and starred flags are attached to the old IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical consequence: prefer fixing tags over rewriting &lt;code&gt;ND_PID_ALBUM&lt;/code&gt;. Loosening the expression to hide a duplicate also merges genuinely distinct releases, such as a 1997 pressing and its 2017 remaster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why missing ALBUMARTIST breaks more than any other single tag
&lt;/h2&gt;

&lt;p&gt;Because it is the only tag that tells Navidrome who owns the album, and when it is absent the scanner falls back to the per track &lt;code&gt;ARTIST&lt;/code&gt; value. On a solo record that fallback is harmless. On anything with a featured guest, a duet, a remixer or an orchestra credit, it shatters one album into several one track albums, each filed under a different artist in the sidebar.&lt;/p&gt;

&lt;p&gt;The tag has a different name in every container, which is exactly why editors write it inconsistently: &lt;code&gt;TPE2&lt;/code&gt; in ID3v2 for MP3, &lt;code&gt;ALBUMARTIST&lt;/code&gt; as a Vorbis comment in FLAC and Ogg, and &lt;code&gt;aART&lt;/code&gt; in MP4 for AAC and ALAC files.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File situation&lt;/th&gt;
&lt;th&gt;What Navidrome shows&lt;/th&gt;
&lt;th&gt;Underlying reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ALBUMARTIST on all 12 tracks&lt;/td&gt;
&lt;td&gt;1 album, 12 tracks&lt;/td&gt;
&lt;td&gt;Every file produces the same album PID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALBUMARTIST on 10 of 12 tracks&lt;/td&gt;
&lt;td&gt;1 album plus 2 strays&lt;/td&gt;
&lt;td&gt;The 2 untagged files key off their own ARTIST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ALBUMARTIST, 4 guest features&lt;/td&gt;
&lt;td&gt;5 albums, same cover&lt;/td&gt;
&lt;td&gt;Each distinct ARTIST value becomes its own key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No ALBUMARTIST on an 18 track compilation&lt;/td&gt;
&lt;td&gt;Up to 18 albums&lt;/td&gt;
&lt;td&gt;Every track has a different performer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALBUMARTIST with a trailing space on 1 file&lt;/td&gt;
&lt;td&gt;2 albums, identical names&lt;/td&gt;
&lt;td&gt;String comparison is exact, whitespace included&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fix it in bulk rather than by hand. In MusicBrainz Picard, matched releases write album artist automatically. In Kid3 or Mp3tag, select a folder and set the field once for every file in it. The rule that survives contact with a real library: &lt;code&gt;ARTIST&lt;/code&gt; describes the performance, &lt;code&gt;ALBUMARTIST&lt;/code&gt; describes the shelf it sits on, and it must be identical across every file in the album.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do you need MusicBrainz IDs in Navidrome, and what actually breaks without them?
&lt;/h2&gt;

&lt;p&gt;No. A library with clean, consistent text tags works fine. What you cannot do is mix the two approaches inside one album, because the pipe fallback in the album PID expression means tagged files key off a 36 character UUID and untagged files key off artist plus title plus date. Half a release with &lt;code&gt;MUSICBRAINZ_ALBUMID&lt;/code&gt; is the single most reliable way to manufacture a duplicate that no amount of retyping the album name will fix.&lt;/p&gt;

&lt;p&gt;What the IDs buy you once coverage is complete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immunity to text edits:&lt;/strong&gt; rename an album from &lt;code&gt;Vol. 1&lt;/code&gt; to &lt;code&gt;Volume 1&lt;/code&gt; and the grouping survives, because the key is the UUID and not the string you just changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist disambiguation:&lt;/strong&gt; three different bands called Nirvana get three &lt;code&gt;MUSICBRAINZ_ARTISTID&lt;/code&gt; values, so they stop collapsing into one crowded artist page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct external lookups:&lt;/strong&gt; artist images, biographies and ListenBrainz submissions resolve against a canonical entity rather than a fuzzy name match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable re tagging later:&lt;/strong&gt; with IDs embedded, &lt;code&gt;beet mbsync&lt;/code&gt; or a Picard reload updates metadata from upstream without you re matching anything by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stable identity across servers:&lt;/strong&gt; move the library to a new machine, rebuild the database, and the albums come back grouped the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost is real. Matching a large collection in Picard is manual work on anything ambiguous: bootlegs, live recordings, regional pressings and unofficial releases often have several candidate releases, or none. A pragmatic split is to tag the 20 percent of albums you actually play, leave the rest on text tags, and never mix within one album folder.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does one album split into CD1 and CD2 in the interface?
&lt;/h2&gt;

&lt;p&gt;The folder layout is not the problem. Navidrome will happily assemble a 2 disc, 24 track release from &lt;code&gt;Album/CD1&lt;/code&gt; and &lt;code&gt;Album/CD2&lt;/code&gt;, because it groups by tags and ignores directory structure. The split happens when the ripper baked the disc number into the album title, or left the disc tag empty.&lt;/p&gt;

&lt;p&gt;The four causes, in the order you should check them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disc suffixes inside the ALBUM tag:&lt;/strong&gt; files reading &lt;code&gt;The Wall (Disc 1)&lt;/code&gt; and &lt;code&gt;The Wall (Disc 2)&lt;/code&gt; produce two different album keys, and the sidebar shows two covers with almost identical names. Strip the suffix so both discs carry the exact string &lt;code&gt;The Wall&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing DISCNUMBER:&lt;/strong&gt; the tag is &lt;code&gt;DISCNUMBER&lt;/code&gt; in FLAC and Ogg, &lt;code&gt;TPOS&lt;/code&gt; in ID3v2 and the &lt;code&gt;disk&lt;/code&gt; atom in MP4. Without it, disc 2 track 1 and disc 1 track 1 both claim position 1, so the album lists two tracks numbered 1 and the play order goes wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release version or date drift between discs:&lt;/strong&gt; if one folder was tagged in a later session with a different &lt;code&gt;DATE&lt;/code&gt; or release version, the grouping expression sees two releases even though the album titles match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per disc MusicBrainz IDs:&lt;/strong&gt; some releases are modelled as separate media within one release, and a mismatched &lt;code&gt;MUSICBRAINZ_ALBUMID&lt;/code&gt; on one folder keeps the halves apart permanently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bulk fix is a regular expression action in Mp3tag or Kid3 over the parent folder: move the trailing &lt;code&gt;(Disc N)&lt;/code&gt; or &lt;code&gt;CDN&lt;/code&gt; out of the album field and into the disc number field. Then set &lt;code&gt;DISCTOTAL&lt;/code&gt; to 2 on all 24 files so the interface labels the discs correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compilations, Various Artists and soundtracks: four ways they fragment
&lt;/h2&gt;

&lt;p&gt;A 40 track compilation is the worst case for a tag driven scanner: every track has a different performer, so anything less than perfectly consistent album level tags produces dozens of one track albums. Four distinct failures account for nearly all of it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The compilation flag is missing:&lt;/strong&gt; the tag is &lt;code&gt;COMPILATION=1&lt;/code&gt; as a Vorbis comment, &lt;code&gt;TCMP&lt;/code&gt; in ID3v2 and the &lt;code&gt;cpil&lt;/code&gt; atom in MP4. Set it on every file of the release. Without it, nothing marks the album as a multi artist release, and the scanner has only the per track artist to work from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Various Artists is spelled four different ways:&lt;/strong&gt; &lt;code&gt;Various&lt;/code&gt;, &lt;code&gt;Various Artists&lt;/code&gt;, &lt;code&gt;VA&lt;/code&gt; and &lt;code&gt;V/A&lt;/code&gt; are four separate album artists and therefore four separate albums. Pick the exact string &lt;code&gt;Various Artists&lt;/code&gt;, which is what MusicBrainz Picard writes, and apply it to all 40 files without exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per track ALBUMARTIST was written by a helpful editor:&lt;/strong&gt; some tools copy &lt;code&gt;ARTIST&lt;/code&gt; into &lt;code&gt;ALBUMARTIST&lt;/code&gt; when the field is blank, so track 7 becomes an album by that one performer. This is silent, and it happens to files you never opened in the editor yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soundtracks credited to the composer on some tracks only:&lt;/strong&gt; a film score with 6 licensed songs mixed into 30 cues ends up half credited to the composer and half to the featured bands, splitting one release into two or more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify the whole folder in one pass rather than spot checking. Run &lt;code&gt;ffprobe -show_entries format_tags -v quiet&lt;/code&gt; over each file and confirm that album artist, album title and compilation flag are byte identical across all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why is cover art missing when the file clearly has an embedded image?
&lt;/h2&gt;

&lt;p&gt;Because Navidrome looks for artwork in a configured order, and stops at the first hit. That order is set by &lt;code&gt;CoverArtPriority&lt;/code&gt;, or &lt;code&gt;ND_COVERARTPRIORITY&lt;/code&gt; in your Compose file, and the default puts embedded art first, then &lt;code&gt;cover.*&lt;/code&gt;, &lt;code&gt;folder.*&lt;/code&gt; and &lt;code&gt;front.*&lt;/code&gt; files in the album directory. If a stale, low resolution &lt;code&gt;folder.jpg&lt;/code&gt; outranks your new embedded image under a modified priority string, the old picture wins and nothing looks broken in the logs.&lt;/p&gt;

&lt;p&gt;The causes worth checking, in order of how often they turn out to be the real one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cache folder is not writable:&lt;/strong&gt; Navidrome resizes artwork and stores it under the cache directory. If the container runs as UID 1000 and the volume is owned by root, extraction silently produces nothing. Run &lt;code&gt;ls -ln&lt;/code&gt; on the cache path and confirm the owner matches the user in your Compose file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Art is embedded in one track only:&lt;/strong&gt; the album cover comes from the files themselves, so tagging track 1 and leaving the other 11 bare gives inconsistent results in track level views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filename case and spelling:&lt;/strong&gt; &lt;code&gt;Folder.jpg&lt;/code&gt;, &lt;code&gt;AlbumArt.jpg&lt;/code&gt; and &lt;code&gt;cover.jpeg&lt;/code&gt; are not the same as the patterns in your priority list. Standardise on lowercase &lt;code&gt;cover.jpg&lt;/code&gt; in every album directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The image is not a format the extractor handles:&lt;/strong&gt; WebP and progressive or CMYK JPEG files embedded by a third party tool are common failures. Re embed as a baseline JPEG or PNG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser cached the placeholder:&lt;/strong&gt; after fixing tags, hard reload the page before concluding the fix failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change one variable at a time, then rescan a single album folder rather than the whole library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does Navidrome rescan the whole library on an SMB or NFS mount?
&lt;/h2&gt;

&lt;p&gt;A quick scan compares each directory's modification time against the timestamp of the last scan. Anything newer gets re read. Network filesystems break that comparison in two ways: they report modification times with different precision or a different timezone offset than the local disk, and they can change the reported value on remount. When every folder looks newer than the last scan, every folder is rescanned, and a 50,000 track library never gets a fast pass.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mount type&lt;/th&gt;
&lt;th&gt;mtime stability across remounts&lt;/th&gt;
&lt;th&gt;Practical consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local ext4 or XFS&lt;/td&gt;
&lt;td&gt;Stable to the nanosecond&lt;/td&gt;
&lt;td&gt;Quick scans stay quick, watcher works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFSv4 with synced clocks&lt;/td&gt;
&lt;td&gt;Stable if server and client run NTP&lt;/td&gt;
&lt;td&gt;Reliable, watcher events unreliable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMB3 via cifs&lt;/td&gt;
&lt;td&gt;Depends on mount options and server&lt;/td&gt;
&lt;td&gt;Frequent full rescans, timestamp drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rclone or other FUSE mount&lt;/td&gt;
&lt;td&gt;Often synthesised or coarse&lt;/td&gt;
&lt;td&gt;Repeated full rescans, high latency per file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Diagnose it before changing anything. Run &lt;code&gt;stat -c '%y %n' /music/SomeAlbum&lt;/code&gt; from inside the container, unmount and remount the share, then run the identical command again. If the two values differ, the mount is the cause and no amount of tag editing will help.&lt;/p&gt;

&lt;p&gt;The fixes that hold, in order of preference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move the library to a local disk:&lt;/strong&gt; the only option that removes the class of problem entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the SMB mount options:&lt;/strong&gt; set &lt;code&gt;vers=3.0&lt;/code&gt; and keep &lt;code&gt;noserverino&lt;/code&gt; out of your fstab line, since it also destabilises inode numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run NTP on both ends:&lt;/strong&gt; a clock skew of a few seconds, or a 3600 second daylight saving shift, is enough to invalidate the comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule scans instead of fighting the watcher:&lt;/strong&gt; covered next, but the mount is the thing to fix first.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Symlinks, bind mounts and container paths: the anti-patterns that hide half a library
&lt;/h2&gt;

&lt;p&gt;Navidrome can only index what it can see and read from inside its own namespace. In Docker that namespace is whatever you bind mounted, so a path that works perfectly in your host shell can be empty, dangling or unreadable to the process doing the scanning. The symptom is not an error. It is an album count that is 8,000 tracks short of what you expected.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symlinks that resolve outside the bind mount:&lt;/strong&gt; if &lt;code&gt;/mnt/music/rock&lt;/code&gt; is a link to &lt;code&gt;/mnt/tank/rock&lt;/code&gt; on the host and you only mounted &lt;code&gt;/mnt/music&lt;/code&gt;, the container sees a broken link. Mount the real target as well, or replace the symlink with a second bind mount in your Compose file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directories the container user cannot traverse:&lt;/strong&gt; a folder with mode 700 owned by root is invisible to a process running as UID 1000, and the scan skips it without stopping. Verify with &lt;code&gt;docker exec -u 1000 navidrome ls /music/Artist&lt;/code&gt; rather than trusting your own shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data folder living inside the music folder:&lt;/strong&gt; if &lt;code&gt;navidrome.db&lt;/code&gt; and the cache sit under the scanned tree, the scanner walks its own working files. Keep music at &lt;code&gt;/music&lt;/code&gt; and state at &lt;code&gt;/data&lt;/code&gt;, as two separate volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renaming the mount point between deployments:&lt;/strong&gt; switching from &lt;code&gt;/music&lt;/code&gt; to &lt;code&gt;/media/music&lt;/code&gt; changes every stored path, so the next scan re imports the entire library even though not one audio file changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Folders you never wanted indexed:&lt;/strong&gt; drop a &lt;code&gt;.ndignore&lt;/code&gt; file into a directory to exclude it and its children, which is the clean way to skip incoming downloads or a lossless archive.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>navidrome</category>
      <category>selfhosted</category>
      <category>music</category>
      <category>docker</category>
    </item>
    <item>
      <title>WordPress Sitewide Outages: Which Misconfigurations Actually Cause Them, Ranked by Frequency and Recovery Time</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 04:35:34 +0000</pubDate>
      <link>https://dev.to/john_182319291/wordpress-sitewide-outages-which-misconfigurations-actually-cause-them-ranked-by-frequency-and-5d79</link>
      <guid>https://dev.to/john_182319291/wordpress-sitewide-outages-which-misconfigurations-actually-cause-them-ranked-by-frequency-and-5d79</guid>
      <description>&lt;p&gt;Most WordPress outages are not attacks. Four causes account for the overwhelming majority: a plugin or theme update that throws a PHP fatal error, a PHP resource limit set below what the installed plugin set needs, a database that stops answering because MySQL hit a limit or the disk filled, and a wp-config or site URL value edited to something the server cannot serve from. Each has a different recovery path, and the fastest one is almost never wp-admin, because wp-admin is usually down with the rest of the site. Rank them by mean recovery time instead of frequency and the ordering inverts: the most common failure is the cheapest to undo, while the rarest, InnoDB corruption on a full disk, is the one that ends in a restore from backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Family running one shared site (Marta hosts the household blog, a shared photo gallery and a recipe page on a small VPS):&lt;/strong&gt; put &lt;code&gt;DISALLOW_FILE_EDIT&lt;/code&gt; and automatic plugin updates under manual control, and keep WP-CLI installed, because the single most likely outage is a well meaning family member updating a plugin from a phone and having no way back in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volunteer admin for a community group (Ade maintains the sports club site nobody else touches):&lt;/strong&gt; budget for a documented restore drill twice a year rather than more monitoring, because your real risk is not detection speed, it is discovering the backup was never tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freelancer hosting several client installs on one box (Jonas runs six WordPress sites behind one nginx instance):&lt;/strong&gt; set per site PHP-FPM pool limits and separate MySQL users, because a single runaway site otherwise takes the other five down with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small store owner on WooCommerce (Priya sells stock she has to reconcile daily):&lt;/strong&gt; raise &lt;code&gt;WP_MEMORY_LIMIT&lt;/code&gt;, move cron off WP-Cron and monitor MySQL connections, because commerce plugin sets fail on resource ceilings long before content sites do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homelab self-hoster running WordPress in Docker (Tomas keeps the whole stack in one compose file):&lt;/strong&gt; treat volume mapping and file ownership as the primary failure surface, because container rebuilds are what silently discard uploads and reset permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is blunt: every guard that stops a bad change from taking the site down, staging environments, file edit locks, pinned plugin versions and manual updates, costs you friction on every ordinary edit you make.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Which WordPress misconfigurations actually cause a full sitewide outage?&lt;/li&gt;
&lt;li&gt;The main failure modes ranked by frequency, blast radius and mean recovery time&lt;/li&gt;
&lt;li&gt;Why does a plugin or theme update end in a white screen of death?&lt;/li&gt;
&lt;li&gt;PHP memory_limit, max_execution_time and OPcache: the settings that fail first&lt;/li&gt;
&lt;li&gt;What does "Error establishing a database connection" mean in each of its cases?&lt;/li&gt;
&lt;li&gt;MySQL connection limits, InnoDB corruption and the full disk that causes both&lt;/li&gt;
&lt;li&gt;How does a wrong WP_HOME or WP_SITEURL value lock you out of wp-admin?&lt;/li&gt;
&lt;li&gt;Permalinks, .htaccess and nginx rewrites: the disagreement that 404s every page&lt;/li&gt;
&lt;li&gt;Do file ownership and permission mistakes really take a site offline?&lt;/li&gt;
&lt;li&gt;WP-Cron misconfiguration: the silent damage that becomes a visible outage&lt;/li&gt;
&lt;li&gt;How do caching layers and a misconfigured object cache serve blank or stale pages?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Which WordPress misconfigurations actually cause a full sitewide outage?
&lt;/h2&gt;

&lt;p&gt;A sitewide outage means every visitor sees an error page and you cannot reach &lt;code&gt;/wp-admin&lt;/code&gt; either. That narrow definition rules out most of what people worry about. Slow queries, a broken contact form and a mangled theme layout are all bad, but they leave the site reachable. Only a short list of misconfigurations take the whole thing down at once.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP fatal errors from plugin or theme code:&lt;/strong&gt; an activated plugin calling a function that no longer exists halts execution before any output, so both the front end and the admin die in the same request. This is the single most common cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource ceilings set below what the site needs:&lt;/strong&gt; a PHP &lt;code&gt;memory_limit&lt;/code&gt; of 128M against a plugin set that needs more, or a &lt;code&gt;max_execution_time&lt;/code&gt; of 30 seconds against an import job, turns a normal page load into a fatal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database refusing connections:&lt;/strong&gt; wrong credentials in &lt;code&gt;wp-config.php&lt;/code&gt;, MySQL exceeding &lt;code&gt;max_connections&lt;/code&gt;, or the disk hitting 100 percent all produce the same "Error establishing a database connection" page with no admin access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong site address values:&lt;/strong&gt; &lt;code&gt;WP_HOME&lt;/code&gt; and &lt;code&gt;WP_SITEURL&lt;/code&gt; pointing at a hostname or scheme the server does not answer on redirects every request into a loop, including the login page you need to fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web server rewrite rules that stop matching:&lt;/strong&gt; a deleted &lt;code&gt;.htaccess&lt;/code&gt; on Apache or a missing &lt;code&gt;try_files&lt;/code&gt; directive on nginx returns 404 for every URL except the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File ownership drift after a rebuild or restore:&lt;/strong&gt; files owned by &lt;code&gt;root&lt;/code&gt; instead of &lt;code&gt;www-data&lt;/code&gt; make PHP unable to read what it must include.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else degrades the site. These six stop it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The main failure modes ranked by frequency, blast radius and mean recovery time
&lt;/h2&gt;

&lt;p&gt;Rank the six causes and a pattern appears: frequency and recovery cost run in opposite directions. What breaks constantly is what you can undo with one command. What almost never breaks is what forces a restore.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Frequency and blast radius&lt;/th&gt;
&lt;th&gt;What recovery actually costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin or theme fatal error&lt;/td&gt;
&lt;td&gt;Most frequent. Blast radius is total but confined to one install, and the trigger is always a change you made&lt;/td&gt;
&lt;td&gt;Lowest. &lt;code&gt;wp plugin deactivate --all&lt;/code&gt; over SSH, or renaming the folder in &lt;code&gt;wp-content/plugins&lt;/code&gt;, restores the site in the time it takes to log in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP resource ceiling hit&lt;/td&gt;
&lt;td&gt;Frequent on plugin heavy sites, intermittent rather than constant, so it looks like a slowness problem first&lt;/td&gt;
&lt;td&gt;Low once diagnosed, but diagnosis dominates. Raising &lt;code&gt;WP_MEMORY_LIMIT&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; is one line, finding the right line takes the log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Site URL or redirect loop&lt;/td&gt;
&lt;td&gt;Occasional, usually right after a domain, HTTPS or migration change. Blast radius includes the login page&lt;/td&gt;
&lt;td&gt;Low with shell access, high without. &lt;code&gt;wp option update home&lt;/code&gt; fixes it, the trap is having no way in except the admin you just broke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rewrite rules stopped matching&lt;/td&gt;
&lt;td&gt;Occasional, often after a server config change rather than a WordPress change&lt;/td&gt;
&lt;td&gt;Low. Regenerate with &lt;code&gt;wp rewrite flush&lt;/code&gt; or restore the &lt;code&gt;.htaccess&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File ownership or permission drift&lt;/td&gt;
&lt;td&gt;Occasional, spikes after container rebuilds and restores&lt;/td&gt;
&lt;td&gt;Medium. One &lt;code&gt;chown -R&lt;/code&gt; command, but the wrong one breaks it further&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database unreachable or corrupt&lt;/td&gt;
&lt;td&gt;Rarest by a wide margin&lt;/td&gt;
&lt;td&gt;Highest by a wide margin. Credentials are minutes, a corrupt InnoDB tablespace means restoring from backup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the asymmetry: the modes you meet weekly are cheap, the one you meet yearly is the one your backups exist for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does a plugin or theme update end in a white screen of death?
&lt;/h2&gt;

&lt;p&gt;A white screen is not an error state of its own. It is a PHP fatal error with &lt;code&gt;display_errors&lt;/code&gt; set to &lt;code&gt;Off&lt;/code&gt;, which is the correct production setting. PHP stops mid request, sends nothing, and the browser renders the empty response as a blank page. The information you need exists, it is just in the log rather than on screen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A PHP version the new code does not support:&lt;/strong&gt; an update written against PHP 8.2 running on PHP 7.4 hits syntax it cannot parse, and a parse error kills the request before any plugin loads. This is why an update can break a site that was untouched for a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two plugins declaring the same function or class:&lt;/strong&gt; PHP throws a fatal on redeclaration. The plugin you updated is often not the one at fault, it just loaded second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A removed hook or function the theme still calls:&lt;/strong&gt; the theme calls something the updated plugin deleted, so the front end dies while &lt;code&gt;/wp-admin&lt;/code&gt; sometimes survives, because admin loads a different template path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The update itself interrupted mid write:&lt;/strong&gt; a PHP timeout during the unzip step leaves half the new files and half the old ones in &lt;code&gt;wp-content/plugins&lt;/code&gt;, a state no version of the plugin was ever tested in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory exhausted during the update routine:&lt;/strong&gt; database migration code in the plugin activation hook is far heavier than a normal page load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turn on &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; and read &lt;code&gt;wp-content/debug.log&lt;/code&gt;. WordPress 5.2 and later also email the address in &lt;code&gt;admin_email&lt;/code&gt; a recovery mode link that loads the admin with the offending plugin paused.&lt;/p&gt;




&lt;h2&gt;
  
  
  PHP memory_limit, max_execution_time and OPcache: the settings that fail first
&lt;/h2&gt;

&lt;p&gt;WordPress core is light. Your plugin set is not. Resource ceilings fail in a specific order, and knowing that order saves you from raising the wrong one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_limit&lt;/code&gt; in php.ini:&lt;/strong&gt; the production default shipped with PHP is 128M. A content site never approaches it. WooCommerce, a page builder and a backup plugin loaded together on the same request can, and the result is &lt;code&gt;Allowed memory size of X bytes exhausted&lt;/code&gt; in the error log rather than a useful message on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WP_MEMORY_LIMIT&lt;/code&gt; in wp-config.php:&lt;/strong&gt; WordPress defaults this to 40M for single site and 64M for multisite, and raises it to &lt;code&gt;WP_MAX_MEMORY_LIMIT&lt;/code&gt;, 256M by default, only inside the admin. It can raise the limit toward the php.ini value, it cannot exceed a hard limit set by your host, so setting it to 512M on a 128M box changes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_execution_time&lt;/code&gt;:&lt;/strong&gt; 30 seconds by default under PHP-FPM. Imports, backup archiving and image regeneration all exceed it on real datasets, and the request dies halfway through, leaving partial data behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;upload_max_filesize&lt;/code&gt; and &lt;code&gt;post_max_size&lt;/code&gt;:&lt;/strong&gt; 2M and 8M by default. These do not take the site down, they silently break media uploads and theme installs, which people misdiagnose as a permissions fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OPcache serving stale bytecode:&lt;/strong&gt; with &lt;code&gt;opcache.validate_timestamps&lt;/code&gt; on, PHP rechecks files every &lt;code&gt;opcache.revalidate_freq&lt;/code&gt; seconds, 2 by default. Turn validation off for performance and a deploy leaves old cached files mixed with new ones on disk, which produces fatals until you run &lt;code&gt;opcache_reset()&lt;/code&gt; or restart PHP-FPM.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What does "Error establishing a database connection" mean in each of its cases?
&lt;/h2&gt;

&lt;p&gt;WordPress prints one message for at least four unrelated faults. The message tells you nothing. The MySQL error code behind it tells you everything, and you get it by connecting manually from the same host: &lt;code&gt;mysql -u wp_user -p -h 127.0.0.1 wp_db&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error 1045, access denied:&lt;/strong&gt; the credentials in &lt;code&gt;wp-config.php&lt;/code&gt; no longer match the grant in MySQL. Common after restoring a database dump onto a new server, where the tables came over but the user did not. Recreate the grant, do not edit the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error 2002, cannot connect through socket:&lt;/strong&gt; &lt;code&gt;DB_HOST&lt;/code&gt; is set to &lt;code&gt;localhost&lt;/code&gt;, which makes the MySQL client use a Unix socket rather than TCP. If MySQL listens on TCP only, or lives in another container, this fails while &lt;code&gt;127.0.0.1&lt;/code&gt; or the container name works. In Docker Compose, &lt;code&gt;DB_HOST&lt;/code&gt; must be the database service name, not &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error 1049, unknown database:&lt;/strong&gt; &lt;code&gt;DB_NAME&lt;/code&gt; points at a database that does not exist, usually because a migration script created it with a different prefix or the import silently failed partway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL is not running at all:&lt;/strong&gt; connection refused rather than denied. Check the service before touching any WordPress file, because nothing in &lt;code&gt;wp-config.php&lt;/code&gt; can fix a stopped daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database answers but tables are unreadable:&lt;/strong&gt; WordPress shows the same page and sometimes offers a repair link. That link only works after you add &lt;code&gt;define('WP_ALLOW_REPAIR', true);&lt;/code&gt; to &lt;code&gt;wp-config.php&lt;/code&gt;, and you must remove it afterwards, because &lt;code&gt;/wp-admin/maint/repair.php&lt;/code&gt; is reachable without logging in while that constant is set.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  MySQL connection limits, InnoDB corruption and the full disk that causes both
&lt;/h2&gt;

&lt;p&gt;These three failures look separate and are usually the same incident at different stages. A disk fills, MySQL cannot write, connections pile up waiting, and what started as a slow page ends with a tablespace that will not open.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error 1040, too many connections:&lt;/strong&gt; MySQL and MariaDB both default &lt;code&gt;max_connections&lt;/code&gt; to 151. Every PHP-FPM worker holds one connection while it runs a query, so if &lt;code&gt;pm.max_children&lt;/code&gt; across your pools exceeds that number, a traffic spike exhausts the pool and every later request fails. Check &lt;code&gt;SHOW GLOBAL STATUS LIKE 'Max_used_connections';&lt;/code&gt; before raising anything, because the real fix is usually fewer slow queries, not more connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The disk at 100 percent:&lt;/strong&gt; binary logs, InnoDB temporary files and unrotated error logs fill a volume quietly. MySQL then refuses all writes, WordPress cannot save a post or a session, and cron jobs fail silently. Run &lt;code&gt;df -h&lt;/code&gt; first on every database incident, before you read a single log line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bloated &lt;code&gt;wp_options&lt;/code&gt; table:&lt;/strong&gt; expired transients and rows with &lt;code&gt;autoload&lt;/code&gt; set to &lt;code&gt;yes&lt;/code&gt; are loaded on every single request. A few hundred kilobytes of autoloaded data turns into real memory pressure per worker, which multiplies across concurrent requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InnoDB corruption after an unclean stop:&lt;/strong&gt; the server refuses to start and the error log names a page. &lt;code&gt;mysqlcheck --repair&lt;/code&gt; does not help here, it only repairs MyISAM tables. The recovery path is starting with &lt;code&gt;innodb_force_recovery&lt;/code&gt; set to 1, dumping with &lt;code&gt;mysqldump&lt;/code&gt;, then reimporting into a clean instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note the ordering: only the last of these needs a backup, and only the last is unrecoverable without one.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does a wrong WP_HOME or WP_SITEURL value lock you out of wp-admin?
&lt;/h2&gt;

&lt;p&gt;WordPress stores its own address twice, in the &lt;code&gt;home&lt;/code&gt; and &lt;code&gt;siteurl&lt;/code&gt; rows of &lt;code&gt;wp_options&lt;/code&gt;, and every page it builds is constructed from those values. Get them wrong and WordPress does not fail, it faithfully redirects you to the address you told it to use. That address is the one that does not work, including for the login page you need in order to correct it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constants silently win over the database:&lt;/strong&gt; &lt;code&gt;WP_HOME&lt;/code&gt; and &lt;code&gt;WP_SITEURL&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; override the stored options and grey out both fields in Settings then General. People change the database values, see no effect, and conclude the site is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheme mismatch behind a reverse proxy:&lt;/strong&gt; the proxy terminates TLS and forwards plain HTTP, so PHP never sets &lt;code&gt;$_SERVER['HTTPS']&lt;/code&gt;. WordPress sees an insecure request for an &lt;code&gt;https://&lt;/code&gt; site address and redirects to itself forever. Browsers give up after roughly 20 hops and show &lt;code&gt;ERR_TOO_MANY_REDIRECTS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The www and bare hostname disagreeing:&lt;/strong&gt; setting &lt;code&gt;siteurl&lt;/code&gt; to &lt;code&gt;https://www.example.com&lt;/code&gt; while the certificate or DNS only answers on &lt;code&gt;example.com&lt;/code&gt; produces the same loop, with a TLS warning in front of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie domain mismatch:&lt;/strong&gt; login appears to succeed, then bounces straight back to the login form. The auth cookie was written for a host you are not browsing. &lt;code&gt;COOKIE_DOMAIN&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; is the lever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix it from the shell, not the browser: &lt;code&gt;wp option update home 'https://example.com' --skip-plugins --skip-themes&lt;/code&gt;, then the same for &lt;code&gt;siteurl&lt;/code&gt;. Content containing hardcoded old URLs needs &lt;code&gt;wp search-replace&lt;/code&gt;, which handles serialized data that a raw SQL &lt;code&gt;UPDATE&lt;/code&gt; would corrupt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Permalinks, .htaccess and nginx rewrites: the disagreement that 404s every page
&lt;/h2&gt;

&lt;p&gt;The signature of this failure is unmistakable: the homepage loads perfectly, every other URL returns 404, and the admin either works or throws you back to the dashboard. The homepage survives because it resolves to &lt;code&gt;index.php&lt;/code&gt; through the default directory index. Everything else depends on a rewrite rule that has stopped applying.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AllowOverride None&lt;/code&gt; in the Apache vhost:&lt;/strong&gt; Apache reads &lt;code&gt;.htaccess&lt;/code&gt; only where the directory block permits it. Set this to &lt;code&gt;None&lt;/code&gt; and your &lt;code&gt;.htaccess&lt;/code&gt; becomes an inert text file. The rules are present, correct and completely ignored, which is why comparing file contents against a working site tells you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mod_rewrite not loaded:&lt;/strong&gt; on a fresh Debian or Ubuntu Apache install the module is off until you run &lt;code&gt;a2enmod rewrite&lt;/code&gt; and reload. WordPress writes the rules regardless and reports no problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The WordPress block overwritten:&lt;/strong&gt; the rules live between the &lt;code&gt;# BEGIN WordPress&lt;/code&gt; and &lt;code&gt;# END WordPress&lt;/code&gt; markers. Security plugins, caching plugins and hand edits all write to the same file, and a plugin that rewrites its own block sometimes truncates that one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx never reads &lt;code&gt;.htaccess&lt;/code&gt; at all:&lt;/strong&gt; it has no such mechanism. The site depends entirely on &lt;code&gt;try_files $uri $uri/ /index.php?$args;&lt;/code&gt; in the server block, so migrating an Apache site to nginx without porting that line 404s every post immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale rewrite rules in the database:&lt;/strong&gt; the &lt;code&gt;rewrite_rules&lt;/code&gt; option holds the compiled ruleset. Change the permalink structure to &lt;code&gt;/%postname%/&lt;/code&gt; without flushing and the stored rules no longer match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visiting Settings then Permalinks and saving regenerates them. From the shell, &lt;code&gt;wp rewrite flush --hard&lt;/code&gt; does the same and also rewrites the &lt;code&gt;.htaccess&lt;/code&gt; block.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do file ownership and permission mistakes really take a site offline?
&lt;/h2&gt;

&lt;p&gt;Yes, but the outage looks different from a PHP fatal. You get 403 Forbidden or a 500 from the web server rather than a white page, and it happens after a restore, a container rebuild or a hasty &lt;code&gt;chown&lt;/code&gt;, never on its own. The tell is that nothing in &lt;code&gt;wp-content/debug.log&lt;/code&gt; explains it, because PHP was never reached.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Correct state&lt;/th&gt;
&lt;th&gt;What a wrong state does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Directories under the web root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;755&lt;/code&gt;, owned by the PHP user, &lt;code&gt;www-data&lt;/code&gt; on Debian and Ubuntu&lt;/td&gt;
&lt;td&gt;Remove the execute bit and Apache or nginx returns 403 for every URL, including the homepage. This is a genuine sitewide outage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP files such as &lt;code&gt;index.php&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;644&lt;/code&gt;, readable by the web server&lt;/td&gt;
&lt;td&gt;Files owned by &lt;code&gt;root&lt;/code&gt; after an unpacked backup are unreadable, so the server 500s on include&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp-config.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;640&lt;/code&gt; or &lt;code&gt;600&lt;/code&gt;, ideally not group readable&lt;/td&gt;
&lt;td&gt;Too tight and PHP cannot read its own credentials, which surfaces as a database connection failure you will misdiagnose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp-content/uploads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writable by the PHP user&lt;/td&gt;
&lt;td&gt;Media uploads fail with a vague error, plugin and core updates fall back to prompting for FTP credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The whole tree set to &lt;code&gt;777&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Never correct&lt;/td&gt;
&lt;td&gt;The site keeps working, which is the problem. Any other account on the host can rewrite your PHP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two specifics worth committing to memory. If WordPress asks for FTP details during an update, PHP cannot write to its own directory, and &lt;code&gt;define('FS_METHOD', 'direct');&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; only helps once ownership is actually correct. In Docker, set the container user to match the bind mount owner rather than chowning the volume to &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  WP-Cron misconfiguration: the silent damage that becomes a visible outage
&lt;/h2&gt;

&lt;p&gt;WP-Cron is not cron. There is no scheduler. WordPress checks the due list on page load and fires a loopback HTTP request to &lt;code&gt;wp-cron.php&lt;/code&gt; to run whatever is overdue. Every failure mode follows from that one design fact, and none of them announce themselves until something else breaks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A low traffic site never runs its jobs:&lt;/strong&gt; no visitors means no page loads means no cron. Scheduled backups, transient cleanup and security scans simply do not happen. You discover this when you need the backup that was never taken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A busy site runs it too often:&lt;/strong&gt; each qualifying request can spawn another &lt;code&gt;wp-cron.php&lt;/code&gt; call. Under traffic these stack against your PHP-FPM worker pool, and once workers are exhausted the site returns 502 to real visitors. This is the case where cron causes the visible outage directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The loopback request cannot reach itself:&lt;/strong&gt; HTTP basic auth on staging, a firewall that blocks the server calling its own public hostname, or a container whose DNS resolves the domain to an address it cannot route, all break the call silently. WordPress logs nothing useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;cron&lt;/code&gt; option grows without bound:&lt;/strong&gt; the schedule lives in a single autoloaded row in &lt;code&gt;wp_options&lt;/code&gt;. Plugins that queue per item jobs, Action Scheduler backlogs in particular, inflate that row so it is unserialized on every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlapping runs:&lt;/strong&gt; &lt;code&gt;WP_CRON_LOCK_TIMEOUT&lt;/code&gt; defaults to 60 seconds, so a job that takes longer than that can start again before the first finishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is the same everywhere: &lt;code&gt;define('DISABLE_WP_CRON', true);&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt;, then a system crontab entry running &lt;code&gt;wp cron event run --due-now&lt;/code&gt; every 5 minutes. Audit the backlog with &lt;code&gt;wp cron event list&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do caching layers and a misconfigured object cache serve blank or stale pages?
&lt;/h2&gt;

&lt;p&gt;Caching does not usually cause an outage. It extends one. A cache in front of a broken site keeps serving the broken response after you have fixed the underlying fault, which is why people spend an hour debugging a problem that stopped existing 40 minutes ago.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 500 page gets cached:&lt;/strong&gt; an nginx &lt;code&gt;fastcgi_cache&lt;/code&gt; or a CDN that caches error responses will hold that 500 for the full TTL. Fix the fatal, purge nothing, and every visitor still sees the error. Always confirm what you are looking at is fresh, with a cache busting query string or a direct request to the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WP_CACHE&lt;/code&gt; set without the drop-in present:&lt;/strong&gt; page cache plugins add &lt;code&gt;define('WP_CACHE', true);&lt;/code&gt; to &lt;code&gt;wp-config.php&lt;/code&gt; and install &lt;code&gt;wp-content/advanced-cache.php&lt;/code&gt;. Delete the plugin folder by hand and the constant remains, pointing at a file that is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two object cache drop-ins fighting:&lt;/strong&gt; only one &lt;code&gt;wp-content/object-cache.php&lt;/code&gt; can exist. Install a second caching plugin and it overwrites the first, so the site now talks to a backend that was never configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis unreachable with no graceful fallback:&lt;/strong&gt; some drop-ins throw a fatal rather than falling back to the database, which turns a restarted Redis container into a white screen across the whole site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis evicting or refusing writes:&lt;/strong&gt; with &lt;code&gt;maxmemory-policy&lt;/code&gt; set to &lt;code&gt;noeviction&lt;/code&gt;, a full instance returns OOM errors on writes instead of discarding old keys. &lt;code&gt;allkeys-lru&lt;/code&gt; is the safer choice for a WordPress object cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged in pages served to everyone:&lt;/strong&gt; if the cache does not exclude the &lt;code&gt;wordpress_logged_in_&lt;/code&gt; cookie, one user's admin bar, cart or session leaks to anonymous visitors.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>selfhosting</category>
      <category>php</category>
      <category>mysql</category>
    </item>
    <item>
      <title>What a Malicious Ollama Model Can Actually Do to Your Host, and How to Sandbox /api/pull</title>
      <dc:creator>John</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:14:46 +0000</pubDate>
      <link>https://dev.to/john_182319291/what-a-malicious-ollama-model-can-actually-do-to-your-host-and-how-to-sandbox-apipull-3al3</link>
      <guid>https://dev.to/john_182319291/what-a-malicious-ollama-model-can-actually-do-to-your-host-and-how-to-sandbox-apipull-3al3</guid>
      <description>&lt;p&gt;A malicious Ollama model is not a virus you double click, but it is untrusted input handed to a C parser, a template engine and your filesystem in one request. The realistic damage from a hostile &lt;code&gt;/api/pull&lt;/code&gt; is disk exhaustion, VRAM starvation, blob writes under &lt;code&gt;~/.ollama/models&lt;/code&gt;, a poisoned chat template that silently rewrites every prompt, and memory corruption in the GGUF loader if the file is crafted for it. None of that requires a vulnerability in your app code, only an Ollama daemon that trusts whoever can reach port 11434 and whichever registry a tag points at. Bind the daemon to localhost, pin models by SHA256 digest, run the container as a non root user with a read only root filesystem and a capped model volume, and the entire class collapses to a bad model that answers badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo developer running Ollama on a laptop, for example a contractor testing &lt;code&gt;llama3.1:8b&lt;/code&gt; locally:&lt;/strong&gt; leave &lt;code&gt;OLLAMA_HOST&lt;/code&gt; at &lt;code&gt;127.0.0.1:11434&lt;/code&gt; and pin digests, because your only real exposure is pulling a model whose tag moved under you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two person startup running Ollama on one rented GPU box, for example a founder pair serving an internal assistant:&lt;/strong&gt; run it in Docker as UID 1000 with &lt;code&gt;--read-only&lt;/code&gt;, &lt;code&gt;--cap-drop ALL&lt;/code&gt; and a sized model volume, because a single unbounded pull can fill the disk that also holds your Postgres data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team fronting Ollama with Open WebUI or Continue, for example five engineers sharing one workstation:&lt;/strong&gt; put model management behind the proxy and block &lt;code&gt;/api/pull&lt;/code&gt;, &lt;code&gt;/api/create&lt;/code&gt;, &lt;code&gt;/api/push&lt;/code&gt; and &lt;code&gt;/api/delete&lt;/code&gt; for normal users, because chat access and registry access are not the same privilege.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone building agents or RAG on Ollama, for example a support bot with tool calling:&lt;/strong&gt; treat the Modelfile &lt;code&gt;TEMPLATE&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; blocks as attacker controlled text, because a poisoned template reaches the model before your prompt does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consultancies holding client data, for example a two person shop under an NDA:&lt;/strong&gt; keep model pulls on a staging host, mirror approved blobs internally, and never let the inference container make outbound calls, because a model that can reach the internet is an exfiltration path with a GPU attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff is that every layer you add between Ollama and the host, non root users, read only mounts, seccomp filters and offline registries, also removes the one click convenience that made you choose Ollama over a served runtime in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What can a malicious model actually do to an Ollama host?&lt;/li&gt;
&lt;li&gt;Where does Ollama put model blobs, and what does /api/pull write to disk?&lt;/li&gt;
&lt;li&gt;Is a GGUF file data or code?&lt;/li&gt;
&lt;li&gt;What can a Modelfile do that a bare weights file cannot?&lt;/li&gt;
&lt;li&gt;Which Ollama endpoints mutate state, and who can reach them?&lt;/li&gt;
&lt;li&gt;How do you pin a model by digest instead of trusting a moving tag?&lt;/li&gt;
&lt;li&gt;Container hardening: non root user, read only rootfs and dropped capabilities&lt;/li&gt;
&lt;li&gt;Seccomp, device passthrough and the GPU exception to every sandbox rule&lt;/li&gt;
&lt;li&gt;How do you cap disk and VRAM so one pull cannot starve the host?&lt;/li&gt;
&lt;li&gt;What should an Ollama container be allowed to reach on the network?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What can a malicious model actually do to an Ollama host?
&lt;/h2&gt;

&lt;p&gt;Start with the honest bound: a GGUF file is not a shell script, and Ollama does not execute it. The damage comes from what the daemon does around the weights, and from the fact that &lt;code&gt;/api/pull&lt;/code&gt; is a write operation with no signature check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fill the disk:&lt;/strong&gt; a pull streams blobs into &lt;code&gt;~/.ollama/models/blobs&lt;/code&gt; with no default size ceiling, so a crafted manifest pointing at a 400 GB blob will keep writing until the filesystem is full, taking down anything else sharing that volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starve the GPU:&lt;/strong&gt; loading a model with an inflated context length or a large layer count reserves VRAM at load time, and with &lt;code&gt;OLLAMA_MAX_LOADED_MODELS&lt;/code&gt; above 1 a few pulls plus a few &lt;code&gt;/api/generate&lt;/code&gt; calls can pin every byte of a 24 GB card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewrite every prompt:&lt;/strong&gt; the &lt;code&gt;TEMPLATE&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; directives travel with the model, so a hostile publisher controls the text wrapped around your user input on every single request, including instructions to your tool calling layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corrupt loader memory:&lt;/strong&gt; GGUF headers, tensor counts and metadata key value pairs are parsed in C before any inference happens, and a malformed field is untrusted input reaching a memory unsafe parser running with whatever privileges the daemon has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persist under a familiar name:&lt;/strong&gt; &lt;code&gt;/api/create&lt;/code&gt; can register a model called &lt;code&gt;llama3.1:8b&lt;/code&gt; locally that points at attacker chosen weights, so &lt;code&gt;ollama list&lt;/code&gt; looks normal while every request goes somewhere else.&lt;/p&gt;

&lt;p&gt;The severity of all five depends entirely on one question, answered in the next section: what does the daemon actually write, and as which user.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where does Ollama put model blobs, and what does /api/pull write to disk?
&lt;/h2&gt;

&lt;p&gt;Ollama keeps everything under one root, &lt;code&gt;$OLLAMA_MODELS&lt;/code&gt;, which defaults to &lt;code&gt;~/.ollama/models&lt;/code&gt; for a user install and &lt;code&gt;/usr/share/ollama/.ollama/models&lt;/code&gt; when the Linux service runs as the &lt;code&gt;ollama&lt;/code&gt; system user. In the official Docker image it is &lt;code&gt;/root/.ollama&lt;/code&gt;, which is why so many compose files mount a volume there and quietly grant the daemon root ownership of every byte it writes.&lt;/p&gt;

&lt;p&gt;Inside that root there are exactly two directories, and knowing which is which decides where your quota and your read only mount go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;blobs/&lt;/code&gt;:&lt;/strong&gt; flat content addressed storage, one file per layer, named &lt;code&gt;sha256-&amp;lt;64 hex characters&amp;gt;&lt;/code&gt;. Weights, adapters and the license text all land here, and a single 8B model at Q4 quantisation typically writes one blob of several gigabytes plus four or five tiny ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;manifests/&lt;/code&gt;:&lt;/strong&gt; a nested path mirroring the registry, for example &lt;code&gt;manifests/registry.ollama.ai/library/llama3.1/8b&lt;/code&gt;, holding a JSON document that lists the digests a tag resolves to. Delete a manifest and the blobs stay, which is how disks fill after people believe they cleaned up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write path during a pull:&lt;/strong&gt; the daemon fetches the manifest first, then each missing blob to a &lt;code&gt;-partial&lt;/code&gt; file, verifies the digest, and renames it into place. A resumed pull reuses the partial, so an interrupted 20 GB download still occupies 20 GB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else in &lt;code&gt;$HOME/.ollama&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;id_ed25519&lt;/code&gt; and &lt;code&gt;id_ed25519.pub&lt;/code&gt;, the keypair identifying this host to the registry, plus &lt;code&gt;history&lt;/code&gt; for the CLI. That private key sits beside the blobs, which matters once you decide who owns the volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is a GGUF file data or code?
&lt;/h2&gt;

&lt;p&gt;Data, and that distinction matters less than people assume. GGUF is a binary container: a magic string &lt;code&gt;GGUF&lt;/code&gt;, a version integer, tensor and metadata counts, a key value metadata block, then the tensor data. There is no bytecode section and no entry point, so nothing in the file gets jumped to. The comparison people reach for is the right one: this is not a pickle file. PyTorch &lt;code&gt;.bin&lt;/code&gt; and &lt;code&gt;.pt&lt;/code&gt; checkpoints deserialise Python objects and can execute on load, which is exactly why safetensors and GGUF exist.&lt;/p&gt;

&lt;p&gt;The risk is that a container format still has to be parsed, and the parser is llama.cpp's C and C++ code running inside the Ollama daemon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attacker controlled counts:&lt;/strong&gt; the header declares how many tensors and metadata entries follow, and those integers drive allocations and loops before any of the actual content is validated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attacker controlled offsets:&lt;/strong&gt; each tensor carries an offset and dimensions, so a file can claim a tensor extends past the end of the mapped region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attacker controlled strings:&lt;/strong&gt; metadata keys and string values are length prefixed, and length prefixed strings in C are the oldest bug class there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory mapping by default:&lt;/strong&gt; Ollama maps weights rather than copying them, so a malformed structure is read straight from a file the attacker wrote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bugs get fixed, then reappear:&lt;/strong&gt; llama.cpp GGUF parsing has had a steady stream of out of bounds and integer overflow fixes, which is evidence the surface is real, not that it is now clean.&lt;/p&gt;

&lt;p&gt;Treat GGUF as untrusted input to a memory unsafe parser. That framing, not malware scanning, is what justifies the container boundaries in the later sections.&lt;/p&gt;




&lt;h2&gt;
  
  
  What can a Modelfile do that a bare weights file cannot?
&lt;/h2&gt;

&lt;p&gt;A Modelfile is the part people forget to review, because it looks like configuration rather than payload. It ships inside the model when you pull a tag, and &lt;code&gt;/api/create&lt;/code&gt; accepts one over HTTP. It cannot spawn a process, but it controls everything that surrounds the weights at runtime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Legitimate use&lt;/th&gt;
&lt;th&gt;What a hostile publisher gets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Points at the base weights or another local model&lt;/td&gt;
&lt;td&gt;Redirects a familiar name to blobs you never inspected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TEMPLATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formats chat turns with Go template syntax&lt;/td&gt;
&lt;td&gt;Wraps, reorders or drops your messages and tool definitions on every request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SYSTEM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets a default persona&lt;/td&gt;
&lt;td&gt;Injects standing instructions your application never sent and cannot see in its own logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PARAMETER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tunes &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;num_ctx&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sets &lt;code&gt;num_ctx&lt;/code&gt; high enough to reserve far more VRAM than the model needs, or strips stop tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADAPTER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies a LoRA on top of the base&lt;/td&gt;
&lt;td&gt;Attaches behaviour changing weights that never appear in the base model's reputation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MESSAGE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seeds few shot examples&lt;/td&gt;
&lt;td&gt;Preloads fake conversation history that steers the first real answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two habits close most of this. Run &lt;code&gt;ollama show --modelfile &amp;lt;name&amp;gt;&lt;/code&gt; after every pull and read the &lt;code&gt;TEMPLATE&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; blocks before the model serves traffic, because a diff against the upstream template takes under a minute. Then treat &lt;code&gt;PARAMETER num_ctx&lt;/code&gt; as a resource declaration, not a preference: it is the single field that turns a model listing into a memory allocation request, which is why the next sections cap it at the daemon level rather than trusting what arrived in the file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which Ollama endpoints mutate state, and who can reach them?
&lt;/h2&gt;

&lt;p&gt;Ollama has no user model. There are no accounts, no API keys and no roles, so the answer to "who can reach them" is exactly "anyone who can open a TCP connection to port 11434". The default bind is &lt;code&gt;127.0.0.1:11434&lt;/code&gt;, and setting &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; to reach the box from a laptop is the single change that converts every endpoint below into a public one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /api/pull&lt;/code&gt;:&lt;/strong&gt; downloads a manifest and its blobs from any registry the tag names, writing to disk and consuming bandwidth, with no confirmation step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /api/create&lt;/code&gt;:&lt;/strong&gt; registers a new model from a Modelfile supplied in the request body, which is how an attacker binds a trusted looking name to weights or a template of their choosing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /api/blobs/:digest&lt;/code&gt;:&lt;/strong&gt; uploads a raw blob directly into the store, so the attacker does not even need a reachable registry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DELETE /api/delete&lt;/code&gt;:&lt;/strong&gt; removes a model. One request destroys a 40 GB download and the next request to your app fails with a model not found error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /api/push&lt;/code&gt;:&lt;/strong&gt; uploads a local model to a registry, signed with the &lt;code&gt;id_ed25519&lt;/code&gt; key sitting next to your blobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /api/generate&lt;/code&gt; and &lt;code&gt;/api/chat&lt;/code&gt;:&lt;/strong&gt; read only for the filesystem, but they load models into VRAM and run inference, which is the resource abuse path.&lt;/p&gt;

&lt;p&gt;The practical rule is that chat access and model management are separate privileges, and Ollama does not separate them for you. Your reverse proxy has to: allow &lt;code&gt;/api/chat&lt;/code&gt;, &lt;code&gt;/api/tags&lt;/code&gt; and the &lt;code&gt;/v1&lt;/code&gt; compatibility routes, and return 403 for the five write endpoints above.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you pin a model by digest instead of trusting a moving tag?
&lt;/h2&gt;

&lt;p&gt;Ollama has no &lt;code&gt;pull name@sha256:...&lt;/code&gt; syntax, so you cannot ask the daemon to pin for you. Tags like &lt;code&gt;llama3.1:8b&lt;/code&gt; are pointers that the publisher can repoint at any time, and a later &lt;code&gt;ollama pull&lt;/code&gt; will happily replace your blobs with different weights under the same name. Pinning is therefore something you enforce around Ollama, not inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record the manifest after the first pull:&lt;/strong&gt; read &lt;code&gt;manifests/registry.ollama.ai/library/llama3.1/8b&lt;/code&gt;, a small JSON file listing every layer digest, and commit a copy to the repository that deploys the service. This is your reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the short ID on every host:&lt;/strong&gt; &lt;code&gt;ollama list&lt;/code&gt; prints an ID column holding the first 12 hex characters of the manifest digest. Two machines showing different IDs for the same tag means the tag moved between the two pulls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail the deploy on drift:&lt;/strong&gt; in CI, diff the live manifest against the committed one and exit non zero on mismatch. A blob rename is enough to catch, because blob filenames are &lt;code&gt;sha256-&amp;lt;digest&amp;gt;&lt;/code&gt; and any weight change produces a different filename.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freeze the store instead of re pulling:&lt;/strong&gt; once verified, mount the model directory read only and never run &lt;code&gt;ollama pull&lt;/code&gt; in production. Pull on a staging host, verify, then copy the &lt;code&gt;blobs&lt;/code&gt; and &lt;code&gt;manifests&lt;/code&gt; trees across.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mirror internally if you run more than two hosts:&lt;/strong&gt; serve the verified blobs from your own object storage or an internal registry, so the production daemon never resolves a public tag at all.&lt;/p&gt;

&lt;p&gt;The cost is honest: you lose one command updates and take on a small manifest review each time you deliberately upgrade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Container hardening: non root user, read only rootfs and dropped capabilities
&lt;/h2&gt;

&lt;p&gt;The official &lt;code&gt;ollama/ollama&lt;/code&gt; image runs as root with &lt;code&gt;HOME=/root/.ollama&lt;/code&gt;, so the default compose file most people copy gives a network reachable daemon root ownership of the volume holding your blobs and the &lt;code&gt;id_ed25519&lt;/code&gt; key. Fixing that is four flags and one chown.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default image behaviour&lt;/th&gt;
&lt;th&gt;Hardened value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Process user&lt;/td&gt;
&lt;td&gt;root, UID 0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;user: "1000:1000"&lt;/code&gt; after &lt;code&gt;chown -R 1000:1000&lt;/code&gt; on the volume, plus &lt;code&gt;HOME=/home/ollama&lt;/code&gt; and &lt;code&gt;OLLAMA_MODELS=/models&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root filesystem&lt;/td&gt;
&lt;td&gt;Writable, so a parser bug can drop files anywhere in the container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;read_only: true&lt;/code&gt; with &lt;code&gt;tmpfs: /tmp:size=512m,noexec&lt;/code&gt; for runner extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capabilities&lt;/td&gt;
&lt;td&gt;Full default Docker set, including &lt;code&gt;CAP_CHOWN&lt;/code&gt; and &lt;code&gt;CAP_NET_RAW&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cap_drop: [ALL]&lt;/code&gt;, adding none back, because inference needs no capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;setuid binaries can still raise privileges&lt;/td&gt;
&lt;td&gt;&lt;code&gt;security_opt: [no-new-privileges:true]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model store&lt;/td&gt;
&lt;td&gt;Bind mount, writable, often the host home directory&lt;/td&gt;
&lt;td&gt;Named volume mounted at &lt;code&gt;/models&lt;/code&gt;, writable only while you pull, &lt;code&gt;:ro&lt;/code&gt; in production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details catch people out. First, the model directory must be writable during a pull and can be read only afterwards, so run pulls with a separate one shot container that mounts the same volume read write, then keep the serving container on &lt;code&gt;:ro&lt;/code&gt;. Second, &lt;code&gt;no-new-privileges&lt;/code&gt; and &lt;code&gt;cap_drop: ALL&lt;/code&gt; do not affect throughput at all, because llama.cpp needs no elevated syscall to read a mapped file and talk to a device node.&lt;/p&gt;

&lt;p&gt;What this buys you is narrow but real: a GGUF parser bug now executes as UID 1000 inside a container with no capabilities and nowhere writable except &lt;code&gt;/tmp&lt;/code&gt; and the model volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seccomp, device passthrough and the GPU exception to every sandbox rule
&lt;/h2&gt;

&lt;p&gt;Docker's default seccomp profile already blocks around 44 syscalls, including &lt;code&gt;mount&lt;/code&gt;, &lt;code&gt;ptrace&lt;/code&gt; and the kernel module calls, and Ollama needs none of them. Keep the default and resist the two pieces of advice that circulate whenever a CUDA container misbehaves: &lt;code&gt;--privileged&lt;/code&gt; and &lt;code&gt;--security-opt seccomp=unconfined&lt;/code&gt;. Neither is required for inference, and both undo the previous section entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass through specific GPUs, not all of them:&lt;/strong&gt; &lt;code&gt;--gpus '"device=0"'&lt;/code&gt; or the compose &lt;code&gt;device_ids: ["0"]&lt;/code&gt; limits the container to one card, so a second GPU stays available for anything else on the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know which device nodes appear:&lt;/strong&gt; the NVIDIA container toolkit injects &lt;code&gt;/dev/nvidiactl&lt;/code&gt;, &lt;code&gt;/dev/nvidia-uvm&lt;/code&gt; and &lt;code&gt;/dev/nvidia0&lt;/code&gt;. ROCm needs &lt;code&gt;/dev/kfd&lt;/code&gt; and &lt;code&gt;/dev/dri&lt;/code&gt;, and &lt;code&gt;/dev/kfd&lt;/code&gt; is the broader grant of the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accept the hole you just opened:&lt;/strong&gt; those nodes are ioctl interfaces straight into a closed source kernel driver. Seccomp filters syscall numbers, not ioctl arguments, so the driver surface stays reachable from inside the container no matter how many capabilities you dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not expect VRAM isolation from Docker:&lt;/strong&gt; separation between processes sharing a card is enforced by the GPU driver, not by the container runtime, which is why the disk and VRAM limits in the next section matter more here than any Linux security module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip user namespace remapping if it fights the toolkit:&lt;/strong&gt; &lt;code&gt;userns-remap&lt;/code&gt; and GPU passthrough interact badly on some driver versions. A plain UID 1000 container with &lt;code&gt;cap_drop: ALL&lt;/code&gt; is the pragmatic stopping point.&lt;/p&gt;

&lt;p&gt;The honest summary: containerising Ollama contains the filesystem well and the GPU barely. Treat the card as shared, hostile territory and budget it explicitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you cap disk and VRAM so one pull cannot starve the host?
&lt;/h2&gt;

&lt;p&gt;Both limits have to live outside Ollama, because the daemon will happily accept whatever the manifest and the request ask for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give the model store its own filesystem:&lt;/strong&gt; a dedicated LVM volume, a ZFS dataset with &lt;code&gt;quota=200G&lt;/code&gt;, or an XFS project quota on the mount point. A &lt;code&gt;--storage-opt size=&lt;/code&gt; on the container only works on overlay2 over XFS, so the separate volume is the portable answer. When the quota is hit the pull fails and Postgres on the root disk keeps running, which is the entire point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert on the partial files, not the total:&lt;/strong&gt; a growing &lt;code&gt;sha256-&amp;lt;digest&amp;gt;-partial&lt;/code&gt; file in &lt;code&gt;blobs/&lt;/code&gt; is an in flight download, and comparing its size against your quota tells you about a hostile pull minutes before the disk fills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load one model at a time:&lt;/strong&gt; &lt;code&gt;OLLAMA_MAX_LOADED_MODELS=1&lt;/code&gt; and &lt;code&gt;OLLAMA_NUM_PARALLEL=1&lt;/code&gt; mean a single resident model and a single concurrent request, so VRAM use becomes predictable instead of a function of how many distinct model names someone sends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unload aggressively:&lt;/strong&gt; &lt;code&gt;OLLAMA_KEEP_ALIVE=30s&lt;/code&gt; instead of the 5 minute default frees the card between bursts. For a two person team hitting the assistant a few times an hour, the reload cost is a few seconds and the card is free the rest of the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Override the context length yourself:&lt;/strong&gt; &lt;code&gt;OLLAMA_CONTEXT_LENGTH&lt;/code&gt; sets the default window at the daemon, which neutralises a &lt;code&gt;PARAMETER num_ctx&lt;/code&gt; shipped inside someone else's Modelfile. Cap the request queue too, &lt;code&gt;OLLAMA_MAX_QUEUE&lt;/code&gt; defaults to 512 pending requests.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;mem_limit&lt;/code&gt; and &lt;code&gt;cpus&lt;/code&gt; on the container so CPU offload cannot swap the host to death either.&lt;/p&gt;




&lt;h2&gt;
  
  
  What should an Ollama container be allowed to reach on the network?
&lt;/h2&gt;

&lt;p&gt;Nothing, in steady state. A serving daemon needs inbound requests from your proxy and no outbound access at all. Egress is only required while pulling, and pulling is a deliberate, occasional act.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish to loopback, never to the host's public interface:&lt;/strong&gt; write &lt;code&gt;127.0.0.1:11434:11434&lt;/code&gt; in the ports mapping. A bare &lt;code&gt;11434:11434&lt;/code&gt; binds every interface, and Docker inserts its rules ahead of &lt;code&gt;ufw&lt;/code&gt;, so a firewall you believe is blocking the port is not blocking it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer no published port at all:&lt;/strong&gt; put Ollama and its proxy on the same user defined bridge network and let the proxy reach &lt;code&gt;http://ollama:11434&lt;/code&gt; by container name. The daemon then has no host port, and &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; is safe inside that network because the network is the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mark the inference network &lt;code&gt;internal: true&lt;/code&gt;:&lt;/strong&gt; a Compose network with that flag gets no route to the outside, so the container cannot resolve or contact any registry. Attach the pull container to a second, normal network only while you pull, then detach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowlist one destination if you must keep egress:&lt;/strong&gt; &lt;code&gt;registry.ollama.ai&lt;/code&gt; on TCP 443 plus DNS. Everything else denied, because a model that can reach arbitrary hosts turns tool calling mistakes into exfiltration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave &lt;code&gt;OLLAMA_ORIGINS&lt;/code&gt; alone:&lt;/strong&gt; it controls which browser origins may call the API, and setting it to &lt;code&gt;*&lt;/code&gt; means any web page you visit can script requests against your local daemon, including the write endpoints. Name the specific origins your UI uses instead.&lt;/p&gt;

&lt;p&gt;Test the result from another machine with &lt;code&gt;curl -m 5 http://&amp;lt;host&amp;gt;:11434/api/tags&lt;/code&gt;. A connection timeout is the correct outcome, not a JSON model list.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>security</category>
      <category>llm</category>
      <category>docker</category>
    </item>
    <item>
      <title>When You Should Not Run Kubernetes, and What a Single Docker Compose Host Really Costs You</title>
      <dc:creator>John</dc:creator>
      <pubDate>Thu, 06 Aug 2026 08:17:33 +0000</pubDate>
      <link>https://dev.to/john_182319291/when-you-should-not-run-kubernetes-and-what-a-single-docker-compose-host-really-costs-you-526i</link>
      <guid>https://dev.to/john_182319291/when-you-should-not-run-kubernetes-and-what-a-single-docker-compose-host-really-costs-you-526i</guid>
      <description>&lt;p&gt;Do not run Kubernetes for a client project that fits on one server, has no autoscaling requirement, and is maintained by fewer than three people who touch infrastructure. A single Docker Compose host running behind a reverse proxy handles the majority of small agency workloads, and it costs you exactly two things: minutes of downtime during reboots, and a manual path back from hardware failure. Kubernetes does not remove those costs, it converts them into a control plane you now have to keep alive. Pick the failure mode you can actually staff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agency running 12 client sites on one provider (WordPress, Node APIs, a few static builds):&lt;/strong&gt; one Compose host per client tier, because per client blast radius matters more to you than scheduling, and your real risk is a botched deploy, not capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two person team with one revenue critical app (SaaS, single Postgres, 24/7 traffic):&lt;/strong&gt; Compose plus a warm standby and tested restores, because a second machine you can promote beats a cluster nobody on the team can debug at 3am.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team of six with a genuine multi service platform (8 or more services, several deploys a day):&lt;/strong&gt; Kubernetes starts paying for itself here, because rolling deploys, health gating and per service resource limits stop being manual work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contractor handing infrastructure back to the client at the end of the engagement:&lt;/strong&gt; Compose, because a docker-compose.yml plus a README is a deliverable the client can actually run, and a cluster is a dependency on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone under a contractual uptime commitment with penalties (SLA with credits attached):&lt;/strong&gt; you need redundancy of some kind, and that is the one case where the orchestration question becomes a real engineering decision rather than a preference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo developer hosting side projects and one paying client:&lt;/strong&gt; Compose on a single host, because every hour spent on cluster maintenance is an hour not billed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff: Kubernetes buys you automatic recovery and rolling deploys at the price of a permanent operational surface you must keep patched, while Compose buys you a system one person can hold in their head at the price of doing recovery yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When should you not run Kubernetes?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When should you not run Kubernetes?
&lt;/h2&gt;

&lt;p&gt;Skip Kubernetes when the cluster would exist to solve a problem you do not have yet. Four conditions, and if three of them describe your project, stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your workload fits on one machine:&lt;/strong&gt; the whole stack, application, database, cache and reverse proxy, runs inside the RAM and CPU of a single server you can afford. If you are not resource constrained, the scheduler has nothing to schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traffic is predictable:&lt;/strong&gt; you know roughly what next Tuesday looks like because it looks like last Tuesday. Horizontal Pod Autoscaler is worth real money when load swings by an order of magnitude within an hour. It is worth nothing when your peak is 3x your trough and both fit on the same box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody on the team owns infrastructure full time:&lt;/strong&gt; Kubernetes is a system that needs a person. Version skew between control plane and nodes, CNI upgrades, cert rotation, ingress controller changes and deprecated API versions all arrive whether or not anyone is assigned to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The client will eventually own this:&lt;/strong&gt; you are building something you hand over. A cluster is a hiring requirement disguised as a deployment target.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Points to Compose&lt;/th&gt;
&lt;th&gt;Points to Kubernetes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capacity&lt;/td&gt;
&lt;td&gt;Everything fits on one host&lt;/td&gt;
&lt;td&gt;You are already sharding across machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic shape&lt;/td&gt;
&lt;td&gt;Predictable, within 3x daily swing&lt;/td&gt;
&lt;td&gt;Spiky, unpredictable, event driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team&lt;/td&gt;
&lt;td&gt;Under 3 people touching infra&lt;/td&gt;
&lt;td&gt;A named platform owner or team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy frequency&lt;/td&gt;
&lt;td&gt;A few times a week&lt;/td&gt;
&lt;td&gt;Several times a day across services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handover&lt;/td&gt;
&lt;td&gt;Client inherits the stack&lt;/td&gt;
&lt;td&gt;You operate it indefinitely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime terms&lt;/td&gt;
&lt;td&gt;Best effort, informal&lt;/td&gt;
&lt;td&gt;Contractual SLA with penalties&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The inverse is also true, and worth saying plainly so this does not read as a hit piece. If you run 8 or more services that deploy independently, if two teams need to ship without coordinating, or if your uptime commitment survives a single machine failing, Kubernetes is the correct answer and Compose will hurt you.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Immich vs Google Photos: Why Self-Hosting Your Photo Library Wins in 2026</title>
      <dc:creator>John</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:31:57 +0000</pubDate>
      <link>https://dev.to/john_182319291/immich-vs-google-photos-why-self-hosting-your-photo-library-wins-in-2026-m34</link>
      <guid>https://dev.to/john_182319291/immich-vs-google-photos-why-self-hosting-your-photo-library-wins-in-2026-m34</guid>
      <description>&lt;p&gt;Immich is the better choice if you own a machine that stays powered on and you care where your photos live. It gives you the parts of Google Photos people actually use every day, mobile auto backup, face grouping, map view, albums and shared links, without a storage meter that raises your bill as your library grows. Google Photos still wins on zero maintenance and on search that understands a sentence. If you are willing to spend one evening on setup and roughly an hour a quarter on updates, Immich replaces it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR by reader profile:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Family archivist with 15 years of photos (Marta, two phones, one shared library):&lt;/strong&gt; move to Immich on a small always on box, because a growing archive is exactly the case where a per gigabyte subscription compounds against you forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photographer shooting RAW every weekend (Tomas, 40 megapixel bodies):&lt;/strong&gt; Immich, because RAW files eat cloud tiers fast and you already keep a local working copy that you can point the server at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non technical user with one phone and no home server (Elena, iPhone, no NAS):&lt;/strong&gt; stay on Google Photos for now, because Immich needs someone to own updates, backups and remote access, and that someone would be you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy sensitive professional handling client images (lawyer, therapist, journalist):&lt;/strong&gt; Immich on hardware you control, because the legal question is not whether the provider is trustworthy but who can be compelled to hand over the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homelab owner already running Docker (Sam, existing NAS and reverse proxy):&lt;/strong&gt; Immich, because the marginal cost is one compose stack on infrastructure you maintain anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small team or studio sharing a shoot library (five people, one archive):&lt;/strong&gt; Immich with per user accounts and shared albums, because Google Photos was built for one person and gets awkward the moment several people need write access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central tradeoff: Google Photos sells you freedom from maintenance and pays for it with a recurring bill and a library you do not control, while Immich hands you control and a one time hardware cost and charges you in setup time, backup discipline and the responsibility for your own uptime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is Immich, and can it really replace Google Photos?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is Immich, and can it really replace Google Photos?
&lt;/h2&gt;

&lt;p&gt;Immich is an open source, self-hosted photo and video backup server. You install it on a machine you own, install the Immich app on your phone, and it uploads new photos in the background the way Google Photos does. It is licensed under AGPL-3.0, developed in the open on GitHub, and released on a fast cadence with frequent tagged versions. It is not a photo viewer bolted onto a file share. It is a full stack with its own database, machine learning container and mobile clients for Android and iOS.&lt;/p&gt;

&lt;p&gt;The honest answer on replacement: yes for the daily loop, partially for search.&lt;/p&gt;

&lt;p&gt;The daily loop is backup, browse, album, share. Immich covers all four. You get automatic background upload, a timeline grouped by date, albums, shared albums with other users on your server, public share links with optional passwords and expiry, a map view built from EXIF GPS data, face grouping into named people, and duplicate detection. Multi user support is built in, so each family member gets their own library on the same server.&lt;/p&gt;

&lt;p&gt;Where it differs is the part Google spent a decade on: semantic search quality and the automatic memories and collages that surface old photos without you asking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Google Photos&lt;/th&gt;
&lt;th&gt;Immich&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automatic phone backup&lt;/td&gt;
&lt;td&gt;Yes, first party OS integration&lt;/td&gt;
&lt;td&gt;Yes, background service in the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Face grouping and named people&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, runs locally on your server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural language search&lt;/td&gt;
&lt;td&gt;Yes, strong&lt;/td&gt;
&lt;td&gt;Yes, CLIP based, weaker on complex phrasing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage limit&lt;/td&gt;
&lt;td&gt;Bound to your Google One tier&lt;/td&gt;
&lt;td&gt;Bound to your disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who holds the data&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance owner&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat Immich as a replacement for the product, not a clone of the company. You give up an army of engineers tuning search ranking. You gain a library that cannot be reindexed, repriced or closed without your consent.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>immich</category>
      <category>privacy</category>
      <category>photography</category>
    </item>
  </channel>
</rss>
