<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>OOM on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/en/tags/oom/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>Copyright 2026 ferstar</copyright>
    <lastBuildDate>Sat, 13 Jun 2026 15:00:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/en/tags/oom/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>A 512MB VPS OOM Incident: SSH Closed Instantly, API 521, and the Low-Memory Cleanup Afterward</title>
      <link>https://blog.ferstar.org/en/posts/vps-low-memory-oom-ssh-api-recovery/</link>
      <pubDate>Sat, 13 Jun 2026 15:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/vps-low-memory-oom-ssh-api-recovery/</guid>
      <description>A 512MB VPS suddenly returned instant SSH closes and Cloudflare 521 errors; page allocation failures under low memory were fixed with XanMod, swap, zswap, earlyoom, OOMScoreAdjust, and Docker limits.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>This was not a migration. It was what happened after the migration, when a small VPS finally pushed back.</p>
<p>The symptoms looked like a network issue: the local SSH alias closed instantly, the provider web console got stuck at “connecting”, and the API behind Cloudflare turned into a 521. The awkward part was that the proxy service on the same host still worked, so it was not a complete host failure. It also did not fit the usual suspects: floating IP, local TUN proxy, or SSH key problems.</p>
<p>After a reboot, SSH came back and I could inspect the machine. The conclusion was clear: 512MB RAM was too tight, and the system had hit repeated page allocation failures in the network receive path. Nginx stream, SSH, and the API entrypoint were all dragged into a half-broken state.</p>

<h2 class="relative group">Symptoms
    <div id="symptoms" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#symptoms" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>SSH failed before key authentication:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">kex_exchange_identification: Connection closed by remote host</span></span></code></pre></div></div>
<p>That detail mattered. The connection had reached the remote side, but SSH never got to normal authentication. So this was not a changed key, bad file permission, or dirty <code>known_hosts</code>.</p>
<p>The entrypoint design on this host is slightly indirect:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">flowchart LR
    Client["client"]
    Nginx["nginx stream :443"]
    SSH["sshd :22"]
    API["api backend :8000"]
    FM["filebrowser"]
    CF["Cloudflare"]

    Client -->|SSH over 443| Nginx
    CF -->|api.example.com| Nginx
    CF -->|fm.example.com| Nginx
    Nginx -->|default stream| SSH
    Nginx -->|SNI api| API
    Nginx -->|SNI fm| FM</code></pre></div>
<p>SSH, the API, and the file service all pass through Nginx stream. If Nginx, the kernel network stack, or local memory pressure goes sideways, the outside world sees it as “SSH is down” and “the API is down” at the same time.</p>

<h2 class="relative group">The real evidence
    <div id="the-real-evidence" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-real-evidence" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>After the reboot, the useful kernel log keyword was not <code>sshd</code>. It was this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -k --since <span class="s2">"2026-06-13 00:00:00"</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  egrep -i <span class="s2">"page allocation failure|out of memory|oom|killed process"</span></span></span></code></pre></div></div>
<p>Before the reboot, there had been repeated <code>page allocation failure</code> messages involving processes like:</p>
<ul>
<li><code>containerd-shim</code></li>
<li><code>containerd</code></li>
<li><code>dockerd</code></li>
<li><code>hysteria</code></li>
<li><code>runc</code></li>
<li><code>kswapd0</code></li>
</ul>
<p>The stack traces were concentrated in the receive path, with functions like <code>virtio_net</code>, <code>tcp_gro_receive</code>, and <code>skb_page_frag_refill</code>. So this was not just one application writing too many logs. Low memory had already started affecting how the kernel handled network packets.</p>
<p>That also explains why the failure looked so strange. A TCP port might still accept connections, and a UDP proxy might still appear alive, while SSH handshakes, Nginx stream forwarding, and API upstream traffic fail randomly under memory pressure.</p>

<h2 class="relative group">Round one: clean up the system and kernel
    <div id="round-one-clean-up-the-system-and-kernel" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#round-one-clean-up-the-system-and-kernel" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>First, I put the package and kernel state back into something predictable.</p>
