<?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: Rozoom News</title>
    <description>The latest articles on DEV Community by Rozoom News (rozoomnews).</description>
    <link>https://dev.to/rozoomnews</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%2Forganization%2Fprofile_image%2F15629%2Fabd1c5c5-fb32-437c-bdb3-91c5d9c875cb.png</url>
      <title>DEV Community: Rozoom News</title>
      <link>https://dev.to/rozoomnews</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rozoomnews"/>
    <language>en</language>
    <item>
      <title>Plex Is Being Exploited. The Fix Shipped Eleven Weeks Earlier.</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:20:28 +0000</pubDate>
      <link>https://dev.to/rozoomnews/plex-is-being-exploited-the-fix-shipped-eleven-weeks-earlier-14b1</link>
      <guid>https://dev.to/rozoomnews/plex-is-being-exploited-the-fix-shipped-eleven-weeks-earlier-14b1</guid>
      <description>&lt;p&gt;A media server is the most forgivable thing to expose to the internet. It is also, this month, the most expensive.&lt;/p&gt;

&lt;p&gt;On 20 October 2026 CISA added four Plex Media Server vulnerabilities to its Known Exploited Vulnerabilities catalogue at once. Chained, they let an attacker with no credentials obtain an admin token and execute code as the Plex process. The researcher who found them scores the chain 9.8.&lt;/p&gt;

&lt;p&gt;The patched build, 1.43.3.10861, was released on &lt;strong&gt;4 August&lt;/strong&gt;. Everything that has happened since is about the gap between a fix existing and a fix being installed, and that gap is the only part of this story you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline, because it is the argument
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;27 July 2026&lt;/td&gt;
&lt;td&gt;Researcher Zach Main reports the chain to Plex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-4 August 2026&lt;/td&gt;
&lt;td&gt;Plex Media Server 1.43.3.10861 ships with the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early September 2026&lt;/td&gt;
&lt;td&gt;Shadowserver counts over 36,000 internet-exposed servers still unpatched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 October 2026&lt;/td&gt;
&lt;td&gt;Full technical write-up published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17 October 2026&lt;/td&gt;
&lt;td&gt;First in-the-wild exploitation observed, against honeypots, dropping a cryptominer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19 October 2026&lt;/td&gt;
&lt;td&gt;Public proof-of-concept exploit on GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 October 2026&lt;/td&gt;
&lt;td&gt;CISA adds all four CVEs to the KEV catalogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23 October 2026&lt;/td&gt;
&lt;td&gt;Federal remediation deadline for the two worst of them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eleven weeks of warning. Thirty-six thousand servers that did not use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the chain actually is
&lt;/h2&gt;

&lt;p&gt;Four issues, each unremarkable alone, which is exactly why they sat unnoticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-96652&lt;/strong&gt; (CVSS 5.3), server-side request forgery: Plex can be made to send POST requests to arbitrary destinations, including its own internal APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-96654&lt;/strong&gt; (CVSS 6.9), URL injection: attacker-controlled parameters reach other plugins' functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-96655&lt;/strong&gt; (CVSS 5.3), another SSRF, this one in the transcoder endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-96656&lt;/strong&gt; (CVSS 8.6), arbitrary file write in the transcoder settings: a shared library can be planted where Plex will load it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the last one again, because it is the pattern worth carrying to every other service you run: &lt;strong&gt;a process that writes files, and later loads code from the directory it writes to, is one file-write bug away from code execution.&lt;/strong&gt; Codecs, plugins, themes, extensions, uploads with an include path. The individual scores are medium. The chain is 9.8.&lt;/p&gt;

&lt;p&gt;Exploitation needs network access to TCP port 32400. Plex's Remote Access feature, or a manual port forward, is what turns a living-room service into an internet-facing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do these three things first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Check the full build number, not the marketing version.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian or Ubuntu package install&lt;/span&gt;
dpkg &lt;span class="nt"&gt;-s&lt;/span&gt; plexmediaserver | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^Version'&lt;/span&gt;
&lt;span class="c"&gt;# official Docker image&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;plex dpkg &lt;span class="nt"&gt;-s&lt;/span&gt; plexmediaserver | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^Version'&lt;/span&gt;
&lt;span class="c"&gt;# any install, from the box itself: the identity endpoint answers without a token&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:32400/identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything below &lt;code&gt;1.43.3.10861&lt;/code&gt; is affected. An early 1.43.3 build is still affected: the last five digits are the ones that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Update, then confirm the running process is the new one.&lt;/strong&gt; A container that was never re-pulled, or a package held back by a pin, will happily report a new version in the UI while the old binary is still serving.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull plex &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; plex
docker &lt;span class="nb"&gt;exec &lt;/span&gt;plex dpkg &lt;span class="nt"&gt;-s&lt;/span&gt; plexmediaserver | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^Version'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. If the server was internet-reachable on an old build, assume it was reached.&lt;/strong&gt; Plex's own advisory says to treat such servers as potentially compromised, and gives indicators: unfamiliar &lt;code&gt;.so&lt;/code&gt; or &lt;code&gt;.dll&lt;/code&gt; files in the Codecs directory, and outbound connections to &lt;code&gt;proxystream.st&lt;/code&gt;. Check both before you call it done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# unexpected shared libraries in the codec directory&lt;/span&gt;
find ~/Library/Application&lt;span class="se"&gt;\ &lt;/span&gt;Support/Plex&lt;span class="se"&gt;\ &lt;/span&gt;Media&lt;span class="se"&gt;\ &lt;/span&gt;Server/Codecs &lt;span class="se"&gt;\&lt;/span&gt;
     /var/lib/plexmediaserver/Library/Application&lt;span class="se"&gt;\ &lt;/span&gt;Support/Plex&lt;span class="se"&gt;\ &lt;/span&gt;Media&lt;span class="se"&gt;\ &lt;/span&gt;Server/Codecs &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.so'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.dll'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt;

&lt;span class="c"&gt;# anything the box is talking to that you cannot name&lt;/span&gt;
ss &lt;span class="nt"&gt;-tn&lt;/span&gt; state established | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1{print $4}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cryptominer announces itself in CPU. Proxyware does not: it sells your address as residential bandwidth and stays quiet. Absence of load is not absence of compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist that would have saved you anyway
&lt;/h2&gt;

