{"id":190,"date":"2026-01-10T00:01:26","date_gmt":"2026-01-10T00:01:26","guid":{"rendered":"https:\/\/togo-lab.io\/?p=190"},"modified":"2026-01-10T13:51:36","modified_gmt":"2026-01-10T13:51:36","slug":"post-mortem-antivirus-integration-on-a-1-gb-nextcloud-vps-failed-due-load","status":"publish","type":"post","link":"https:\/\/togo-lab.io\/?p=190","title":{"rendered":"Post-Mortem: Antivirus Integration on a 1 GB Nextcloud VPS (failed due load)"},"content":{"rendered":"<h4>Failing Experiments Are Useful<\/h4>\n<p>Today I tried to push my togo-lab.io setup (<a href=\"https:\/\/tgonet.de\/\">see 2nd block at my landing page for all services<\/a>) a bit further than strictly necessary. So maybe you noticed some outages. <\/p>\n<p>Why I did this? Not only for production security reasons, but also out of curiosity: How far can I go with limited resources? I want to understand where the real limits for my setup are.<\/p>\n<p>In this experiment, I wanted to see whether a single small VPS could reasonably host Nextcloud, a Matrix server, and Gitea, and still handle antivirus scanning for file uploads. At first glance it looked tight, but maybe possible. In practice, it pushed this small VPS over the edge.<\/p>\n<p>That outcome was useful for my learning and understanding. When things fail or become unstable, I usually learn much more than when everything works smoothly. As a technician, breaking things on purpose for learning is, for me, typically the fastest way to understand them.<\/p>\n<p>The following section is a post-mortem of that experiment: what it revealed, what I learned from it, and what would be required if I want to add antivirus scanning in the future.<\/p>\n<hr \/>\n<p><strong>Context:<\/strong><br \/>\nThis server hosts multiple self-managed services on a small VPS (1 GB RAM, 1 vCPU):<\/p>\n<ul>\n<li>Nextcloud (primary collaboration platform)<\/li>\n<li>Matrix server<\/li>\n<li>Gitea<\/li>\n<li>Supporting stack (PHP-FPM, MariaDB, Redis, Apache\/Nginx, Fail2Ban)<\/li>\n<\/ul>\n<p>The goal was to add <strong>antivirus scanning for uploaded files<\/strong> in Nextcloud, as preparation for future collaborative use.<\/p>\n<hr \/>\n<h4>Initial Goal<\/h4>\n<p>Enable server-side antivirus scanning for Nextcloud uploads using <strong>ClamAV<\/strong>, with the following constraints:<\/p>\n<ul>\n<li>Lightweight<\/li>\n<li>Automated<\/li>\n<li>No interactive maintenance<\/li>\n<li>Suitable for a self-hosted environment<\/li>\n<\/ul>\n<p>This is a reasonable baseline requirement once multiple external contributors are involved.<\/p>\n<hr \/>\n<h4>Attempted Approaches<\/h4>\n<h5>1. ClamAV Daemon (<code>clamd<\/code>) + Nextcloud (Socket Mode)<\/h5>\n<p><strong>What was tried<\/strong><\/p>\n<ul>\n<li>\n<p>Installed ClamAV daemon<\/p>\n<\/li>\n<li>\n<p>Tuned <code>clamd.conf<\/code> aggressively (single thread, reduced parsers, size limits)<\/p>\n<\/li>\n<li>\n<p>Added strict systemd memory limits<\/p>\n<\/li>\n<li>\n<p>Disabled background scans<\/p>\n<\/li>\n<li>\n<p>Socket-based integration with Nextcloud<\/p>\n<\/li>\n<li>\n<p>**changes to the default <code>\/etc\/clamav\/clamd.conf<\/code><\/p>\n<pre><code>      # === VPS-safe limits ===\n      MaxThreads 1\n      ConcurrentDatabaseReload no\n\n      # File size limits (Nextcloud uploads)\n      MaxFileSize 50M\n      MaxScanSize 75M\n      StreamMaxLength 75M\n\n      # Archive \/ recursion limits\n      MaxRecursion 10\n      MaxFiles 5000\n\n      # Timeouts\n      ReadTimeout 120\n      CommandReadTimeout 120\n\n      # Disable low-value \/ memory-heavy scanners\n      ScanHTML false\n      ScanMail false\n      ScanSWF false\n      ScanHWP3 false\n      ScanXMLDOCS false\n\n      # Reduce bytecode impact\n      Bytecode true\n      BytecodeTimeout 20000\n\n      # Reduce RAM further (Nextcloud upload use-case)\n      PhishingSignatures false\n      PhishingScanURLs false\n      DisableCache true\n      ExtendedDetectionInfo false<\/code><\/pre>\n<\/li>\n<li>\n<p>best I got, via <code>free -h<\/code><\/p>\n<\/li>\n<\/ul>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: right\"><\/th>\n<th style=\"text-align: left\">total<\/th>\n<th style=\"text-align: left\">used<\/th>\n<th style=\"text-align: left\">free<\/th>\n<th style=\"text-align: left\">shared<\/th>\n<th style=\"text-align: left\">buff\/cache<\/th>\n<th style=\"text-align: left\">available<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: right\">Mem:<\/td>\n<td style=\"text-align: left\">960Mi<\/td>\n<td style=\"text-align: left\">926Mi<\/td>\n<td style=\"text-align: left\">68Mi<\/td>\n<td style=\"text-align: left\">31Mi<\/td>\n<td style=\"text-align: left\">101Mi<\/td>\n<td style=\"text-align: left\">33Mi<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right\">Swap:<\/td>\n<td style=\"text-align: left\">1.0Gi<\/td>\n<td style=\"text-align: left\">843Mi<\/td>\n<td style=\"text-align: left\">180Mi<\/td>\n<td style=\"text-align: left\">&#8211;<\/td>\n<td style=\"text-align: left\">&#8211;<\/td>\n<td style=\"text-align: left\">&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<pre><code>    $ swapon --show\n    NAME      TYPE  SIZE   USED PRIO\n    \/swapfile file 1024M 848.5M   -2`<\/code><\/pre>\n<p><strong>Observed behavior<\/strong><\/p>\n<ul>\n<li><code>clamd<\/code> resident memory usage: ~500\u2013600 MB<\/li>\n<li>Heavy swap usage even after tuning<\/li>\n<li>Periodic stalls, SSH lag, partial service unresponsiveness<\/li>\n<li>OOM kills during database reload or startup<\/li>\n<\/ul>\n<p><strong>Conclusion<\/strong>\nEven heavily tuned, <strong>resident ClamAV is not viable<\/strong> on a 1 GB VPS that already runs multiple services.<\/p>\n<hr \/>\n<h5>2. ClamAV Executable Mode (<code>clamscan<\/code> on upload)<\/h5>\n<p><strong>What was tried<\/strong><\/p>\n<ul>\n<li>Disabled <code>clamd<\/code> entirely<\/li>\n<li>Used Nextcloud <em>Antivirus for Files<\/em> app in <strong>Executable mode<\/strong><\/li>\n<li>Scanning only on upload<\/li>\n<li>Strict size limits<\/li>\n<li>No background scans<\/li>\n<\/ul>\n<p>FYI <strong>Final authoritative configuration<\/strong> in NextCloud App &#8220;Antivirus for Files&#8221;<\/p>\n<ul>\n<li>Mode: ClamAV <code>Executable<\/code><\/li>\n<li>Path to clamscan: <code>\/usr\/bin\/clamscan<\/code><\/li>\n<li>Extra command line options (comma-separated):\n<code>--no-summary,--infected,--max-filesize=50M,--max-scansize=75M<\/code><\/li>\n<li>Stream Length: <code>104857600<\/code><\/li>\n<li>Block uploads when scanner is not reachable: <code>Yes<\/code><\/li>\n<li>Block unscannable files: <code>No<\/code><\/li>\n<li>Background scans: effectively <code>off<\/code> (unchecked)<\/li>\n<\/ul>\n<p><strong>Observed behavior<\/strong><\/p>\n<ul>\n<li>Technically functional<\/li>\n<li>No permanent memory footprint<\/li>\n<li>However:\n<ul>\n<li>Uploads caused noticeable CPU + IO spikes<\/li>\n<li>PHP-FPM workers stalled under load<\/li>\n<li>Combined service activity still led to instability<\/li>\n<\/ul><\/li>\n<\/ul>\n<p><strong>Conclusion<\/strong>\nEven non-resident scanning <strong>adds too much peak load<\/strong> for this VPS when combined with:<\/p>\n<ul>\n<li>Nextcloud<\/li>\n<li>Matrix<\/li>\n<li>Gitea<\/li>\n<li>Database and cache services<\/li>\n<\/ul>\n<hr \/>\n<h4>Final Decision<\/h4>\n<h5>Antivirus disabled (for now)<\/h5>\n<p>The <strong>Nextcloud Antivirus app is currently disabled<\/strong>.<\/p>\n<p>Reasons:<\/p>\n<ul>\n<li>System stability has higher priority than partial security measures<\/li>\n<li>Trusted users only<\/li>\n<li>Strict file permissions<\/li>\n<li>Regular backups<\/li>\n<li>No public upload endpoints<\/li>\n<\/ul>\n<p>After disabling AV and rebooting:<\/p>\n<ul>\n<li>System became stable<\/li>\n<li>Swap usage normalized<\/li>\n<li>All services responsive:\n<ul>\n<li>Nextcloud<\/li>\n<li>Matrix<\/li>\n<li>Gitea<\/li>\n<\/ul><\/li>\n<\/ul>\n<hr \/>\n<h4>Post-Mortem Summary<\/h4>\n<table>\n<thead>\n<tr>\n<th>Item<\/th>\n<th>Result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Configuration error<\/td>\n<td>\u274c No<\/td>\n<\/tr>\n<tr>\n<td>ClamAV bug<\/td>\n<td>\u274c No<\/td>\n<\/tr>\n<tr>\n<td>Nextcloud bug<\/td>\n<td>\u274c No<\/td>\n<\/tr>\n<tr>\n<td>VPS resource limit<\/td>\n<td>\u2705 Yes<\/td>\n<\/tr>\n<tr>\n<td>Wrong architecture<\/td>\n<td>\u2705 Yes (for this size)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Meaning: <\/p>\n<ul>\n<li>This was <strong>not a misconfiguration<\/strong>.  <\/li>\n<li>It was a <strong>capacity mismatch<\/strong>.<\/li>\n<\/ul>\n<hr \/>\n<h4>Lessons Learned<\/h4>\n<ol>\n<li>\n<p><strong>1 GB VPS is already at the limit<\/strong> for:<\/p>\n<ul>\n<li>Nextcloud<\/li>\n<li>Matrix<\/li>\n<li>Gitea<br \/>\ncombined.<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Antivirus scanning is loadwise <strong>not \u201cfree\u201d<\/strong>, even in executable mode.<\/p>\n<\/li>\n<li>\n<p>Security features that trigger <strong>CPU + IO spikes<\/strong> must be sized for <em>worst-case concurrency<\/em>, not idle averages.<\/p>\n<\/li>\n<li>\n<p>Adding AV without increasing resources creates <strong>negative security<\/strong> by destabilizing the system.<\/p>\n<\/li>\n<\/ol>\n<hr \/>\n<h4>When Antivirus Will Be Re-Enabled<\/h4>\n<p>Antivirus scanning <strong>will be mandatory<\/strong> once this instance is used for real group collaboration.<\/p>\n<p>That will require <strong>one of the following<\/strong> options:<\/p>\n<h5>Option A \u2014 VPS Upgrade (actual preferred)<\/h5>\n<ul>\n<li>Upgrade to <strong>\u2265 2 GB RAM<\/strong><\/li>\n<li>Re-enable ClamAV (daemon or executable mode)<\/li>\n<li>Keep all services on one host<\/li>\n<\/ul>\n<h5>Option B \u2014 Service Split<\/h5>\n<ul>\n<li>VPS 1: Nextcloud<\/li>\n<li>VPS 2: Matrix + Gitea<\/li>\n<li>Antivirus enabled only on Nextcloud host<\/li>\n<\/ul>\n<hr \/>\n<h4>Current Security Posture (Interim)<\/h4>\n<ul>\n<li>If, than only trusted users<\/li>\n<li>No public upload endpoints<\/li>\n<li>Strict permissions<\/li>\n<li>Fail2Ban + firewall<\/li>\n<li>Frequent backups<\/li>\n<li>Fast restore tested<\/li>\n<\/ul>\n<p>This is acceptable <strong>temporarily<\/strong>, but <strong>not a final state<\/strong>.<\/p>\n<hr \/>\n<h4>Closing Notes<\/h4>\n<p>This experiment was intentional and valuable, learned a lot, also during configuration and tuning.<\/p>\n<p>It clarified:<\/p>\n<ul>\n<li>the real resource cost of antivirus scanning<\/li>\n<li>the practical limits of small VPS setups<\/li>\n<li>and the architectural decisions required for future growth<\/li>\n<\/ul>\n<p>When collaboration expands, <strong>the infrastructure will be expanded accordingly<\/strong>.\nSo clamav and configuration stays, but Nextcloud App is disabled for now, but not uninstalled.<\/p>","protected":false},"excerpt":{"rendered":"<p>Failing Experiments Are Useful Today I tried to push my togo-lab.io setup (see 2nd block at my landing page for all services) a bit further than strictly necessary. So maybe you noticed some outages. Why I did this? Not only for production security reasons, but also out of curiosity: How far can I go with &hellip; <a href=\"https:\/\/togo-lab.io\/?p=190\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Post-Mortem: Antivirus Integration on a 1 GB Nextcloud VPS (failed due load)&#8221;<\/span><\/a><\/p>","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[5,16],"post_folder":[],"class_list":["post-190","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-engine-room-log","tag-linux"],"_links":{"self":[{"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/posts\/190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/togo-lab.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=190"}],"version-history":[{"count":7,"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/posts\/190\/revisions"}],"predecessor-version":[{"id":197,"href":"https:\/\/togo-lab.io\/index.php?rest_route=\/wp\/v2\/posts\/190\/revisions\/197"}],"wp:attachment":[{"href":"https:\/\/togo-lab.io\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/togo-lab.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/togo-lab.io\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=190"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/togo-lab.io\/index.php?rest_route=%2Fwp%2Fv2%2Fpost_folder&post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}