<p>The XanMod source was still using the old form:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">deb <span class="o">[</span>signed-by<span class="o">=</span>/etc/apt/keyrings/xanmod-archive-keyring.gpg<span class="o">]</span> http://deb.xanmod.org releases main</span></span></code></pre></div></div>
<p>For Debian 13, I changed it to the codename-based source:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">deb <span class="o">[</span>signed-by<span class="o">=</span>/etc/apt/keyrings/xanmod-archive-keyring.gpg<span class="o">]</span> http://deb.xanmod.org trixie main</span></span></code></pre></div></div>
<p>Then I installed the current x64v3 XanMod kernel:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt update
</span></span><span class="line"><span class="cl">apt install linux-xanmod-x64v3
</span></span><span class="line"><span class="cl">reboot</span></span></code></pre></div></div>
<p>After reboot:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uname -r
</span></span><span class="line"><span class="cl"><span class="c1"># 7.0.12-x64v3-xanmod1</span></span></span></code></pre></div></div>
<p>Old kernels were removed as well, leaving only the current XanMod packages and avoiding stale boot entries:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt autoremove --purge
</span></span><span class="line"><span class="cl">apt clean
</span></span><span class="line"><span class="cl">update-grub</span></span></code></pre></div></div>
<p>I also cleaned journald and unused Docker objects:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl --vacuum-time<span class="o">=</span>7d
</span></span><span class="line"><span class="cl">docker system prune -af</span></span></code></pre></div></div>
<p>This did not remove Docker volumes. Business data should not be part of a cleanup shortcut.</p>

<h2 class="relative group">Round two: give 512MB RAM a fallback path
    <div id="round-two-give-512mb-ram-a-fallback-path" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#round-two-give-512mb-ram-a-fallback-path" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The machine has only about 454MiB of usable RAM. “Hope the applications behave” is not an operating model. The system needs defined escape routes when memory gets tight.</p>

<h3 class="relative group">Expand swap
    <div id="expand-swap" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#expand-swap" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I expanded swap to 2GB:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">swapoff /swapfile
</span></span><span class="line"><span class="cl">fallocate -l 2G /swapfile
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="cl">mkswap /swapfile
</span></span><span class="line"><span class="cl">swapon /swapfile</span></span></code></pre></div></div>
<p><code>/etc/fstab</code> stays simple:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-fstab" data-lang="fstab">/swapfile none swap sw 0 0</code></pre></div>

<h3 class="relative group">Change zswap to zstd
    <div id="change-zswap-to-zstd" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#change-zswap-to-zstd" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>zswap was already enabled, but its default compressor was <code>lzo</code>. I checked that the kernel supported zstd:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep -i zstd /proc/crypto
</span></span><span class="line"><span class="cl">grep CONFIG_CRYPTO_ZSTD /boot/config-<span class="k">$(</span>uname -r<span class="k">)</span></span></span></code></pre></div></div>
<p>Runtime switch:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> zstd > /sys/module/zswap/parameters/compressor
</span></span><span class="line"><span class="cl">cat /sys/module/zswap/parameters/compressor
</span></span><span class="line"><span class="cl"><span class="c1"># zstd</span></span></span></code></pre></div></div>
<p>Then I made it persistent in GRUB:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GRUB_CMDLINE_LINUX_DEFAULT</span><span class="o">=</span><span class="s2">"zswap.enabled=1 zswap.compressor=zstd net.ifnames=0 biosdevname=0"</span>
</span></span><span class="line"><span class="cl">update-grub</span></span></code></pre></div></div>

<h3 class="relative group">Leave some room for network and memory pressure
    <div id="leave-some-room-for-network-and-memory-pressure" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#leave-some-room-for-network-and-memory-pressure" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I added <code>/etc/sysctl.d/99-lowmem-network-tuning.conf</code>:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-conf" data-lang="conf">vm.min_free_kbytes = 16384
vm.swappiness = 30
vm.vfs_cache_pressure = 100
net.core.netdev_max_backlog = 2500</code></pre></div>
<p>I had tried a higher <code>vm.min_free_kbytes</code> first, but on a 512MB host it was too aggressive and squeezed user-space memory. Around 16MB is closer to what this machine can tolerate.</p>

<h2 class="relative group">Round three: decide what should die first
    <div id="round-three-decide-what-should-die-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#round-three-decide-what-should-die-first" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The worst low-memory behavior is everyone fighting for memory until the kernel randomly kills something critical. I wanted the priorities to be explicit.</p>

<h3 class="relative group">Protect entrypoint services
    <div id="protect-entrypoint-services" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#protect-entrypoint-services" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I added a systemd drop-in for <code>ssh</code>, <code>nginx</code>, and <code>supervisor</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">OOMScoreAdjust</span><span class="o">=</span><span class="s">-700</span></span></span></code></pre></div></div>