&lt;p&gt;None of the following requires knowing about a CVE in advance. That is the entire point: these controls pay off during the weeks when you have not patched yet, which is every service you run, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not port-forward a service to the internet to use it yourself.&lt;/strong&gt; Remote Access and a forwarded port exist to serve people who are not on your network. If the only remote user is you, a WireGuard tunnel or a mesh VPN removes the attack surface completely rather than defending it. Exposure is a decision, and most of the time nobody ever makes it deliberately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default-deny inbound, with the open ports written down.&lt;/strong&gt; On a box that runs several things, the firewall is the only place where "what is reachable" is answerable in one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw default deny incoming
ufw allow 443/tcp
ufw status numbered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the output top to bottom and ask what matches first. On a legacy box we audited this month, a careful three-subnet allowlist sat four lines below a blanket &lt;code&gt;ACCEPT&lt;/code&gt; for the same port from &lt;code&gt;0.0.0.0/0&lt;/code&gt;, which meant it had never restricted anything. &lt;a href="https://news.rozoom.app/self-hosting/inherited-server-security-audit/" rel="noopener noreferrer"&gt;The full audit is here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put HTTP services behind one reverse proxy, never directly on the internet.&lt;/strong&gt; One TLS configuration, one access log, one place to add authentication. The pattern we run on this very site, in full, is in &lt;a href="https://news.rozoom.app/self-hosting/caddy-docker-production/" rel="noopener noreferrer"&gt;Caddy and Docker in production&lt;/a&gt;. The short version for a service that needs no public access at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;media.example.com {
    @notlocal not remote_ip 10.0.0.0/8 100.64.0.0/10 192.168.0.0/16
    respond @notlocal 403
    reverse_proxy 127.0.0.1:32400
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bind the service to localhost and let the proxy be the only door.&lt;/strong&gt; A container published as &lt;code&gt;-p 32400:32400&lt;/code&gt; is on every interface the host owns, including the public one; &lt;code&gt;-p 127.0.0.1:32400:32400&lt;/code&gt; is not. This single character difference is the most common accidental exposure we find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat outbound traffic as something you are allowed to care about.&lt;/strong&gt; Both observed payloads here, the miner and the proxyware, are useless without egress. A home or small-office box has no business opening arbitrary outbound connections, and a baseline you can diff is worth more than any IOC list published after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know how you would rebuild, and have tested it.&lt;/strong&gt; After this class of compromise the honest advice is a clean install, which is only survivable if your data is somewhere else and restoring is something you have done before, not something you plan to learn under pressure. Where that somewhere else should be, with real prices on real data, is in our &lt;a href="https://news.rozoom.app/cloud/best-backup-storage-for-self-hosters-2026/" rel="noopener noreferrer"&gt;backup storage comparison&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscribe to the release notes of anything you expose.&lt;/strong&gt; Eleven weeks is not an unlucky interval, it is an unread mailing list. For a self-hosted service the vendor has no way to reach you, and no obligation to try.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do not know
&lt;/h2&gt;

&lt;p&gt;We have not seen the inside of a compromised host ourselves, and we are not going to pretend otherwise. The indicators above are Plex's, the exposure counts are Shadowserver's, the CVSS numbers are the researcher's, and the exploitation dates come from the vendors and CISA. Our contribution is the part we can stand behind: what the pattern means for the rest of your stack, and the configuration we actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which Plex version fixes Plex2Shell?&lt;/strong&gt;&lt;br&gt;
1.43.3.10861 and later, released on 4 August 2026. Builds before that are affected, including earlier 1.43.3 builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is my Plex server at risk if it is not exposed to the internet?&lt;/strong&gt;&lt;br&gt;
The chain needs network access to TCP port 32400. A server reachable only from your LAN is still exploitable from that LAN, so a compromised laptop or an untrusted guest device is enough. It is a smaller risk, not a theoretical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if my Plex server was already compromised?&lt;/strong&gt;&lt;br&gt;
Plex's advisory points at two indicators: unfamiliar &lt;code&gt;.so&lt;/code&gt; or &lt;code&gt;.dll&lt;/code&gt; files in the Codecs directory, and outbound connections to &lt;code&gt;proxystream.st&lt;/code&gt;. Observed payloads have been cryptominers and proxy malware. If the server was internet-facing on an old build, the safe assumption is compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does updating remove an existing compromise?&lt;/strong&gt;&lt;br&gt;
No. Patching closes the door; it does not remove anything already inside. After a confirmed compromise the only clean path is a rebuild from known-good media plus a restore of data you can verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I just turn off Plex Remote Access?&lt;/strong&gt;&lt;br&gt;
If the only person streaming from outside your home is you, yes, and reach it over a VPN instead. Remote Access is the feature that puts port 32400 on the internet, and the 36,000 exposed servers counted in September were mostly not deliberate decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If this prompted a look at the rest of your stack, the &lt;a href="https://news.rozoom.app/self-hosting/inherited-server-security-audit/" rel="noopener noreferrer"&gt;one-hour audit for a server you inherited&lt;/a&gt; is the checklist we use on an unknown box, and the &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;VPS comparison&lt;/a&gt; covers where to put a service you would rather not run at home at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/plex-exploited-hardening-checklist/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>security</category>
      <category>plex</category>
      <category>firewall</category>
    </item>
    <item>
      <title>Best VPS for Self-Hosting in 2026: Hetzner vs DigitalOcean vs Vultr</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:20:01 +0000</pubDate>
      <link>https://dev.to/rozoomnews/best-vps-for-self-hosting-in-2026-hetzner-vs-digitalocean-vs-vultr-4op6</link>
      <guid>https://dev.to/rozoomnews/best-vps-for-self-hosting-in-2026-hetzner-vs-digitalocean-vs-vultr-4op6</guid>
      <description>&lt;p&gt;We queried the Hetzner API twice in one day while writing this. At midday, the cheap plan every comparison recommends was unavailable in all three European locations. Five hours later, the same plan was orderable again in two of them. Nobody who copies a price out of another comparison can tell you that, because it is not in the price.&lt;/p&gt;

&lt;p&gt;So this one is built differently. The performance numbers come from a box we pay for and benchmarked ourselves. The prices come from the vendors' own APIs, read on the day of writing, with the plan ID next to every figure so you can check them yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner wins on price for self-hosting&lt;/strong&gt;: &lt;strong&gt;CX23&lt;/strong&gt; (x86, 2 vCPU / 4 GB / 40 GB) at €5.49/mo, about &lt;strong&gt;4x cheaper&lt;/strong&gt; than the equivalent Droplet, with 5x the included traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check availability before you plan around it.&lt;/strong&gt; CX23 is not orderable everywhere, and whether it is orderable at all changes during the day. The always-available fallback is &lt;strong&gt;CAX11&lt;/strong&gt; (Arm, €5.99/mo); the x86 fallback is &lt;strong&gt;CPX22 at €19.49/mo&lt;/strong&gt;, which is only about 20% under DigitalOcean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean&lt;/strong&gt; ($24/mo) buys you the best API and docs, per-second billing since January 2026, and 4 TB of transfer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vultr&lt;/strong&gt; ($20/mo) buys you reach: 31 locations, plus a genuinely free 512 MB tier most comparisons claim doesn't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Live prices, read today
&lt;/h2&gt;

&lt;p&gt;The 2 vCPU / 4 GB tier is where most self-hosters land: a few containers, a database, a reverse proxy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;vCPU / RAM / disk&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Included traffic&lt;/th&gt;
&lt;th&gt;Over-usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner (x86)&lt;/td&gt;
&lt;td&gt;CX23&lt;/td&gt;
&lt;td&gt;2 / 4 GB / 40 GB&lt;/td&gt;
&lt;td&gt;€5.49/mo&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;€1.00/TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner (Arm)&lt;/td&gt;
&lt;td&gt;CAX11&lt;/td&gt;
&lt;td&gt;2 / 4 GB / 40 GB&lt;/td&gt;
&lt;td&gt;€5.99/mo&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;€1.00/TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner (x86)&lt;/td&gt;
&lt;td&gt;CPX22&lt;/td&gt;
&lt;td&gt;2 / 4 GB / 80 GB&lt;/td&gt;
&lt;td&gt;€19.49/mo&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;€1.00/TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;2 / 4 GB / 80 GB&lt;/td&gt;
&lt;td&gt;$24.00/mo&lt;/td&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;~$0.01/GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vultr&lt;/td&gt;
&lt;td&gt;vc2-2c-4gb&lt;/td&gt;
&lt;td&gt;2 / 4 GB / 80 GB&lt;/td&gt;
&lt;td&gt;$20.00/mo&lt;/td&gt;
&lt;td&gt;3 TB&lt;/td&gt;
&lt;td&gt;~$0.01/GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hetzner bills a public IPv4 separately at €0.50/mo. IPv6 is free. Add it to the Hetzner rows if you need a v4 address, which you probably do.&lt;/p&gt;

&lt;p&gt;Prices change. Every number above was read from the vendors' own pricing APIs on the date of this post, not copied from another comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  The availability trap nobody mentions
&lt;/h2&gt;

&lt;p&gt;Search for a VPS recommendation and you will be told to buy the cheapest Hetzner CX box. The price is right. Whether you can buy it is a different question, and the answer changes.&lt;/p&gt;

&lt;p&gt;Here are our two reads of the Hetzner API, the same endpoint, the same day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Midday.&lt;/strong&gt; CX23 (2 vCPU / 4 GB / 40 GB, €5.49) listed a price for Falkenstein, Nuremberg and Helsinki, and an &lt;strong&gt;empty availability list&lt;/strong&gt;. Price visible, server not orderable anywhere in Europe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five hours later.&lt;/strong&gt; The same plan came back as orderable in Nuremberg and Helsinki. Falkenstein still not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither read was wrong. Capacity moves. What matters is that the number everyone quotes is not the number that tells you whether you can deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;prices[]&lt;/code&gt; array still lists locations you cannot order in, at their old prices. It is not an availability signal.&lt;/li&gt;
&lt;li&gt;The signal is &lt;code&gt;server_types[].locations[].available&lt;/code&gt;, per location, and it is live.&lt;/li&gt;
&lt;li&gt;If you order anyway, the API fails with &lt;code&gt;invalid_input: unsupported location for server type&lt;/code&gt;, which reads like you passed a bad region rather than like the plan is gone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /v1/datacenters&lt;/code&gt; was removed in June 2026 and returns HTTP 410, so there is no second place to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical advice: check availability for your target location immediately before you order, try a second European location before giving up, and keep a fallback plan in mind. Today that fallback is CAX11, which has been orderable in all three locations every time we looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually matters when you self-host
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Egress is the silent budget killer
&lt;/h3&gt;

&lt;p&gt;Nextcloud, photo sync, backups: traffic adds up faster than compute. This is where the three split hardest.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner&lt;/strong&gt; includes &lt;strong&gt;20 TB&lt;/strong&gt; on every Cloud plan, then bills &lt;strong&gt;€1.00/TB&lt;/strong&gt;, in 100 MB blocks. Not free beyond the cap - but ten times cheaper than the other two, and the cap is high enough that most self-hosters never reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean&lt;/strong&gt; includes 0.5-6 TB depending on plan (4 TB at the tier above), then about &lt;strong&gt;$0.01/GB&lt;/strong&gt; - roughly &lt;strong&gt;$10/TB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vultr&lt;/strong&gt; includes 0.5-5 TB (3 TB at this tier), then about &lt;strong&gt;$0.01/GB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hetzner notifies you at 75% and 100% of the included traffic. Those are notifications, not a cap: usage keeps billing either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. vCPU is marketing, not performance
&lt;/h3&gt;

&lt;p&gt;All three sell "vCPU", but a shared Hetzner core and a dedicated DigitalOcean core are not the same unit. The only way to know is to measure - which we did, for the one box we own.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Snapshots and backups are not free anywhere
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner&lt;/strong&gt;: snapshots at &lt;strong&gt;€0.0143/GB per month&lt;/strong&gt; on the &lt;em&gt;compressed&lt;/em&gt; size; backups are a flat &lt;strong&gt;20% of the server price&lt;/strong&gt; and give you 7 slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean&lt;/strong&gt;: snapshots &lt;strong&gt;$0.06/GB/month&lt;/strong&gt;; backups 20% (weekly) or 30% (daily) of Droplet cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vultr&lt;/strong&gt;: snapshots billed per GB of stored data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody in this bracket gives you free snapshot storage. Budget it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark: a real Hetzner box, measured
&lt;/h2&gt;

&lt;p&gt;We took a Hetzner Cloud server we run in production - 2 vCPU shared x86 (Intel Xeon Skylake), 4 GB RAM, 40 GB SSD, Nuremberg - and ran standard, reproducible tests. These numbers are ours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU&lt;/strong&gt; (OpenSSL speed, single thread):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AES-256-CBC (16 KB blocks)&lt;/td&gt;
&lt;td&gt;~630 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-256 (16 KB blocks)&lt;/td&gt;
&lt;td&gt;~255 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Disk&lt;/strong&gt; (40 GB SSD):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sequential write (fdatasync)&lt;/td&gt;
&lt;td&gt;468 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential read, cold&lt;/td&gt;
&lt;td&gt;1.1 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential read, warm (cached)&lt;/td&gt;
&lt;td&gt;6.0 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Network&lt;/strong&gt; (single stream):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Download from OVH mirror&lt;/td&gt;
&lt;td&gt;175 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Download from Ubuntu mirror&lt;/td&gt;
&lt;td&gt;82 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The box sits on a 1 Gbit/s port with IPv6. For a shared-vCPU plan at this price the disk is the quiet star: 468 MB/s sustained write is plenty for a database and a media stack on the same host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three honest caveats.&lt;/strong&gt; This is a CX-line x86 box, the same 2 vCPU / 4 GB / 40 GB spec as the CX23 we recommend, but not the Arm CAX11. We do not rent from DigitalOcean or Vultr, so their figures here are vendor-published, not measured. And a single-thread OpenSSL run on a shared core is a floor, not a guarantee: noisy neighbours are the trade-off of the shared tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the prices actually compare
&lt;/h2&gt;

&lt;p&gt;Against DigitalOcean's $24 for 2 vCPU / 4 GB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CX23 (x86) at €5.49&lt;/strong&gt; - roughly &lt;strong&gt;4x cheaper&lt;/strong&gt;, with 5x the included traffic. Half the disk (40 GB vs 80 GB), and subject to the availability caveat above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAX11 (Arm) at €5.99&lt;/strong&gt; - the same gap, always orderable so far, if your stack runs on arm64. Most upstream container images ship arm64 builds now; check the ones you depend on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPX22 (x86) at €19.49&lt;/strong&gt; - about &lt;strong&gt;20% cheaper&lt;/strong&gt;, with 5x the traffic and the same 80 GB disk. This is your number if CX23 is unavailable and you need x86.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is the one missing from every comparison we read. The Hetzner price advantage is 4x when the cheap line is available and roughly a fifth off when it is not, and which of those you get depends on the hour you sign up. Add €0.50/mo for IPv4 to any of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to pick which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Hetzner if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're in or near Europe, or your users are.&lt;/li&gt;
&lt;li&gt;You run Nextcloud, a media stack, or backups - anything traffic-heavy.&lt;/li&gt;
&lt;li&gt;You want the most compute per euro, and you can check plan availability before you commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick DigitalOcean if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the smoothest API and the best docs.&lt;/li&gt;
&lt;li&gt;You need per-second billing for short-lived workloads.&lt;/li&gt;
&lt;li&gt;You want one-click apps instead of configuring a stack by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Vultr if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need a region the others don't cover - 31 locations against Hetzner's five.&lt;/li&gt;
&lt;li&gt;You want to test on the free tier before paying.&lt;/li&gt;
&lt;li&gt;You run bursty workloads and spin instances up and down often.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our recommendation
&lt;/h2&gt;

&lt;p&gt;For European self-hosting, &lt;strong&gt;Hetzner Cloud&lt;/strong&gt; is the pick, and the benchmark above shows the cheap shared tier is not slow. Order &lt;strong&gt;CX23&lt;/strong&gt; at €5.49/mo if it is available in your location; take &lt;strong&gt;CAX11&lt;/strong&gt; at €5.99/mo if it is not and your stack runs on Arm; fall back to &lt;strong&gt;CPX22&lt;/strong&gt; at €19.49/mo if you need x86 either way. Check availability first, in that order, and the decision takes a minute.&lt;/p&gt;

&lt;p&gt;If your users are in the US or Asia, DigitalOcean and Vultr are both solid - just model the egress before you sign up.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Hetzner good for self-hosting?&lt;/strong&gt;&lt;br&gt;
Yes, and it is still the price leader in Europe. Two caveats: the identity verification step at signup, and plan availability that changes without notice - check your target location before ordering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can't I order the cheap Hetzner plan everyone recommends?&lt;/strong&gt;&lt;br&gt;
Because CX-line availability varies by location and over time, while the price stays visible on comparison sites and in the API's own price list. Availability is a separate field and it flips: we saw CX23 unavailable across all of Europe and orderable in two locations five hours later. Try another European location, then fall back to CAX11.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does DigitalOcean charge for egress?&lt;/strong&gt;&lt;br&gt;
Yes. Beyond the included transfer (4 TB at the 4 GB tier) it's about $0.01/GB, roughly $10 per TB. Model this before you sign up if you serve media or move backups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Vultr have a free tier?&lt;/strong&gt;&lt;br&gt;
Yes - a 1 vCPU / 512 MB / 10 GB instance in a small number of locations, with no included bandwidth. There is also a $2.50/mo plan, but it is IPv6-only and limited to two locations. The cheapest generally useful plan is $5/mo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which VPS has the best price-to-performance?&lt;/strong&gt;&lt;br&gt;
At 2 vCPU / 4 GB, Hetzner leads on both price and included traffic by a wide margin when its cheap line (CX23, CAX11) is available. If you are pushed onto CPX22, the gap narrows to roughly 20%, which is close enough that DigitalOcean's tooling or Vultr's regions can be the better buy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I self-host on a $6 VPS?&lt;/strong&gt;&lt;br&gt;
Yes, for a reverse proxy, a couple of containers and a small database. The moment you add media or heavy backups, the thing that breaks your budget is egress, not CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is the first in a series. Coming up: &lt;strong&gt;running Nextcloud AIO on Hetzner&lt;/strong&gt; and &lt;strong&gt;Caddy + Docker in production&lt;/strong&gt; - both built on the same box we benchmarked here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>vps</category>
      <category>hetzner</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>Deploy From GitHub Actions Without Giving CI a Shell on Your Server</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:19:25 +0000</pubDate>
      <link>https://dev.to/rozoomnews/deploy-from-github-actions-without-giving-ci-a-shell-on-your-server-47bh</link>
      <guid>https://dev.to/rozoomnews/deploy-from-github-actions-without-giving-ci-a-shell-on-your-server-47bh</guid>
      <description>&lt;p&gt;Every "deploy a static site with GitHub Actions" tutorial ends the same way: generate an SSH key, paste the private half into repository secrets, rsync the build to your server. It works on the first try, which is the problem. What you just created is an interactive shell account on your server, held by a system that runs code from pull requests.&lt;/p&gt;

&lt;p&gt;This post is the version with the guard rails: a key that can do exactly one thing, a host key you pinned yourself, and three checks that stop a broken build from replacing a working site. It is the pipeline that ships this blog, and it is maybe fifteen extra minutes of work.&lt;/p&gt;

&lt;p&gt;The server side assumes you have a box. If you don't, we compared the cheap tiers in &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;Best VPS for self-hosting in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the naive deploy key actually grants
&lt;/h2&gt;

&lt;p&gt;An entry in &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; with no options is a login. Anything holding that private key can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open an interactive shell as that user,&lt;/li&gt;
&lt;li&gt;read every file that user can read, including other projects on the same box and any &lt;code&gt;.env&lt;/code&gt; lying around,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; if the user has it,&lt;/li&gt;
&lt;li&gt;forward ports and use your server as a jump host into whatever it can reach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is needed to copy a directory of HTML. And the private key lives in a CI system where a workflow change, a compromised third-party action, or a misconfigured &lt;code&gt;pull_request_target&lt;/code&gt; trigger can read it.&lt;/p&gt;

&lt;p&gt;We ran exactly this setup for months without thinking about it, which is the honest reason this post exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock the key to one command
&lt;/h2&gt;

&lt;p&gt;OpenSSH lets you attach options to each key in &lt;code&gt;authorized_keys&lt;/code&gt;. The one that matters is &lt;code&gt;command=&lt;/code&gt;, which replaces whatever the client asked to run with what you specify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;command&lt;/span&gt;="/usr/bin/rrsync -wo /home/deploy/site",restrict ssh-ed25519 AAAA... github-actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Piece by piece:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rrsync&lt;/code&gt;&lt;/strong&gt; ships with rsync (&lt;code&gt;/usr/bin/rrsync&lt;/code&gt; on Debian and Ubuntu; it may be a gzipped script under &lt;code&gt;/usr/share/doc/rsync/scripts/&lt;/code&gt; on older releases). It is a wrapper that accepts only rsync's server-side invocation and refuses everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-wo&lt;/code&gt;&lt;/strong&gt; means write-only: the key can upload into the directory and cannot download from it. If your deploy only pushes, take the stricter option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The path argument&lt;/strong&gt; is a jail. Paths outside it are rejected, including anything clever with &lt;code&gt;..&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restrict&lt;/code&gt;&lt;/strong&gt; disables port forwarding, agent forwarding, X11 and PTY allocation in one word. On any OpenSSH from the last several years, prefer it over listing &lt;code&gt;no-port-forwarding,no-agent-forwarding,...&lt;/code&gt; by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test it the direct way, from your laptop with that private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; deploy_key deploy@your-server &lt;span class="s1"&gt;'id'&lt;/span&gt;      &lt;span class="c"&gt;# must fail&lt;/span&gt;
rsync &lt;span class="nt"&gt;-rltz&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i deploy_key"&lt;/span&gt; ./public/ deploy@your-server:/   &lt;span class="c"&gt;# must succeed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command failing is the whole point. If it prints a user id, the options are not being applied - check that the whole thing is on one line and that the line starts with &lt;code&gt;command=&lt;/code&gt;, not with the key type.&lt;/p&gt;

&lt;p&gt;Note the destination path in the rsync call: it is relative to the jail, so &lt;code&gt;/&lt;/code&gt; means the directory you locked the key into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the host key, don't disable the check
&lt;/h2&gt;

&lt;p&gt;The other half of the tutorials' shortcut is &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt;, which turns off the check that the server you are shipping to is the server you think it is. Pin it instead. On the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keyscan &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 your-server.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put that line into a repository secret and write it to &lt;code&gt;known_hosts&lt;/code&gt; in the job. Now a changed host key fails the deploy loudly instead of silently shipping your site to whoever answers on that address.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;submodules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;recursive&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-build-command&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Guard against an empty build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
          &lt;span class="s"&gt;test -s public/index.html&lt;/span&gt;
          &lt;span class="s"&gt;test -s public/sitemap.xml&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy over rsync&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_SSH_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_KNOWN_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_KNOWN_HOSTS }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;printf '%s\n' "$DEPLOY_SSH_KEY" &amp;gt; ~/.ssh/deploy_key&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/deploy_key&lt;/span&gt;
          &lt;span class="s"&gt;printf '%s\n' "$DEPLOY_KNOWN_HOSTS" &amp;gt; ~/.ssh/known_hosts&lt;/span&gt;
          &lt;span class="s"&gt;rsync -rltz --delete --omit-dir-times \&lt;/span&gt;
            &lt;span class="s"&gt;-e "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o UserKnownHostsFile=~/.ssh/known_hosts" \&lt;/span&gt;
            &lt;span class="s"&gt;public/ "$DEPLOY_USER@$DEPLOY_HOST:/"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smoke test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
          &lt;span class="s"&gt;for url in https://example.com/ https://example.com/sitemap.xml; do&lt;/span&gt;
            &lt;span class="s"&gt;code=$(curl -s -o /dev/null -w '%{http_code}' --retry 3 --retry-delay 5 "$url")&lt;/span&gt;
            &lt;span class="s"&gt;echo "$url -&amp;gt; $code"&lt;/span&gt;
            &lt;span class="s"&gt;test "$code" = "200"&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parts that are not decoration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;concurrency&lt;/code&gt; without &lt;code&gt;cancel-in-progress&lt;/code&gt;.&lt;/strong&gt; Two deploys running at once against the same directory with &lt;code&gt;--delete&lt;/code&gt; is how you serve a half-copied site. Queueing them costs nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The empty-build guard.&lt;/strong&gt; &lt;code&gt;--delete&lt;/code&gt; is what makes rsync a deploy rather than an upload, and it is also what turns a build that silently produced nothing into a deleted website. Three &lt;code&gt;test -s&lt;/code&gt; lines are cheaper than that outage. Assert on files that must exist and on at least one that only appears when the build really ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;IdentitiesOnly=yes&lt;/code&gt;.&lt;/strong&gt; Without it, ssh may offer other keys from the agent first and get refused before it reaches yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--omit-dir-times&lt;/code&gt;.&lt;/strong&gt; With a write-only jail, rsync's attempts to set directory timestamps can fail noisily. This keeps the output clean without weakening anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The smoke test runs against the public URL&lt;/strong&gt;, not against the server over ssh. It is the only step that proves a visitor can see the result: it catches a dead container, an expired certificate and a proxy pointing at the wrong directory, none of which rsync knows about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and does not protect you from
&lt;/h2&gt;

&lt;p&gt;It does protect you from the realistic failure: a leaked CI secret turning into shell access on your server. The key can write one directory and nothing else, cannot read back, cannot forward, cannot open a terminal.&lt;/p&gt;

&lt;p&gt;It does not protect you from someone who can change the workflow file, because they can replace the content you ship. If the site is the thing you care about, protect the branch: require review, and keep the deploy on &lt;code&gt;push&lt;/code&gt; to the default branch only - never on &lt;code&gt;pull_request_target&lt;/code&gt;, which runs with your secrets available to a fork's code.&lt;/p&gt;

&lt;p&gt;It also does not protect against the directory itself. Whatever is served from that path can be overwritten by the key. Keep the deploy target separate from config, data volumes and anything else the server holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrofit, in order
&lt;/h2&gt;

&lt;p&gt;If you already have an unrestricted deploy key, fixing it takes a few minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm &lt;code&gt;rrsync&lt;/code&gt; exists on the server: &lt;code&gt;ls -l /usr/bin/rrsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;authorized_keys&lt;/code&gt; and add &lt;code&gt;command="/usr/bin/rrsync -wo /path/to/site",restrict&lt;/code&gt; in front of the existing key line.&lt;/li&gt;
&lt;li&gt;From your laptop with that key, check that &lt;code&gt;ssh ... 'id'&lt;/code&gt; fails and a test rsync still succeeds.&lt;/li&gt;
&lt;li&gt;Re-run the workflow with &lt;code&gt;workflow_dispatch&lt;/code&gt; and watch the smoke test pass.&lt;/li&gt;
&lt;li&gt;Only then delete the backup of the old line.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rotating the key at the same time is a reasonable habit, especially if it was ever printed in a log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The server side of the same setup: &lt;a href="https://news.rozoom.app/self-hosting/caddy-docker-production/" rel="noopener noreferrer"&gt;Caddy and Docker in production&lt;/a&gt; - automatic HTTPS, several sites on one box, and the cache headers that decide whether your readers see the page you just published.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/github-actions-rsync-deploy-no-shell/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>githubactions</category>
      <category>ssh</category>
      <category>deployment</category>
    </item>
    <item>
      <title>The One-Hour Audit for a Server You Inherited</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:18:49 +0000</pubDate>
      <link>https://dev.to/rozoomnews/the-one-hour-audit-for-a-server-you-inherited-2f2l</link>
      <guid>https://dev.to/rozoomnews/the-one-hour-audit-for-a-server-you-inherited-2f2l</guid>
      <description>&lt;p&gt;Somebody hands you the root password to a machine that has been running for years. It serves real sites, nobody remembers how it was set up, and the person who did set it up is gone.&lt;/p&gt;

&lt;p&gt;The temptation is to do the big thing first: rebuild it, migrate it, containerise it. Do the small thing first. One hour, five questions, and you will know whether the machine is an inconvenience or an emergency.&lt;/p&gt;

&lt;p&gt;This is the audit we ran on exactly such a box last week, with what it actually turned up. Every finding below is real, and not one of them was visible from the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What is listening, and what does the firewall think?
&lt;/h2&gt;

&lt;p&gt;These two answers are always compared, never read separately. The first is the truth about the machine; the second is what somebody once intended.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;                     &lt;span class="c"&gt;# every listening TCP socket, with the process&lt;/span&gt;
iptables &lt;span class="nt"&gt;-L&lt;/span&gt; INPUT &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--line-numbers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our finding, and it is the one to steal from this article: the firewall had a careful allowlist for the admin panel port, three trusted subnets, written by somebody who knew what they were doing. Four lines &lt;strong&gt;above&lt;/strong&gt; it sat a blanket &lt;code&gt;ACCEPT&lt;/code&gt; for the same port from &lt;code&gt;0.0.0.0/0&lt;/code&gt;, added later.&lt;/p&gt;

&lt;p&gt;iptables evaluates in order and stops at the first match. The allowlist had never, from the moment that line was added, allowed anything less than everybody. The admin login had been open to the internet for years behind a rule that read, to a casual glance, like a restriction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read your rules top to bottom, with line numbers, and ask what matches first.&lt;/strong&gt; A rule list is a program, not a set.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Who is actually using the thing you are about to close?
&lt;/h2&gt;

&lt;p&gt;The audit's single most useful habit. Before closing any path, read its access log and count real users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# top talkers against one path, last 10k lines&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s1"&gt;'phpmyadmin'&lt;/span&gt; /var/log/nginx/&lt;span class="k"&gt;*&lt;/span&gt;access&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of our findings came straight out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;phpMyAdmin from 2017&lt;/strong&gt;, reachable from the internet, on a web server whose PHP had no upstream patches left. The log showed 201 requests from one address and 38 from another, both scanners walking a wordlist. Not one human session in the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;proftpd running&lt;/strong&gt;, in plaintext, no TLS. &lt;code&gt;xferlog&lt;/code&gt; was empty since June. The only connections were &lt;code&gt;anonymous&lt;/code&gt; login attempts from a botnet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both were closed in minutes, with no risk of breaking anyone's workflow, because the logs proved there was no workflow. Had the logs shown daily sessions from one office IP, the answer would have been an allowlist and a conversation rather than a shutdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule: never close a service on the belief that nobody uses it. Close it on evidence that nobody uses it.&lt;/strong&gt; Those feel the same right up until the invoicing department calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Do not lock yourself out
&lt;/h2&gt;

&lt;p&gt;Everything in this section is firewall work on a machine you reach only through that firewall. Two precautions, both cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arm an auto-revert before you commit anything.&lt;/strong&gt; A timer that restores the known-good ruleset in ten minutes, cancelled by hand once you have confirmed you can still get in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables-save &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/iptables.good
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'iptables-restore &amp;lt; /root/iptables.good'&lt;/span&gt; | at now + 10 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply your change, open a &lt;strong&gt;brand new SSH session&lt;/strong&gt; from somewhere else, and only then cancel the job with &lt;code&gt;atrm&lt;/code&gt;. Testing on the session you already have proves nothing: an established connection survives rules that block new ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then make it survive a reboot,&lt;/strong&gt; which is a separate step and the one people forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;service iptables save          &lt;span class="c"&gt;# or: netfilter-persistent save&lt;/span&gt;
systemctl is-enabled iptables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ours rebooted on a weekly schedule we did not know about. The hardening was still there afterwards, because the rules had been saved, and we knew it was still there because we checked after the reboot rather than assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What renews itself, and what only appears to?
&lt;/h2&gt;

&lt;p&gt;The quiet killer. On this box, TLS renewal was a job inside a control panel whose licence had expired two months earlier. The renewal cron died with the licence. Four sites had already been failing their TLS handshake for five days before the audit found it, and seven more were scheduled to expire within three weeks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# every scheduled job on the box, in one pass&lt;/span&gt;
crontab &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /etc/cron.&lt;span class="k"&gt;*&lt;/span&gt;/ /etc/cron.d/&lt;span class="p"&gt;;&lt;/span&gt; systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read all of it, including the commented lines, because they tell you what used to run. And look for duplicates: we disabled one renewal cron and only on a second pass found a &lt;strong&gt;second one&lt;/strong&gt;, same job, different hour, four lines further down.&lt;/p&gt;

&lt;p&gt;Taking renewal back is a short job even on an end-of-life distro - we wrote it up in &lt;a href="https://news.rozoom.app/self-hosting/acme-sh-certificates-legacy-server/" rel="noopener noreferrer"&gt;acme.sh on a server where certbot will not install&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What is installed that nothing uses?
&lt;/h2&gt;

&lt;p&gt;Legacy boxes accumulate runtimes. Ours had eleven separate PHP builds, going back to 5.2. The interesting question is not what is installed but what is &lt;strong&gt;wired up&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="s1"&gt;'php[0-9]+'&lt;/span&gt; /etc/nginx/conf.d/&lt;span class="k"&gt;*&lt;/span&gt;.conf /etc/httpd/conf.d/&lt;span class="k"&gt;*&lt;/span&gt;.conf &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight sites were on 7.4, three on 8.0, and every build older than that was dead weight. That single command changed the migration plan: a machine that looks like it needs a hosting platform supporting PHP 5.2 actually needs 7.4 and 8.x, which is to say anything modern.&lt;/p&gt;

&lt;p&gt;An audit is not only about what to close. It is about discovering that the scary part of the job does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an hour bought
&lt;/h2&gt;

&lt;p&gt;Four findings on one machine: an allowlist that allowed everyone, a nine-year-old admin tool exposed to scanners, an FTP daemon serving nobody, and certificate renewal that had been dead for two months with four sites already down.&lt;/p&gt;

&lt;p&gt;None of it required a rebuild. None of it required a maintenance window. Three of the four were fixed the same hour, and the fourth - the distro itself being past end of life - is the one honest finding you cannot patch, which is exactly why it belongs in the migration plan rather than in today's work.&lt;/p&gt;