<p>In practice, <code>sshd</code> was already at <code>-1000</code>, while Nginx, Supervisor, and gunicorn were at <code>-700</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> pid in <span class="k">$(</span>pgrep -f <span class="s2">"sshd|nginx|supervisord|gunicorn"</span><span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s2">"%s score=%s adj=%s cmd=%s\n"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s2">"</span><span class="nv">$pid</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s2">"</span><span class="k">$(</span>cat /proc/<span class="nv">$pid</span>/oom_score<span class="k">)</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s2">"</span><span class="k">$(</span>cat /proc/<span class="nv">$pid</span>/oom_score_adj<span class="k">)</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s2">"</span><span class="k">$(</span>tr <span class="s1">'\0'</span> <span class="s1">' '</span> </proc/<span class="nv">$pid</span>/cmdline<span class="k">)</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div></div>
<p>One operational note: if SSH enters through Nginx stream, restarting <code>nginx</code> will cut the SSH session. Reload if possible, or be ready to reconnect.</p>

<h3 class="relative group">Limit containers
    <div id="limit-containers" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#limit-containers" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The proxy and file-service containers got resource limits:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hysteria</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">96m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">memswap_limit</span><span class="p">:</span><span class="w"> </span><span class="l">160m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pids_limit</span><span class="p">:</span><span class="w"> </span><span class="m">128</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">oom_score_adj</span><span class="p">:</span><span class="w"> </span><span class="m">500</span></span></span></code></pre></div></div>
<p>The final limits were:</p>
<table>
  <thead>
      <tr>
          <th>Container</th>
          <th style="text-align: right">mem_limit</th>
          <th style="text-align: right">memswap_limit</th>
          <th style="text-align: right">pids_limit</th>
          <th style="text-align: right">oom_score_adj</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>hysteria</td>
          <td style="text-align: right">96m</td>
          <td style="text-align: right">160m</td>
          <td style="text-align: right">128</td>
          <td style="text-align: right">500</td>
      </tr>
      <tr>
          <td>hysteria2</td>
          <td style="text-align: right">96m</td>
          <td style="text-align: right">160m</td>
          <td style="text-align: right">128</td>
          <td style="text-align: right">500</td>
      </tr>
      <tr>
          <td>tuic-server</td>
          <td style="text-align: right">64m</td>
          <td style="text-align: right">128m</td>
          <td style="text-align: right">128</td>
          <td style="text-align: right">500</td>
      </tr>
      <tr>
          <td>filebrowser</td>
          <td style="text-align: right">96m</td>
          <td style="text-align: right">160m</td>
          <td style="text-align: right">128</td>
          <td style="text-align: right">500</td>
      </tr>
  </tbody>
</table>
<p>One small trap: the Debian-packaged Docker on this host is <code>26.1.5+dfsg1</code>, and <code>docker update</code> does not support changing <code>--oom-score-adj</code> dynamically:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker update --oom-score-adj <span class="m">500</span> hysteria
</span></span><span class="line"><span class="cl"><span class="c1"># unknown flag: --oom-score-adj</span></span></span></code></pre></div></div>
<p>So <code>oom_score_adj</code> belongs in compose, followed by a container recreation:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d --force-recreate</span></span></code></pre></div></div>
<p>Verification:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect hysteria hysteria2 tuic-server filebrowser <span class="se">\
</span></span></span><span class="line"><span class="cl">  --format <span class="s2">"{{.Name}} OOM={{.HostConfig.OomScoreAdj}} Mem={{.HostConfig.Memory}} Swap={{.HostConfig.MemorySwap}} Pids={{.HostConfig.PidsLimit}}"</span></span></span></code></pre></div></div>

<h3 class="relative group">Add earlyoom
    <div id="add-earlyoom" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#add-earlyoom" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Finally, I added earlyoom so something acts before the kernel reaches a real OOM:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt install earlyoom</span></span></code></pre></div></div>
<p><code>/etc/default/earlyoom</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">EARLYOOM_ARGS</span><span class="o">=</span><span class="s2">"-m 10,5 -s 20,10 -r 300 --prefer '(^|/)(hysteria|tuic-server|filebrowser)( |</span>$<span class="s2">)' --avoid '(^|/)(sshd|sshd-session|nginx|supervisord|gunicorn|systemd|dockerd|containerd)( |:|</span>$<span class="s2">)'"</span></span></span></code></pre></div></div>
<p>This does not conflict with systemd <code>OOMScoreAdjust</code>. <code>OOMScoreAdjust</code> changes <code>/proc/*/oom_score</code>, and earlyoom also uses those scores by default. <code>--avoid</code> is an extra “do not actively kill these entrypoint services” layer.</p>
<p>The service log shows the policy clearly:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Preferring to kill process names that match regex '(^|/)(hysteria|tuic-server|filebrowser)( |$)'
</span></span><span class="line"><span class="cl">Will avoid killing process names that match regex '(^|/)(sshd|sshd-session|nginx|supervisord|gunicorn|systemd|dockerd|containerd)( |:|$)'
</span></span><span class="line"><span class="cl">sending SIGTERM when mem avail <= 10.00% and swap free <= 20.00%,
</span></span><span class="line"><span class="cl">        SIGKILL when mem avail <=  5.00% and swap free <= 10.00%</span></span></code></pre></div></div>

<h2 class="relative group">Stop logs from eating the disk
    <div id="stop-logs-from-eating-the-disk" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#stop-logs-from-eating-the-disk" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The root disk is small too, so journald and Docker logs need limits.</p>
<p><code>/etc/systemd/journald.conf.d/99-vps-limits.conf</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Journal]</span>
</span></span><span class="line"><span class="cl"><span class="na">SystemMaxUse</span><span class="o">=</span><span class="s">128M</span>
</span></span><span class="line"><span class="cl"><span class="na">SystemKeepFree</span><span class="o">=</span><span class="s">512M</span>
</span></span><span class="line"><span class="cl"><span class="na">RuntimeMaxUse</span><span class="o">=</span><span class="s">16M</span>
</span></span><span class="line"><span class="cl"><span class="na">MaxRetentionSec</span><span class="o">=</span><span class="s">7day</span>
</span></span><span class="line"><span class="cl"><span class="na">RateLimitIntervalSec</span><span class="o">=</span><span class="s">30s</span>
</span></span><span class="line"><span class="cl"><span class="na">RateLimitBurst</span><span class="o">=</span><span class="s">1000</span></span></span></code></pre></div></div>
<p>Docker daemon defaults:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"log-driver"</span><span class="p">:</span> <span class="s2">"json-file"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"log-opts"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"max-size"</span><span class="p">:</span> <span class="s2">"5m"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"max-file"</span><span class="p">:</span> <span class="s2">"2"</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>I also disabled UFW logging:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ufw logging off</span></span></code></pre></div></div>
<p>There had been plenty of scan noise producing UFW BLOCK logs. On a small disk and small memory host, that noise is not worth keeping.</p>

<h2 class="relative group">Final checks
    <div id="final-checks" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#final-checks" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The closing checks looked like this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uname -r
</span></span><span class="line"><span class="cl"><span class="c1"># 7.0.12-x64v3-xanmod1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">apt list --upgradable
</span></span><span class="line"><span class="cl"><span class="c1"># Listing...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl --failed --no-pager
</span></span><span class="line"><span class="cl"><span class="c1"># 0 loaded units listed.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl is-active earlyoom ssh nginx supervisor docker containerd
</span></span><span class="line"><span class="cl"><span class="c1"># active</span>
</span></span><span class="line"><span class="cl"><span class="c1"># active</span>
</span></span><span class="line"><span class="cl"><span class="c1"># active</span>
</span></span><span class="line"><span class="cl"><span class="c1"># active</span>
</span></span><span class="line"><span class="cl"><span class="c1"># active</span>
</span></span><span class="line"><span class="cl"><span class="c1"># active</span></span></span></code></pre></div></div>
<p>Memory state:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Mem: 454Mi total, 235Mi available
</span></span><span class="line"><span class="cl">Swap: 2.0Gi total, about 271Mi used</span></span></code></pre></div></div>
<p>Entrypoint checks:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh VPS_ALIAS <span class="s1">'echo ok'</span>
</span></span><span class="line"><span class="cl">curl -sS -o /dev/null -w <span class="s1">'%{http_code}\n'</span> https://api.example.com/
</span></span><span class="line"><span class="cl">curl -sS -o /dev/null -w <span class="s1">'%{http_code}\n'</span> https://fm.example.com/</span></span></code></pre></div></div>
<p>SSH worked. The API returned an application-level 404, and the file service returned 200. The API 404 was a routing result, not Cloudflare 521 and not a dead upstream.</p>
<p>I also checked the kernel log after the tuning:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -k --since <span class="s2">"2026-06-13 06:28:32 UTC"</span> --no-pager <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  egrep -i <span class="s2">"out of memory|oom-kill|page allocation failure|killed process"</span></span></span></code></pre></div></div>
<p>No new OOM or page allocation failures showed up.</p>

<h2 class="relative group">Summary
    <div id="summary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#summary" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The main lesson: a 512MB VPS can run services, but it cannot be left on defaults.</p>
<p>This matters even more when SSH enters through Nginx stream and the API shares the same 443 entrypoint. Entrypoint services must be protected. The processes that should give way first are proxy containers, file services, and temporary jobs, not <code>sshd</code>, <code>nginx</code>, or the API supervisor.</p>
<p>Of course, all of this only pushes the boundary of 512MB a little further. The real fix is still upgrading to 1GB RAM. Low-end VPS tuning is useful, but it is not magic.</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