&lt;p&gt;If the audit sends you shopping for a replacement box, we compared the options with live prices in &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;Best VPS for self-hosting in 2026&lt;/a&gt;, and wrote up four years on one provider in &lt;a href="https://news.rozoom.app/cloud/hetzner-cloud-review-2026/" rel="noopener noreferrer"&gt;our Hetzner Cloud review&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/inherited-server-security-audit/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>security</category>
      <category>iptables</category>
      <category>audit</category>
    </item>
    <item>
      <title>Hetzner Cloud Review 2026: Four Years, Three Servers, and Every Bill Explained</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:18:13 +0000</pubDate>
      <link>https://dev.to/rozoomnews/hetzner-cloud-review-2026-four-years-three-servers-and-every-bill-explained-26kn</link>
      <guid>https://dev.to/rozoomnews/hetzner-cloud-review-2026-four-years-three-servers-and-every-bill-explained-26kn</guid>
      <description>&lt;p&gt;The oldest server in our account was created on 29 September 2022 and is still running. It has never been migrated, rebuilt or restored. Two more boxes joined it since, the newest three weeks ago. That is the entire basis of this review: three machines, four years, one invoice a month, and a list of things that went wrong that is almost entirely our own fault.&lt;/p&gt;

&lt;p&gt;Most Hetzner reviews are a pricing table with adjectives. This one is the bill, itemised, plus the parts of the bill that are easy to miss, and the honest account of what we broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually run
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Box&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Created&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CX32, 4 vCPU / 8 GB&lt;/td&gt;
&lt;td&gt;Sep 2022&lt;/td&gt;
&lt;td&gt;Legacy multi-site host, eleven small sites, mail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CX22, 2 vCPU / 4 GB&lt;/td&gt;
&lt;td&gt;Nov 2023&lt;/td&gt;
&lt;td&gt;Three containerised storefronts plus three CI runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CX23, 2 vCPU / 4 GB&lt;/td&gt;
&lt;td&gt;Oct 2026&lt;/td&gt;
&lt;td&gt;Static blog, analytics, a relay service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No load balancers, no managed anything, no Kubernetes. Three Linux boxes, Docker, and a reverse proxy. Four years of uptime that we did not have to think about is the headline finding, and it is worth saying clearly before the complaints start: in four years, not one of our outages was caused by Hetzner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill, every line
&lt;/h2&gt;

&lt;p&gt;These figures come from the Hetzner pricing API, read the day this was published, in EUR excluding VAT.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CX23, 2 vCPU / 4 GB / 40 GB&lt;/td&gt;
&lt;td&gt;€5.49/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAX11, Arm, 2 vCPU / 4 GB / 40 GB&lt;/td&gt;
&lt;td&gt;€5.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPX22, 2 vCPU / 4 GB / 80 GB&lt;/td&gt;
&lt;td&gt;€19.49/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary IPv4 address&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€0.50/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPv6&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Included traffic, EU locations&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 TB/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic over the allowance&lt;/td&gt;
&lt;td&gt;€1.00/TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated backups&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+20% of the server price&lt;/strong&gt;, 7 slots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshots and custom images&lt;/td&gt;
&lt;td&gt;€0.0143/GB/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block storage volumes&lt;/td&gt;
&lt;td&gt;€0.0572/GB/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Floating IP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€3.00/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those lines deserve a paragraph each, because they are the two that quietly take your money.&lt;/p&gt;

&lt;h3&gt;
  
  
  The floating IP line
&lt;/h3&gt;

&lt;p&gt;A floating IP costs &lt;strong&gt;€3.00 a month, six times the price of the primary IPv4 on the server&lt;/strong&gt;, and it is billed whether or not anything is attached to it. We found two of them on our account left over from a migration that had finished months earlier, routing nothing. Deleting them cut €6 a month, which on a fleet whose cheapest box is €5.49 is a whole extra server.&lt;/p&gt;

&lt;p&gt;Go and look at your floating IPs now. If one is unattached, it is not a spare, it is a subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  The traffic line is genuinely generous
&lt;/h3&gt;

&lt;p&gt;20 TB per month included, per server, in the European locations, and €1.00 per TB beyond it. For comparison, the equivalent allowance at the big US clouds is measured in hundreds of gigabytes and the overage is priced per gigabyte, typically around €0.09. At a terabyte of egress a month, the difference is between a rounding error and a bill that doubles.&lt;/p&gt;

&lt;p&gt;This is the single strongest reason to run anything bandwidth-shaped - media, backups, a package mirror, an analytics endpoint - on Hetzner rather than on a hyperscaler. We looked at the alternatives in &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;Best VPS for self-hosting in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong in four years
&lt;/h2&gt;

&lt;p&gt;Every incident below was ours. We are listing them because they are the ones you will hit too, and none of them appear in a feature comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The images ship with no swap
&lt;/h3&gt;

&lt;p&gt;A CI runner on box 2 went offline and stayed offline for &lt;strong&gt;ten days&lt;/strong&gt;. The symptom read like an unreachable machine. The cause was a build that allocated more than the 4 GB available, the kernel OOM-killer taking the runner process, and systemd not retrying. The box had been up for 85 days with &lt;strong&gt;zero swap&lt;/strong&gt;, because the stock image does not configure any.&lt;/p&gt;

&lt;p&gt;A 4 GB swapfile in &lt;code&gt;/etc/fstab&lt;/code&gt; fixed it permanently, and the same build then completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 4G /swapfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;600 /swapfile
mkswap /swapfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/swapfile none swap sw 0 0'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run builds on a 4 GB box, do this on day one. Treat "service has been down for days" on a Hetzner box as a possible OOM before you treat it as a network problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Docker build cache ate two thirds of the disk
&lt;/h3&gt;

&lt;p&gt;On a 38 GB disk, 24 GB was Docker build cache. Nothing warns you; the box simply starts failing writes. &lt;code&gt;docker system prune -af --volumes&lt;/code&gt; on a schedule, or at minimum a disk alert, is not optional on a 40 GB plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. fail2ban banned our own bridge
&lt;/h3&gt;

&lt;p&gt;A host started answering 503 to everything. fail2ban had matched traffic coming from the Docker bridge, which is to say from our own reverse proxy, and banned the gateway address. Whitelist your bridge networks in &lt;code&gt;ignoreip&lt;/code&gt; before you deploy fail2ban next to Docker, not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The plan you budgeted for may not be orderable
&lt;/h3&gt;

&lt;p&gt;The CX line's availability flips through the day, per location. We read the API at midday and the €5.49 plan was unavailable across all three European locations; five hours later it was orderable in two of them. The &lt;code&gt;prices[]&lt;/code&gt; array lists locations you cannot order in, which makes a price look like an offer when it is only a record. The live signal is &lt;code&gt;server_types[].locations[].available&lt;/code&gt;, and &lt;code&gt;GET /v1/datacenters&lt;/code&gt; has returned HTTP 410 since June 2026, so there is no second source to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hetzner still does not sell
&lt;/h2&gt;

&lt;p&gt;As of this month's changelog, and this is the honest reason not to choose them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No managed database.&lt;/strong&gt; No equivalent of RDS or managed Postgres. You run the database, you handle the failover, you own the backups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No managed Kubernetes.&lt;/strong&gt; There is an official cloud controller manager and a CSI driver, which make a self-managed cluster pleasant, but the control plane is yours to operate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin managed everything else.&lt;/strong&gt; Object storage and storage boxes exist; load balancers, networks and firewalls exist and work. Beyond that the catalogue is deliberately short, and 2026 added features, not products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer regions.&lt;/strong&gt; Three European locations plus a handful elsewhere. If your users are in Asia or South America, latency is a real argument against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support is a ticket, not a phone call.&lt;/strong&gt; In four years we have opened almost none, which is its own kind of answer, but do not expect an account manager.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backups: the checkbox we leave off, and why
&lt;/h2&gt;

&lt;p&gt;Hetzner's automated backups cost &lt;strong&gt;20% of the server price&lt;/strong&gt; and keep 7 slots. On a €5.49 box that is €1.10 a month, which is cheap enough that the default advice should be to turn it on.&lt;/p&gt;

&lt;p&gt;We leave it off, and the reasoning is worth copying only if the second half applies to you: snapshots of a whole server restore a whole server, which is the wrong unit when what you need back is one database and one config directory. Our backups go to S3-compatible object storage on a schedule, versioned, and they get restore-tested. If you are &lt;strong&gt;not&lt;/strong&gt; going to build that, turn the checkbox on today. An untested €1.10 backup beats a disciplined plan you never wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should use Hetzner, and who should not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use it if&lt;/strong&gt; you run your own stack, your traffic is bandwidth-heavy, your users are in Europe, and you would rather spend an hour on &lt;code&gt;apt&lt;/code&gt; than €200 a month on managed services. The price-to-performance is not a marginal win, it is a factor of three or four, and the hardware has been boring in the best sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use it if&lt;/strong&gt; you need a managed database with a support contract behind it, a control plane somebody else gets paged for, or a region in Asia. The money you save will go straight back out as your own time, and if your time is the scarce resource, that trade is bad.&lt;/p&gt;

&lt;p&gt;For us, across four years and three machines, the verdict is simple: the infrastructure has never been the problem. Everything on the incident list above we did to ourselves, and every one of those mistakes would have cost more to make somewhere else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/cloud/hetzner-cloud-review-2026/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>hetzner</category>
      <category>vps</category>
      <category>review</category>
    </item>
    <item>
      <title>acme.sh on a Server Where certbot Will Not Install</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:17:37 +0000</pubDate>
      <link>https://dev.to/rozoomnews/acmesh-on-a-server-where-certbot-will-not-install-f60</link>
      <guid>https://dev.to/rozoomnews/acmesh-on-a-server-where-certbot-will-not-install-f60</guid>
      <description>&lt;p&gt;Four sites on one legacy box went dark. Not slow, not 500 - dark, as in the TLS handshake failed before a request existed. They had been down for five days before anyone noticed, because nothing was watching the one thing that mattered.&lt;/p&gt;

&lt;p&gt;The cause was mundane: certificate renewal lived inside a control panel, the panel's licence expired, and its daily renewal cron died with it. Seven more certificates on the same machine were scheduled to expire over the following three weeks.&lt;/p&gt;

&lt;p&gt;The distro was years past end of life, so &lt;code&gt;certbot&lt;/code&gt; would not install cleanly and the vendor packages were long gone. This is the recipe that fixed it in under thirty minutes without touching a single vhost file, and the monitoring that makes sure it cannot happen quietly again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why acme.sh and not certbot
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;certbot&lt;/code&gt; is a Python application with a dependency tree. On a current distro that is fine. On an end-of-life one it is the whole problem: the system Python is too old for modern &lt;code&gt;cryptography&lt;/code&gt; wheels, pip wants a compiler you do not want on a production box, and the snap route needs a daemon the distro never shipped.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;acme.sh&lt;/code&gt; is &lt;strong&gt;a shell script&lt;/strong&gt;. It needs &lt;code&gt;bash&lt;/code&gt; and either &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt;. That is the entire dependency list. It talks to the ACME API over HTTPS via curl, so even an ancient OpenSSL is not in the signing path, and &lt;code&gt;bash 4.2&lt;/code&gt; is new enough.&lt;/p&gt;

&lt;p&gt;There is also a pleasant irony worth knowing if you are escaping a panel: several commercial panels use acme.sh under the hood. You are not replacing the mechanism, you are taking it out from under something that stopped running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install, and fix the default CA
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://get.acme.sh | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@yourdomain.dev
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then immediately do the step most guides skip. Since version 3, &lt;strong&gt;acme.sh defaults to ZeroSSL, not Let's Encrypt&lt;/strong&gt;, and ZeroSSL requires account registration with EAB credentials. If you wanted Let's Encrypt, say so once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--set-default-ca&lt;/span&gt; &lt;span class="nt"&gt;--server&lt;/span&gt; letsencrypt
acme.sh &lt;span class="nt"&gt;--register-account&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; you@yourdomain.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip this and your first issue attempt fails with a registration error that looks like a network problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue without touching the vhosts
&lt;/h2&gt;

&lt;p&gt;The goal is zero changes to the web server configuration, because on a box like this the configs are generated by something you no longer control and a hand edit will be overwritten.&lt;/p&gt;

&lt;p&gt;Two things make that possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: use webroot validation into the directory the server already serves.&lt;/strong&gt; Check first whether a path is already wired up - panels usually leave one behind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://yourdomain.dev/.well-known/acme-challenge/test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything other than a connection error means the path is reachable and you only need to find which directory on disk it maps to. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--issue&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; yourdomain.dev &lt;span class="nt"&gt;-d&lt;/span&gt; www.yourdomain.dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; /var/www/letsencrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two: write the certificate to the filenames the server already expects.&lt;/strong&gt; Do not point the config at acme.sh's directory; point acme.sh at the config's filenames. &lt;code&gt;--install-cert&lt;/code&gt; does exactly that, and the reload hook makes it live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--install-cert&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; yourdomain.dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fullchain-file&lt;/span&gt; /etc/ssl/sites/yourdomain.dev.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-file&lt;/span&gt;      /etc/ssl/sites/yourdomain.dev.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reloadcmd&lt;/span&gt;     &lt;span class="s2"&gt;"nginx -s reload &amp;amp;&amp;amp; apachectl graceful"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content of the files changes, the paths never do, and nothing in the web server configuration is edited.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expired certificates do not block renewal
&lt;/h3&gt;

&lt;p&gt;The natural worry: every site redirects HTTP to HTTPS, and HTTPS is broken, so how does validation reach the challenge file?&lt;/p&gt;

&lt;p&gt;It works. Let's Encrypt follows the redirect to HTTPS &lt;strong&gt;and does not validate the certificate&lt;/strong&gt; during the HTTP-01 challenge. A long-expired certificate, a self-signed one, a hostname mismatch - none of it stops issuance. You do not need to unwind the redirects first, which is good, because on a generated config you could not anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate it, then check for a rival
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--install-cronjob&lt;/span&gt;
crontab &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;acme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the important one, and it is the mistake we made. The panel's renewal cron was disabled - and a &lt;strong&gt;second&lt;/strong&gt; panel cron doing the same job at a different hour was sitting four lines further down, missed on the first pass. Two renewal mechanisms racing over the same files is a problem that surfaces weeks later as a certificate that is valid on disk and stale in the server's memory.&lt;/p&gt;

&lt;p&gt;Read the whole crontab. Comment out, do not delete, so you can see what used to be there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/crontab.bak-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;acme.sh renews at 60 days by default, so a failed run has 30 days of retries before anything is visible to users. That is a generous margin and exactly why a silent failure can run for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitor the certificate that is served, not the file on disk
&lt;/h2&gt;

&lt;p&gt;This is the part that would have caught the original outage, and almost nobody sets it up.&lt;/p&gt;

&lt;p&gt;Checking the file is not enough: it tells you that renewal wrote something, not that the running process picked it up. A reload that silently failed leaves a fresh file on disk and an expired certificate in memory. Check the handshake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# days until the served certificate expires, per domain&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:443 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
        | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CRITICAL &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;: no TLS handshake"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$days&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 25 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$days&lt;/span&gt;&lt;span class="s2"&gt; days left"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$days&lt;/span&gt;&lt;span class="s2"&gt; days"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it daily across every hostname the box serves and alert under 25 days. Two properties make it the right check: it fails loudly when the handshake fails at all, which is the real outage, and because it reads what the server hands a client, a reload that did not happen cannot hide behind a renewed file.&lt;/p&gt;

&lt;p&gt;Add a tail of the renewal log to the same job and you have the whole story in one alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5 /var/log/acme-renew.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Eleven certificates reissued in one pass, all valid for the next three months, four dead sites back up, renewal owned by a 200 KB shell script with a cron entry we can read. No vhost file was modified; only the bytes inside the certificate files changed.&lt;/p&gt;

&lt;p&gt;The wider lesson is not about ACME clients. It is that &lt;strong&gt;the expiry of a thing is not the failure; the silence is.&lt;/strong&gt; The renewal had been broken for roughly two months before the first site went down, and the only reason anyone found out was a site going dark in front of users. A twenty-line handshake check would have raised it the first week.&lt;/p&gt;

&lt;p&gt;If you are putting a new box together rather than rescuing an old one, Caddy does all of this with no configuration at all - we wrote that stack up in &lt;a href="https://news.rozoom.app/self-hosting/caddy-docker-production/" rel="noopener noreferrer"&gt;Caddy and Docker in production&lt;/a&gt;. This recipe is for the machine you inherited, the one where the config is generated, the distro is frozen, and the only acceptable change is the smallest one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/acme-sh-certificates-legacy-server/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>tls</category>
      <category>acme</category>
      <category>letsencrypt</category>
    </item>
    <item>
      <title>Best Privacy-First Email for Developers in 2026: Proton vs Infomaniak</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:17:01 +0000</pubDate>
      <link>https://dev.to/rozoomnews/best-privacy-first-email-for-developers-in-2026-proton-vs-infomaniak-4ci8</link>
      <guid>https://dev.to/rozoomnews/best-privacy-first-email-for-developers-in-2026-proton-vs-infomaniak-4ci8</guid>
      <description>&lt;p&gt;Every side project ends up needing two different things from email, and most guides only talk about one of them.&lt;/p&gt;

&lt;p&gt;The first is a &lt;strong&gt;human mailbox&lt;/strong&gt;: &lt;code&gt;you@yourdomain.dev&lt;/code&gt;, read on a laptop and a phone, kept away from an advertising company. The second is an &lt;strong&gt;outbound SMTP endpoint&lt;/strong&gt;: the thing your app authenticates against at three in the morning to send a password reset. These are not the same job, the same traffic pattern, or the same failure mode, and the moment you treat them as one you get a project whose signups silently stop working.&lt;/p&gt;

&lt;p&gt;This is a comparison of the two privacy-first providers that are genuinely worth running a domain on in 2026, with the prices as they are billed today, the DNS records you will actually paste, and an honest line about where each one stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Proton&lt;/th&gt;
&lt;th&gt;Infomaniak&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entry paid tier&lt;/td&gt;
&lt;td&gt;Mail Plus, &lt;strong&gt;$3.99/user/mo&lt;/strong&gt; billed yearly ($4.99 monthly)&lt;/td&gt;
&lt;td&gt;kSuite Standard, &lt;strong&gt;CHF 1.76/user/mo&lt;/strong&gt; (about €1.91)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business entry&lt;/td&gt;
&lt;td&gt;Mail Essentials, &lt;strong&gt;$6.99/user/mo&lt;/strong&gt; yearly, 15 GB, 3 domains&lt;/td&gt;
&lt;td&gt;Mail Service, &lt;strong&gt;CHF 2.29/address/mo&lt;/strong&gt; (about €2.48)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailbox storage&lt;/td&gt;
&lt;td&gt;15 GB on Mail Essentials, 1 TB on Workspace Standard&lt;/td&gt;
&lt;td&gt;Unlimited on the mail service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain IMAP/SMTP for a desktop client&lt;/td&gt;
&lt;td&gt;No. Needs the Bridge app, desktop only&lt;/td&gt;
&lt;td&gt;Yes, standard IMAP and SMTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTP for your application&lt;/td&gt;
&lt;td&gt;Yes, submission tokens, paid plans, custom-domain addresses only&lt;/td&gt;
&lt;td&gt;Yes, ordinary SMTP auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end encryption between users&lt;/td&gt;
&lt;td&gt;Yes, by default&lt;/td&gt;
&lt;td&gt;No, encryption at rest and in transit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundled cloud storage&lt;/td&gt;
&lt;td&gt;500 GB on Proton Unlimited at $9.99/mo yearly&lt;/td&gt;
&lt;td&gt;50 GB kDrive per user on Standard, 3 TB on Business&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rates checked 25 October 2026; the CHF conversion uses 1 CHF = €1.0838 from 23 October 2026. Proton publishes USD on the plan pages and converts at checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "privacy-first" actually buys you
&lt;/h2&gt;

&lt;p&gt;Be precise about the threat you are paying to remove, because the two providers remove different ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proton&lt;/strong&gt; encrypts mail end to end between Proton users, and at rest with zero-access encryption for everything else. The practical consequence is the one people forget: the provider cannot read your inbox, and neither can anything you plug into it. There is no plain IMAP, because there is nothing on the server to hand over in plaintext. That is the feature. It is also the limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infomaniak&lt;/strong&gt; does not do end-to-end encryption. It does ordinary, well-run, European-hosted mail with encryption in transit and at rest, no ad profiling, and a company that sells hosting rather than attention. For most side projects that is the real alternative to a free Gmail account, and it speaks every protocol your tooling already knows.&lt;/p&gt;

&lt;p&gt;If your mail must be unreadable to the provider, that decision is already made for you. If what you want is to stop being the product, Infomaniak is cheaper, faster to set up, and far less likely to fight your tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer reality check: the Bridge
&lt;/h2&gt;

&lt;p&gt;This is the single thing that decides the choice for most people writing code, so it goes before the price table in your head.&lt;/p&gt;

&lt;p&gt;Proton has no IMAP server you can connect to. To use Thunderbird, Apple Mail, &lt;code&gt;mutt&lt;/code&gt;, &lt;code&gt;neomutt&lt;/code&gt;, &lt;code&gt;aerc&lt;/code&gt;, or anything else that speaks the protocol, you run &lt;strong&gt;Proton Bridge&lt;/strong&gt;: a local daemon that holds your keys, exposes &lt;code&gt;127.0.0.1&lt;/code&gt; IMAP and SMTP, and decrypts on your machine. It works well and it has a headless mode. But it is a process that has to be running, on a machine that has to be unlocked, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mail on a headless server through Proton means running the Bridge on that server and trusting it with the keys.&lt;/li&gt;
&lt;li&gt;A phone uses the Proton app, not your preferred client. There is no way around this.&lt;/li&gt;
&lt;li&gt;If the Bridge is not running, your client is simply offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infomaniak gives you &lt;code&gt;mail.infomaniak.com&lt;/code&gt; on 993 and 465, and your client connects. That is the whole paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending from your application
&lt;/h2&gt;

&lt;p&gt;Both providers can send mail for an app. Neither should be your transactional mail provider. Those statements are compatible, and the distinction is where most people get burned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proton SMTP submission
&lt;/h3&gt;

&lt;p&gt;Paid plans can generate &lt;strong&gt;SMTP submission tokens&lt;/strong&gt; for custom-domain addresses. The settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;host&lt;/span&gt;:     &lt;span class="n"&gt;smtp&lt;/span&gt;.&lt;span class="n"&gt;protonmail&lt;/span&gt;.&lt;span class="n"&gt;ch&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt;:     &lt;span class="m"&gt;587&lt;/span&gt;
&lt;span class="n"&gt;security&lt;/span&gt;: &lt;span class="n"&gt;STARTTLS&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;:     &lt;span class="n"&gt;noreply&lt;/span&gt;@&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt;: &amp;lt;&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that are not obvious from the setup screen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The token is shown &lt;strong&gt;once&lt;/strong&gt;, at creation. Put it in your secret store the same minute.&lt;/li&gt;
&lt;li&gt;Tokens only pair with &lt;strong&gt;custom-domain&lt;/strong&gt; addresses, never with your &lt;code&gt;@proton.me&lt;/code&gt; one.&lt;/li&gt;
&lt;li&gt;Generate one token per client and per address. Revoking then costs you one integration instead of all of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;code&gt;msmtp&lt;/code&gt; config, which is all a cron job or a small container needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="na"&gt;proton&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;smtp.protonmail.ch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;587&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;tls_starttls&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;noreply@yourdomain.dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;noreply@yourdomain.dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="na"&gt;passwordeval&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="na"&gt;"cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;/run/secrets/smtp_token"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;proton&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The limit nobody documents
&lt;/h3&gt;

&lt;p&gt;Proton's published limits are 50 emails an hour and 150 a day on free accounts, and for paid accounts "it depends on the reputation of your account", with up to 100 recipients on a single message. There is no published paid ceiling.&lt;/p&gt;

&lt;p&gt;Read that twice before you point a signup flow at it. An undocumented, reputation-shaped limit is fine for a contact form, a backup alert, or a nightly report. It is not a foundation for a product that sends onboarding mail in bursts, because the first time you hit the limit you will learn about it from your users rather than from a dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule worth keeping:&lt;/strong&gt; your mailbox provider sends mail &lt;em&gt;to you&lt;/em&gt; and &lt;em&gt;as you&lt;/em&gt;. A transactional provider with a real API, webhooks and a bounce dashboard sends mail &lt;em&gt;to your users&lt;/em&gt;. Pay one bill for each. The second bill is usually zero at side-project volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DNS records, in full
&lt;/h2&gt;

&lt;p&gt;Identical in shape for both providers, and the step people skip at the cost of a week in spam folders. Replace the selector and host values with the ones your provider shows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; MX
yourdomain.dev.          3600  IN  MX   10 mail.provider.example.
; SPF - exactly one TXT record starting with v=spf1
yourdomain.dev.          3600  IN  TXT  "v=spf1 include:_spf.provider.example -all"
; DKIM - copy the public key your provider generates
sel._domainkey.yourdomain.dev. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBg..."
; DMARC - start at none, read the reports, then tighten
_dmarc.yourdomain.dev.   3600  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.dev; fo=1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things that cause nearly every delivery problem we have had to debug:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two SPF records.&lt;/strong&gt; Only one TXT record may begin with &lt;code&gt;v=spf1&lt;/code&gt;. A second one, usually left behind by an old provider, invalidates both. Merge them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-all&lt;/code&gt; before you are ready.&lt;/strong&gt; Start with &lt;code&gt;~all&lt;/code&gt;, confirm in the DMARC reports that everything legitimate passes, then harden to &lt;code&gt;-all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;p=reject&lt;/code&gt; on day one.&lt;/strong&gt; You will reject your own mail. &lt;code&gt;p=none&lt;/code&gt; for two weeks is not timidity, it is the only way to find the sender you forgot about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A missing PTR when you send from your own box.&lt;/strong&gt; Not relevant with either provider here, but it is the reason self-hosted SMTP on a VPS goes to spam. If you are tempted to run your own mail server, read &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;Best VPS for self-hosting in 2026&lt;/a&gt; first and then do it for the mailbox, not for sending.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost at the sizes that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One person, one domain.&lt;/strong&gt; Proton Mail Plus at $3.99/month yearly gives one user, custom domain support and SMTP tokens. Infomaniak's standalone mail service is CHF 2.29 per address per month with unlimited mail storage, or CHF 1.76 per user on kSuite Standard, which bundles two addresses and 50 GB of cloud storage. Infomaniak wins on price; Proton wins if you want the mailbox encrypted against its own operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small team of five.&lt;/strong&gt; Proton Mail Essentials is $6.99 per user per month on a yearly term, so about $420 a year, with 15 GB per user and up to 3 domains. Infomaniak kSuite Business is CHF 7.33 per user (about €7.94), halved for the first year, and brings 3 TB of cloud storage per user, which is the line item that actually changes the comparison if you were about to pay for storage anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You already pay for a VPN or cloud storage.&lt;/strong&gt; Proton Unlimited at $9.99/month yearly folds mail, 500 GB, VPN and a password manager into one bill. Against four separate subscriptions it is the cheaper answer, and that bundle, not the mail client, is the honest reason most developers end up on Proton.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would pick
&lt;/h2&gt;

&lt;p&gt;For a &lt;strong&gt;private mailbox on your own domain&lt;/strong&gt; where the provider must not be able to read it, take Proton, accept the Bridge, and use the SMTP token only for low-volume machine mail.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;everything else&lt;/strong&gt; - a team that lives in standard clients, a project that wants cheap mailboxes and storage in one place, a mail setup that should take fifteen minutes and never be thought about again - take Infomaniak.&lt;/p&gt;

&lt;p&gt;And whichever you choose, send your product's mail through a transactional provider. The €2 mailbox and the 10,000-message sending tier are different purchases, and conflating them is the most expensive mistake in this entire article.&lt;/p&gt;

&lt;p&gt;Next in this series: the full mail setup on a custom domain, from DNS to a working client, with the DMARC reports read out loud.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/cloud/best-privacy-email-for-developers/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>email</category>
      <category>privacy</category>
      <category>smtp</category>
    </item>
    <item>
      <title>Best Backup Storage for Self-Hosters in 2026: the price nobody shows you</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:16:25 +0000</pubDate>
      <link>https://dev.to/rozoomnews/best-backup-storage-for-self-hosters-in-2026-the-price-nobody-shows-you-1949</link>
      <guid>https://dev.to/rozoomnews/best-backup-storage-for-self-hosters-in-2026-the-price-nobody-shows-you-1949</guid>
      <description>&lt;p&gt;Every comparison of backup storage quotes a price per terabyte. That number is real, but it is not the number that decides your bill. Most self-hosters do not back up a terabyte. They back up a database, a config directory and a photo folder - a few tens of gigabytes - and what they actually pay is set by each provider's &lt;strong&gt;minimum&lt;/strong&gt;, not by the per-terabyte rate.&lt;/p&gt;

&lt;p&gt;We tested this on our own data. We run two separate object-storage backups: an ERP database and a hosting panel. Here is what they are, read out of the buckets the day we wrote this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually back up
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backup&lt;/th&gt;
&lt;th&gt;Objects&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ERP database&lt;/td&gt;
&lt;td&gt;2,256&lt;/td&gt;
&lt;td&gt;13 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting panel&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;30 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,640&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the realistic self-hoster profile: a handful of services, a few tens of gigabytes, all of it worth keeping, none of it huge. Now price those same 43 GB everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same 43 GB, six providers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Headline price&lt;/th&gt;
&lt;th&gt;Egress&lt;/th&gt;
&lt;th&gt;Minimum&lt;/th&gt;
&lt;th&gt;S3 API&lt;/th&gt;
&lt;th&gt;Our 43 GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2&lt;/td&gt;
&lt;td&gt;$6.95 / TB&lt;/td&gt;
&lt;td&gt;free up to 3x stored, then $0.01/GB&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $0.30/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3 Standard&lt;/td&gt;
&lt;td&gt;$24.50 / TB (eu-central-1)&lt;/td&gt;
&lt;td&gt;$0.09/GB after 100 GB/mo free&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $1.05/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner Storage Box BX11&lt;/td&gt;
&lt;td&gt;€3.20 flat, 1 TB&lt;/td&gt;
&lt;td&gt;unlimited&lt;/td&gt;
&lt;td&gt;1 TB block&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€3.20/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infomaniak backup storage&lt;/td&gt;
&lt;td&gt;CHF 4.18 / TB + CHF 1.84 per device&lt;/td&gt;
&lt;td&gt;not published&lt;/td&gt;
&lt;td&gt;1 TB block&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ CHF 6.02/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner Object Storage&lt;/td&gt;
&lt;td&gt;€6.49 flat, includes 1 TB + 1 TB egress&lt;/td&gt;
&lt;td&gt;1 TB included&lt;/td&gt;
&lt;td&gt;1 TB block&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€6.49/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasabi&lt;/td&gt;
&lt;td&gt;$7.99 / TB&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;1 TB + 90 days&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$7.99/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices read from each vendor's own pricing page on the day of writing; European prices exclude VAT. Sorted by what our 43 GB costs, the per-terabyte ranking turns upside down: the "expensive" AWS row lands second-cheapest, and the cheap-per-terabyte rows cost an order of magnitude more than the pay-as-you-go leader.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reason is the minimum, not the rate
&lt;/h3&gt;

&lt;p&gt;Four of the six providers sell storage in blocks. You buy a terabyte whether you use 43 GB or 950 GB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wasabi&lt;/strong&gt; states a minimum monthly charge tied to 1 TB per account, plus a 90-day minimum storage duration. Delete a file after a week and you still pay for it for 90 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Object Storage&lt;/strong&gt; charges a €6.49 base price that includes 1 TB of storage and 1 TB of egress. Below a terabyte you are paying for headroom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Storage Box&lt;/strong&gt; is a fixed 1 TB box for €3.20.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infomaniak&lt;/strong&gt; sells backup capacity in 1 TB steps at CHF 4.18, plus CHF 1.84 per device you back up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only &lt;strong&gt;Backblaze B2&lt;/strong&gt; and &lt;strong&gt;AWS S3&lt;/strong&gt; bill purely for what you store. At 43 GB that difference is the whole story.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flat prices are not a trap at the right size
&lt;/h3&gt;

&lt;p&gt;None of this makes the block-priced providers bad. It makes them wrong &lt;em&gt;at our size&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store 900 GB and Hetzner Storage Box at €3.20 is the cheapest terabyte you can buy anywhere, with snapshots included.&lt;/li&gt;
&lt;li&gt;Store several terabytes and restore often, and Wasabi's $0 egress starts paying for itself.&lt;/li&gt;
&lt;li&gt;Hetzner Object Storage bundles 1 TB of egress into the base price, which for a 1 TB dataset means a full restore every month at no extra cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision is not "which provider is cheapest" but "where does my data size sit relative to their minimum".&lt;/p&gt;

&lt;h3&gt;
  
  
  The one that is not S3
&lt;/h3&gt;

&lt;p&gt;Hetzner Storage Box is the cheapest row in the table and the one most likely to waste your afternoon. It is reachable over SFTP, rsync, SCP, SMB and WebDAV, and it has &lt;strong&gt;no S3 API&lt;/strong&gt;. Hetzner sells S3 separately as Object Storage, at twice the price.&lt;/p&gt;

&lt;p&gt;If your backup tool speaks S3 - and &lt;code&gt;restic&lt;/code&gt;, &lt;code&gt;rclone&lt;/code&gt;, &lt;code&gt;duplicity&lt;/code&gt;, &lt;code&gt;pgBackRest&lt;/code&gt; and most modern tools default to it - a Storage Box needs a translation layer or a different tool. Decide on the tool first, then on the storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The default that quietly costs the most
&lt;/h3&gt;

&lt;p&gt;S3 Standard is where every tutorial starts, and at 43 GB it is genuinely fine: about a dollar a month, no minimum, and a 43 GB restore fits inside the 100 GB of free monthly data transfer AWS grants across its services. That allowance is the part most comparisons get wrong in both directions - AWS egress is not automatically expensive at small scale.&lt;/p&gt;

&lt;p&gt;Where it turns is scale. At 500 GB you are paying $12 a month for storage that costs $3.50 on B2, and a full restore is 500 GB at $0.09/GB once past the free 100 GB: about $36, on the worst possible day. Per-request charges on PUT and LIST apply on top, which matters when your backup tool writes thousands of small objects - ours writes 2,640 of them per run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Egress is the bill you pay on your worst day
&lt;/h2&gt;

&lt;p&gt;Storage price is what you pay every month. Egress is what you pay the day you actually need the backup, which is exactly when you are in no position to shop around.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backblaze B2&lt;/strong&gt;: free egress up to 3x your average monthly storage, then $0.01/GB. At 43 GB that is ~129 GB free per month - three full restores, free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wasabi&lt;/strong&gt;: $0 egress, always. Its real selling point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Object Storage&lt;/strong&gt;: 1 TB of egress in the base price, then billed per TB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Storage Box&lt;/strong&gt;: unlimited traffic, no egress charge by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS S3&lt;/strong&gt;: 100 GB/month free across services, then $0.09/GB. Small restores free, large restores expensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model one full restore before you commit, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommendation, by size
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Under ~500 GB, tool speaks S3 → Backblaze B2.&lt;/strong&gt; No minimum, $6.95/TB, free egress up to 3x stored. Our 43 GB costs about 30 cents a month. This is the default for most self-hosters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Around 1 TB, tool speaks rsync/SFTP → Hetzner Storage Box.&lt;/strong&gt; €3.20 a month is the cheapest terabyte on this list, as long as you never need the S3 API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Around 1 TB, tool speaks S3, you want one European provider for storage and mail → Infomaniak or Hetzner Object Storage.&lt;/strong&gt; Both are block-priced, both are S3-compatible, both are in Europe; Infomaniak bills per device on top of capacity, Hetzner bundles a terabyte of egress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Several TB with frequent restores → Wasabi.&lt;/strong&gt; The 1 TB minimum stops being a penalty once you actually store that much, and $0 egress works in your favour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already inside AWS → S3 Standard with lifecycle rules.&lt;/strong&gt; Move old versions to a cheaper class and set a retention policy, or you pay the highest storage rate here for the least forgiving restore path.&lt;/p&gt;

&lt;p&gt;Whichever row you pick: make sure the backup exists and that you have restored from it at least once. The cheapest storage in the world costs nothing if you never pull from it, and the moment you do is the moment the pricing table becomes real.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the cheapest object storage for small backups?&lt;/strong&gt;&lt;br&gt;
Backblaze B2, because it has no monthly minimum. Our 43 GB costs about $0.30/month. The flat-price competitors - Wasabi, Hetzner Object Storage and Storage Box, Infomaniak - charge for a full terabyte whether you use it or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Wasabi really cheaper than AWS S3?&lt;/strong&gt;&lt;br&gt;
Per terabyte yes, and it has no egress or API fees. But it carries a minimum monthly charge tied to 1 TB and a 90-day minimum storage duration, so below a terabyte you pay for storage you do not use. At 43 GB, Wasabi costs roughly eight times what AWS S3 costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Hetzner Storage Box S3-compatible?&lt;/strong&gt;&lt;br&gt;
No. It speaks SFTP, rsync, SCP, SMB and WebDAV. Hetzner's S3-compatible product is Object Storage, a separate service with a €6.49 base price that includes 1 TB of storage and 1 TB of egress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Backblaze B2 charge for egress?&lt;/strong&gt;&lt;br&gt;
Free up to 3x your average monthly storage, then $0.01/GB. At a few tens of gigabytes that is effectively unlimited for restores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does it cost to restore 43 GB?&lt;/strong&gt;&lt;br&gt;
Nothing, on every provider here: B2 within its 3x allowance, Wasabi and Storage Box by design, Hetzner Object Storage within its included terabyte, and AWS within the 100 GB of free monthly transfer. Egress only starts to hurt at hundreds of gigabytes - which is exactly when nobody checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which backup storage has no egress fees at all?&lt;/strong&gt;&lt;br&gt;
Wasabi ($0 egress, always) and Hetzner Storage Box (unlimited traffic). The others include an allowance and charge beyond it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Backups are only as good as the restore you have practised. The companion pieces: &lt;a href="https://news.rozoom.app/self-hosting/umami-self-hosted-analytics/" rel="noopener noreferrer"&gt;self-hosted analytics on the same box&lt;/a&gt;, and the &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;VPS comparison&lt;/a&gt; that hosts the whole stack.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/cloud/best-backup-storage-for-self-hosters-2026/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>backup</category>
      <category>objectstorage</category>
      <category>s3</category>
    </item>
    <item>
      <title>Self-Hosted Analytics in 20 Minutes, and the Traffic It Will Never Show You</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:15:49 +0000</pubDate>
      <link>https://dev.to/rozoomnews/self-hosted-analytics-in-20-minutes-and-the-traffic-it-will-never-show-you-4odm</link>
      <guid>https://dev.to/rozoomnews/self-hosted-analytics-in-20-minutes-and-the-traffic-it-will-never-show-you-4odm</guid>
      <description>&lt;p&gt;Eleven days ago we put a privacy-first analytics server next to a site that was already running. No new machine, no managed service, no cookie banner. It took about twenty minutes, it now uses 340 MB of RAM, and its database has grown to 9.5 MB.&lt;/p&gt;

&lt;p&gt;It also recorded 96 pageviews in those eleven days, while the web server in front of it logged 2016 successful page responses.&lt;/p&gt;

&lt;p&gt;Both numbers are real, both are from the same box, and the gap between them is the most useful thing in this article. Start with the install, because the install is the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-host this at all
&lt;/h2&gt;

&lt;p&gt;Three reasons, in the order they actually matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You already pay for the box.&lt;/strong&gt; Hosted analytics is priced per month and scales with pageviews, so the bill grows exactly when the site starts working. A container next to an existing site costs whatever 340 MB of RAM costs you, which on a €5.49 server is a rounding error. We sized the rest of the fleet in &lt;a href="https://news.rozoom.app/cloud/hetzner-cloud-review-2026/" rel="noopener noreferrer"&gt;our Hetzner review&lt;/a&gt; and in &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;the VPS comparison&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No cookie banner.&lt;/strong&gt; Umami does not set cookies and does not build cross-site profiles. That is the difference between a site that opens with content and a site that opens with a consent dialog, and it is worth more than any metric the dialog is protecting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data stays in your Postgres.&lt;/strong&gt; You can query it with SQL, back it up with everything else, and delete it when you want to. No export quota, no retention tier.&lt;/p&gt;

&lt;p&gt;The honest counter-argument: you now operate a database and an app that nobody else patches for you. If you will not run &lt;code&gt;docker compose pull&lt;/code&gt; once a month, use the hosted version and stop reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compose file, for a box that already runs something
&lt;/h2&gt;

&lt;p&gt;This is the part most guides get wrong by assuming an empty machine. The realistic case is a server with a reverse proxy and a site on it already, and the goal is to add two containers without touching what works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;umami-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami-db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.env.umami&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;umami_db:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;umami"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;umami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/umami-software/umami:postgresql-latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.env.umami&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;umami-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;umami_db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are doing real work here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither container publishes a port.&lt;/strong&gt; They talk to the reverse proxy over the compose network and are unreachable from the internet except through it. If you add &lt;code&gt;ports: ["3000:3000"]&lt;/code&gt; out of habit, you have just put an admin login on a public port with a default password on it, and the firewall is the only thing between you and a bad evening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The healthcheck is not decoration.&lt;/strong&gt; Umami runs its migrations on first boot. Start it against a Postgres that is still initialising and the first run fails on a connection error, leaving you to debug a problem that fixes itself on restart. &lt;code&gt;condition: service_healthy&lt;/code&gt; removes the race.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.env.umami&lt;/code&gt; file next to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;umami
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;umami
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;long random string&amp;gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://umami:&amp;lt;same string&amp;gt;@umami-db:5432/umami
&lt;span class="nv"&gt;APP_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;another long random string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt; is the only variable the application truly requires. &lt;code&gt;APP_SECRET&lt;/code&gt; signs the authentication tokens, and the documentation is explicit that every installation should have its own. Generate both the same way and never reuse them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep that file at &lt;code&gt;chmod 600&lt;/code&gt;. It holds the database password in two places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it to Caddy
&lt;/h2&gt;

&lt;p&gt;If you already run Caddy the way we described in &lt;a href="https://news.rozoom.app/self-hosting/caddy-docker-production/" rel="noopener noreferrer"&gt;the Caddy and Docker setup&lt;/a&gt;, the whole integration is five lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stats.yourdomain.dev {
    encode zstd gzip

    header {
        X-Content-Type-Options nosniff
        Referrer-Policy no-referrer
        -Server
    }

    reverse_proxy umami:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy resolves &lt;code&gt;umami&lt;/code&gt; over the compose network and provisions the certificate on first request. There is no TLS configuration to write, which is the entire reason to use it on a small box.&lt;/p&gt;

&lt;p&gt;Point the DNS record at the server before you reload, or the certificate request fails and Caddy backs off for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step you must not postpone
&lt;/h2&gt;

&lt;p&gt;Umami creates a default account on first boot: username &lt;code&gt;admin&lt;/code&gt;, password &lt;code&gt;umami&lt;/code&gt;. This is documented, which means every scanner on the internet knows it too.&lt;/p&gt;

&lt;p&gt;Log in, change the password, then prove the old one is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://stats.yourdomain.dev/api/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"username":"admin","password":"umami"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;401&lt;/code&gt; is the answer you want. Anything else means the change did not take, and you have a public dashboard with a published password on it.&lt;/p&gt;

&lt;p&gt;Then add your site in the UI and drop the snippet it gives you into your HTML head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://stats.yourdomain.dev/script.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-website-id=&lt;/span&gt;&lt;span class="s"&gt;"your-website-id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website id is not a secret. It identifies which site a pageview belongs to, nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What eleven days of real numbers look like
&lt;/h2&gt;

&lt;p&gt;Here is the whole cost of running this, measured on a live box rather than estimated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Umami container memory&lt;/td&gt;
&lt;td&gt;302 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres container memory&lt;/td&gt;
&lt;td&gt;37 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database size, 96 pageviews&lt;/td&gt;
&lt;td&gt;9.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU, both containers, steady state&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime since install, restarts&lt;/td&gt;
&lt;td&gt;11 days, zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 9.5 MB is almost entirely empty schema. Postgres does not grow linearly with a handful of events, so do not read a per-pageview cost into that number. The memory figure is the one that matters for capacity planning: budget roughly 350 MB and you will not be surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the uncomfortable part
&lt;/h2&gt;

&lt;p&gt;Same box, same eleven days, two sources of truth that disagree violently.&lt;/p&gt;

&lt;p&gt;The access log recorded &lt;strong&gt;2016 successful HTML responses&lt;/strong&gt;. Umami recorded &lt;strong&gt;96 pageviews across 26 sessions&lt;/strong&gt;. If you take those at face value, analytics saw 4.8% of your traffic and you should panic.&lt;/p&gt;

&lt;p&gt;Do not panic. Take the log apart instead.&lt;/p&gt;

&lt;p&gt;Filtering user agents containing &lt;code&gt;bot&lt;/code&gt;, &lt;code&gt;spider&lt;/code&gt;, &lt;code&gt;crawl&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt; or &lt;code&gt;headless&lt;/code&gt; removes 1143 of those 2016 requests. More than half the traffic to a new site is openly declared automation, and that is before counting the ones that lie.&lt;/p&gt;

&lt;p&gt;Of the 873 that survive, &lt;strong&gt;196 are &lt;code&gt;GoogleOther&lt;/code&gt;&lt;/strong&gt; - a Google crawler whose user agent contains no occurrence of the string &lt;code&gt;bot&lt;/code&gt; at all. If your log analysis greps for "bot", you have been silently counting a crawler as a reader. That one is worth remembering the next time a dashboard built on access logs tells you something flattering.&lt;/p&gt;

&lt;p&gt;The remainder still contains unlabelled scrapers. A heavy concentration of one exact ancient Safari build, for instance, is a fingerprint, not an audience.&lt;/p&gt;

&lt;p&gt;So the real comparison is 96 recorded pageviews against a few hundred plausibly-human requests, and the gap has three sources, in descending order of size:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automation that does not run JavaScript.&lt;/strong&gt; It appears in the log and never in analytics. This is most of the gap, and it is analytics being right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocked trackers.&lt;/strong&gt; Some filter lists block by hostname pattern, and a host called &lt;code&gt;stats.&lt;/code&gt; is a pattern. Self-hosting does not grant immunity, it only removes the well-known third-party domain. The project documents proxying the script under your own domain and moving the collection endpoint with &lt;code&gt;COLLECT_API_ENDPOINT&lt;/code&gt; if you need to close that gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefetch and no-JS readers.&lt;/strong&gt; Small, real, unfixable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The conclusion is not "logs are honest and analytics lies". It is that they measure different populations: the log counts requests, analytics counts browsers that execute script. A new site's log is mostly robots, which is why reading raw logs makes a young site feel popular and is the single easiest way to lie to yourself about traction.&lt;/p&gt;

&lt;p&gt;If you want one number to steer by in the first months, use sessions from analytics and treat the log as a debugging tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent, briefly and carefully
&lt;/h2&gt;

&lt;p&gt;Cookieless analytics avoids the consent requirement that attaches to storing or reading information on the visitor's device, which is what makes the banner mandatory for conventional tracking. It does not make the processing invisible: an IP address still reaches your server, as it does for every request to every site.&lt;/p&gt;

&lt;p&gt;Practical consequences, not legal advice: say in your privacy page what you collect, keep it aggregate, and set &lt;code&gt;IGNORE_IP&lt;/code&gt; for your own addresses so your dashboard is not mostly you. If your situation is regulated, ask someone qualified rather than a blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance, which is the whole deal you signed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull umami umami-db
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; umami umami-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the monthly job. Schema migrations run automatically on container start, which is convenient and also exactly why the volume matters: &lt;code&gt;umami_db&lt;/code&gt; is the only thing you cannot recreate from this file. Back it up with the rest of your data and test the restore once, or you are keeping an untested copy of 9.5 MB and a false sense of safety.&lt;/p&gt;

&lt;p&gt;After the first week, do one more thing: open the dashboard and compare it against what you know to be true. If you visited the site yourself twenty times and the dashboard shows two, something between the script tag and the collector is being blocked, and better to learn that now than in three months, when you are making decisions from the number.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/umami-self-hosted-analytics/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>analytics</category>
      <category>docker</category>
      <category>caddy</category>
    </item>
    <item>
      <title>Where You Can Actually Run a Blockchain Node in 2026</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:15:13 +0000</pubDate>
      <link>https://dev.to/rozoomnews/where-you-can-actually-run-a-blockchain-node-in-2026-2366</link>
      <guid>https://dev.to/rozoomnews/where-you-can-actually-run-a-blockchain-node-in-2026-2366</guid>
      <description>&lt;p&gt;Search for how to run an Ethereum or Bitcoin node and you get a hundred tutorials that start with "rent a VPS". Not one of them checks whether that VPS provider allows it. Then the server disappears, usually with no warning, and the tutorial's comment section fills up with people who lost a synced chain.&lt;/p&gt;

&lt;p&gt;The confusion has a simple root: &lt;strong&gt;mining and running a node are different activities, and hosting contracts are mostly written as if only mining exists.&lt;/strong&gt; This post is what the contracts actually say, quoted from the providers' own legal pages, plus what a node really costs in disk, RAM and bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mining, staking, and just running a node
&lt;/h2&gt;

&lt;p&gt;Three different things get lumped together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mining&lt;/strong&gt; burns CPU or GPU cycles continuously to win blocks. It saturates hardware, and it is why hosts care: it makes shared infrastructure slow for everyone and it attracts fraud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running a full node&lt;/strong&gt; downloads and validates the chain. It is mostly disk and bandwidth, with modest CPU. It earns nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staking or validating&lt;/strong&gt; runs a full node plus signing duties, with capital locked up. Also not CPU-heavy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A provider that bans "mining" has written a rule about the first one. Whether that rule covers the other two is usually not stated anywhere, and that gap is where people lose servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the contracts actually say
&lt;/h2&gt;

&lt;p&gt;Read on the providers' own legal pages at the time of writing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Mining&lt;/th&gt;
&lt;th&gt;Nodes mentioned?&lt;/th&gt;
&lt;th&gt;Exact wording&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner&lt;/td&gt;
&lt;td&gt;prohibited&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;"The operation of applications for mining cryptocurrencies remains prohibited." Elsewhere: "mining, farming and plotting of cryptocurrencies."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netcup&lt;/td&gt;
&lt;td&gt;prohibited&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;"Operation of mining-services such as 'Bitcoin', 'Ethereum', 'OneCoin' or 'Monero'."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;prohibited without permission&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;"Mining any cryptocurrency, including but not limited to BitCoin or any similar currency, without explicit written permission."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaleway&lt;/td&gt;
&lt;td&gt;prohibited&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;"not to engage in any harmful practices, such as but not limited to: cryptocurrency mining, Black Hat SEO, spamming..."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contabo&lt;/td&gt;
&lt;td&gt;not mentioned in the terms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sells node hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no crypto clause in the terms and conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OVHcloud&lt;/td&gt;
&lt;td&gt;not mentioned in the terms we read&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sells node hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no crypto clause in the dedicated-server conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not one of these contracts uses the word "node".&lt;/strong&gt; Every ban is written about mining. If you run a Bitcoin full node on a provider whose rule says "mining", you are in a grey area that a support agent resolves, not a lawyer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean is the only one that leaves a door open in writing.&lt;/strong&gt; "Without explicit written permission" means there is a process: ask, get an answer in writing, keep it. If you want a node on a mainstream cloud and you want to sleep at night, that sentence is the most useful one in this table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hetzner is the clear no
&lt;/h3&gt;

&lt;p&gt;Hetzner's terms ban mining outright, and in 2022 the company went further than the written text: a spokesperson stated that even running a single node violates its terms, and over a thousand Solana validators went offline when the enforcement landed. The written policy still only says "mining", but the enforcement record says nodes. Treat Hetzner as off-limits for anything chain-related, however cheap the server is.&lt;/p&gt;

&lt;p&gt;That is a genuine loss, because for everything that is &lt;em&gt;not&lt;/em&gt; a node, Hetzner is usually the price-performance answer. We say so in our own &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;VPS comparison&lt;/a&gt; and we still mean it. Just not for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contabo and OVHcloud sell it as a product
&lt;/h3&gt;

&lt;p&gt;The flip side is the part the tutorials miss. Two of the providers in the table do not merely tolerate nodes, they advertise them.&lt;/p&gt;

&lt;p&gt;Contabo has a page titled &lt;em&gt;Cryptocurrency Nodes at Contabo&lt;/em&gt;. OVHcloud runs a blockchain infrastructure line whose copy reads "Run your node, chain, or layer 2 exactly how you want to" and "A network that keeps your blockchain nodes synched". When a provider markets the exact workload you are deploying, your terms-of-service risk is close to zero: you are the customer they built the page for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we could not verify
&lt;/h3&gt;

&lt;p&gt;Vultr's use policy returns an access error to automated requests, so we could not read it first-hand and have left it out of the table rather than quote a figure we did not see. Latitude.sh's terms defer to a third-party master agreement we did not read. If you are choosing between those two, open the pages yourself before you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a node actually needs
&lt;/h2&gt;

&lt;p&gt;From each project's own documentation, not from forum folklore:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Disk&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin Core&lt;/td&gt;
&lt;td&gt;1 GHz, any modern core&lt;/td&gt;
&lt;td&gt;512 MB minimum&lt;/td&gt;
&lt;td&gt;750 GB&lt;/td&gt;
&lt;td&gt;740 GB one-time initial download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum (Geth + consensus client)&lt;/td&gt;
&lt;td&gt;quad core&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;2 TB SSD&lt;/td&gt;
&lt;td&gt;grows ~14 GB/week; archive node over 12 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana validator&lt;/td&gt;
&lt;td&gt;12 cores / 24 threads&lt;/td&gt;
&lt;td&gt;256 GB ECC&lt;/td&gt;
&lt;td&gt;1 TB accounts + 1 TB ledger + 500 GB snapshots, separate NVMe each&lt;/td&gt;
&lt;td&gt;RPC node: 16 cores, 512 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Ethereum line is the one most people get wrong. A 2 TB disk is not a comfort margin, it is the requirement, and the chain grows about 14 GB a week underneath you. A node that fits today stops fitting in a year.&lt;/p&gt;

&lt;p&gt;The Solana line is not a self-hosting project at all. 256 GB of ECC memory and three separate NVMe drives is a dedicated machine, and Solana's own docs add a line worth quoting to anyone planning it on a cheap VPS: running in the cloud "requires significantly greater operational expertise to achieve stability and performance".&lt;/p&gt;

&lt;h2&gt;
  
  
  The bandwidth bill nobody mentions
&lt;/h2&gt;

&lt;p&gt;This is the number missing from every node tutorial. A Bitcoin Core node with default settings uploads &lt;strong&gt;5 GB a day, about 150 GB a month&lt;/strong&gt;, serving blocks to other peers. It is not an accident, it is the job.&lt;/p&gt;

&lt;p&gt;What that costs depends entirely on how your host bills traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unmetered or generous included traffic&lt;/strong&gt; (most European VPS providers): free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metered cloud egress&lt;/strong&gt;: 150 GB a month at $0.09/GB is about $13.50 a month in bandwidth alone, which is more than the server on most plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the hosting decision for a node is not "which VPS has the best CPU". It is "which VPS does not meter the 150 GB a month my node will push out by design". You can cap it (Bitcoin Core takes a bandwidth limit), but then you are a worse peer and a slower node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does any of it pay?
&lt;/h2&gt;

&lt;p&gt;Worth saying plainly, because the search results around this topic are full of promises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A plain full node earns nothing.&lt;/strong&gt; Bitcoin, Ethereum, anything. It is infrastructure you run for sovereignty, privacy or an application you are building, not income.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum staking&lt;/strong&gt; needs 32 ETH locked up. At the time of writing that is roughly 74,000 USD of capital for a yield in the low single digits, before the server bill. The yield is denominated in an asset whose price moves more in a week than the yield pays in a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana validating&lt;/strong&gt; costs money before it earns any: the docs state that voting "can cost up to 1.1 SOL per day" in vote transactions. At today's price that is about 110 USD a day, roughly 40,000 USD a year, paid regardless of what you earn, and you need a large delegated stake to cover it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run a node because you want your own view of the chain. Do not run one as a side income: the economics only work with capital or scale you do not have if you are reading a self-hosting blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you deploy, do this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open the provider's acceptable use policy and search it for "mining".&lt;/strong&gt; Thirty seconds. If the word is there and "node" is not, you are in the grey area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If it is a grey area and the server matters, ask support in writing.&lt;/strong&gt; On DigitalOcean this is explicitly the documented path. Keep the reply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check how egress is billed&lt;/strong&gt; before you check CPU benchmarks. 150 GB a month is the baseline for Bitcoin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size the disk for next year, not for today.&lt;/strong&gt; Ethereum adds roughly 14 GB a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a provider that advertises the workload.&lt;/strong&gt; A marketing page about node hosting is a stronger guarantee than silence in the terms.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I run a crypto node on Hetzner?&lt;/strong&gt;&lt;br&gt;
No. Hetzner's terms prohibit mining applications, and in 2022 the company stated that even a single node violates its terms, removing over a thousand Solana validators in the process. The written text still says "mining", but the enforcement record is clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is running a node the same as mining?&lt;/strong&gt;&lt;br&gt;
No. Mining burns CPU or GPU continuously to win blocks. A full node downloads and validates the chain: mostly disk and bandwidth, with modest CPU, and it earns nothing. Most hosting contracts only ban the first and say nothing about the second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which hosting providers allow blockchain nodes?&lt;/strong&gt;&lt;br&gt;
Contabo and OVHcloud both market node hosting as a product, which is the strongest signal you can get. DigitalOcean allows crypto workloads with explicit written permission. Hetzner, Netcup and Scaleway prohibit mining in their terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much disk does an Ethereum node need?&lt;/strong&gt;&lt;br&gt;
2 TB SSD for a full node running Geth plus a consensus client, per Geth's own documentation, and the database grows about 14 GB per week. An archive node needs over 12 TB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much bandwidth does a Bitcoin node use?&lt;/strong&gt;&lt;br&gt;
About 5 GB per day of upload, 150 GB per month, with default settings, plus a one-time 740 GB download for the initial sync. On a metered cloud that traffic can cost more than the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I make money running a node?&lt;/strong&gt;&lt;br&gt;
Not from a plain full node: it earns nothing. Ethereum staking needs 32 ETH of capital. Solana validating costs up to 1.1 SOL per day in vote fees before it earns anything. Nodes are infrastructure, not income.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If you are picking a server for something that is not a node, our &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;VPS comparison&lt;/a&gt; has live pricing from three vendors, and the &lt;a href="https://news.rozoom.app/cloud/best-backup-storage-for-self-hosters-2026/" rel="noopener noreferrer"&gt;backup storage comparison&lt;/a&gt; covers where the chain snapshots should go.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/where-to-run-a-blockchain-node/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>cloud</category>
      <category>blockchain</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Caddy and Docker in Production: the Whole Config Behind This Site</title>
      <dc:creator>Rozoom News Editor</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:14:55 +0000</pubDate>
      <link>https://dev.to/rozoomnews/caddy-and-docker-in-production-the-whole-config-behind-this-site-1620</link>
      <guid>https://dev.to/rozoomnews/caddy-and-docker-in-production-the-whole-config-behind-this-site-1620</guid>
      <description>&lt;p&gt;This site runs on one small VPS. On the same box: a static blog, a self-hosted analytics stack, two internal APIs and a password-protected app. One Caddy container terminates TLS for all of them, renews certificates without being asked, and has needed exactly zero manual attention since it was set up.&lt;/p&gt;

&lt;p&gt;This post is that setup, config file included, plus the two mistakes we made along the way. If you want the hardware side first, we compared the options in &lt;a href="https://news.rozoom.app/self-hosting/best-vps-self-hosting-2026/" rel="noopener noreferrer"&gt;Best VPS for self-hosting in 2026&lt;/a&gt; - everything below runs comfortably on the cheapest tier there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Caddy instead of nginx
&lt;/h2&gt;

&lt;p&gt;nginx is excellent and we still run it as the static file server inside this stack. But for the edge, Caddy wins on one thing that matters at this scale: &lt;strong&gt;TLS is not a project&lt;/strong&gt;. No certbot, no renewal cron, no &lt;code&gt;--dry-run&lt;/code&gt; rehearsal at 2 a.m. You write a hostname, Caddy obtains the certificate on first request and renews it in the background.&lt;/p&gt;

&lt;p&gt;The trade-off is real and worth knowing: fewer tuning knobs, a smaller ecosystem of copy-paste configs, and a config language that is its own thing rather than nginx syntax you already know. For a single-box setup, that trade is strongly in your favour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compose file
&lt;/h2&gt;

&lt;p&gt;Five services, one network, no orchestration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;blog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blog&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./site:/usr/share/nginx/html:ro&lt;/span&gt;

  &lt;span class="na"&gt;analytics-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics-db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.env.analytics&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;analytics_db:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;analytics"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/umami-software/umami:postgresql-latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;.env.analytics&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;analytics-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./caddy:/etc/caddy:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;analytics_db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four details in there are load-bearing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only Caddy publishes ports.&lt;/strong&gt; Nothing else binds to the host. The blog and the analytics app are reachable on the Compose network by container name and nowhere else. This is the single biggest security win of the layout: you cannot accidentally expose a database to the internet if it has no published port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;caddy_data&lt;/code&gt; is a named volume, not a bind mount.&lt;/strong&gt; It holds the certificates and the ACME account key. Lose it and you re-issue everything on next start, which is survivable but counts against rate limits if you do it repeatedly while debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database has a healthcheck and the app waits on it.&lt;/strong&gt; Without &lt;code&gt;condition: service_healthy&lt;/code&gt;, the app container starts first, fails to connect, and restarts in a loop until Postgres is up. It converges either way, but the logs are a mess and a first deploy looks broken when it is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;, not &lt;code&gt;always&lt;/code&gt;.&lt;/strong&gt; The difference shows up when you deliberately stop a container to debug it and then reboot the host: &lt;code&gt;always&lt;/code&gt; brings it back and overrides your intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Caddyfile
&lt;/h2&gt;

&lt;p&gt;One block per hostname. This is the real structure, with our specifics swapped for placeholders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blog.example.com {
    encode zstd gzip

    header {
        X-Content-Type-Options nosniff
        Referrer-Policy no-referrer-when-downgrade
        -Server
    }

    # fingerprinted assets can be cached hard, html stays revalidated
    @immutable path /assets/* /favicon*.png /apple-touch-icon.png /og-default.png
    header @immutable Cache-Control "public, max-age=31536000, immutable"
    header /favicon.ico Cache-Control "public, max-age=604800"

    reverse_proxy blog:80
}

stats.example.com {
    encode zstd gzip
    header -Server
    reverse_proxy analytics:3000
}

app.example.com {
    basic_auth {
        admin $2a$14$...bcrypt hash here...
    }
    encode zstd gzip
    reverse_proxy app:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire TLS configuration: there isn't any. Each hostname gets a certificate on first request, renewed automatically, with HTTP redirected to HTTPS for free.&lt;/p&gt;

&lt;p&gt;Three things worth copying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-Server&lt;/code&gt; removes the server header.&lt;/strong&gt; Minor, but free. Caddy announces itself by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cache split matters more than it looks.&lt;/strong&gt; Hashed asset filenames can be cached for a year because a content change produces a new filename. HTML must not be, or visitors keep reading a stale page after you publish. Getting this backwards is the classic "I deployed but nothing changed" bug, and it is invisible to you because your own browser revalidates on reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;basic_auth&lt;/code&gt; takes a bcrypt hash, not a password.&lt;/strong&gt; Generate it with &lt;code&gt;docker exec caddy caddy hash-password&lt;/code&gt;. Never put the plaintext in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirects as a config concern
&lt;/h2&gt;

&lt;p&gt;Our affiliate links never point at a partner directly. Every one goes through a redirect defined in the edge config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    redir /go/vendor-a* https://www.vendor-a.example/ 302
    redir /go/vendor-b* https://www.vendor-b.example/ 302
    redir /go/* /affiliate-disclosure/ 302
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two reasons, and the second is the one nobody thinks of until it bites:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Partner URLs change.&lt;/strong&gt; Tracking parameters get reissued, programmes move domains. With this layout you edit one line and reload Caddy; without it you rewrite every post that ever linked there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The catch-all protects you.&lt;/strong&gt; &lt;code&gt;/go/*&lt;/code&gt; falling through to the disclosure page means a typo or a retired programme produces a real page, not a 404 in the middle of a monetised article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the trailing &lt;code&gt;*&lt;/code&gt; on each pattern so query strings survive the hop, and keep 302 rather than 301: a permanent redirect to a partner URL you will eventually change is cached in browsers you do not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two mistakes we actually made
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bind-mounting a single file pinned a stale inode.&lt;/strong&gt; The Caddyfile was mounted as &lt;code&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/code&gt;. Edits on the host stopped reaching the container: Docker bind-mounts a file by inode, and most editors write a new file and rename it over the old one, which leaves the container holding the deleted original. The site served an old config for days with nothing in the logs to say so. Fix: &lt;strong&gt;mount the directory, not the file&lt;/strong&gt;, as in the Compose file above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unrestricted deploy key, still in use.&lt;/strong&gt; The blog's own deploy key is locked to a single rsync target, but an older CI key for another app on the same box was created with no restrictions at all: whatever holds it gets an interactive shell, not write access to one directory. It has worked perfectly for months, which is exactly why nobody looked at it. Audit every line in &lt;code&gt;authorized_keys&lt;/code&gt;, not just the one you set up most recently - the next post is about getting that right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating it
&lt;/h2&gt;

&lt;p&gt;The whole maintenance surface is three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;          &lt;span class="c"&gt;# apply changes&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;caddy caddy reload &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/caddy/Caddyfile   &lt;span class="c"&gt;# config only, no downtime&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; caddy  &lt;span class="c"&gt;# watch the ACME dance on a new hostname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a new site by adding a service and a hostname block, then reload. First request to the new name takes a second or two while the certificate is issued; after that it is indistinguishable from the rest.&lt;/p&gt;

&lt;p&gt;One caveat on certificates: if your DNS sits behind a proxy that terminates TLS itself, the ACME challenge Caddy prefers can fail in ways that look like a Caddy bug. Point the record straight at the box while you are setting up, then put the proxy back if you want it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The companion piece: &lt;strong&gt;deploying to this box from GitHub Actions without giving CI a shell&lt;/strong&gt; - the restricted-rsync setup we should have used from day one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://news.rozoom.app/self-hosting/caddy-docker-production/" rel="noopener noreferrer"&gt;news.rozoom.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>caddy</category>
      <category>docker</category>
      <category>hetzner</category>
    </item>
  </channel>
</rss>
