<?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: shangyin</title>
    <description>The latest articles on DEV Community by shangyin (@shangyin).</description>
    <link>https://dev.to/shangyin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071637%2Ff32f2c4a-be4c-4256-9303-67e3312912dd.png</url>
      <title>DEV Community: shangyin</title>
      <link>https://dev.to/shangyin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shangyin"/>
    <language>en</language>
    <item>
      <title>Building a Lightweight Knowledge Base for a Small Team: Migrating from Nextcloud to Cloudreve</title>
      <dc:creator>shangyin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 16:34:15 +0000</pubDate>
      <link>https://dev.to/shangyin/building-a-lightweight-knowledge-base-for-a-small-team-migrating-from-nextcloud-to-cloudreve-42fi</link>
      <guid>https://dev.to/shangyin/building-a-lightweight-knowledge-base-for-a-small-team-migrating-from-nextcloud-to-cloudreve-42fi</guid>
      <description>&lt;p&gt;One server, two containers, 1.6 GB RAM—handling online preview and streaming for 100 GB of company assets.  &lt;/p&gt;

&lt;p&gt;Plus three gnarly bugs that took me the better part of a day to track down.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why We Ditched Nextcloud&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our company has about two dozen people. Over the years, we accumulated more than 100 GB of data on Nextcloud: product docs, project artifacts, training videos, and presales decks. The data is valuable, but the system became increasingly painful to use:&lt;/p&gt;

&lt;p&gt;• Heavy. Nextcloud (PHP) + OnlyOffice Document Server routinely consumed over 4 GB of RAM, with OnlyOffice alone eating 2–3 GB.&lt;/p&gt;

&lt;p&gt;• Wrong mental model. Nextcloud assumes "everyone gets their own cloud drive." What we needed was "the company has one shared knowledge base." The workaround? Everything lived in a single shared account; the other hundred-odd accounts were effectively dead weight.&lt;/p&gt;

&lt;p&gt;• Feature bloat. Calendar, chat, tasks, mail, Kanban—none of it used.&lt;/p&gt;

&lt;p&gt;Our actual needs were simple: browse directories, preview documents and videos online, stream videos, and download files. Paying 4 GB of RAM and a pile of unused modules for that was a bad trade.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choosing the Stack: Where the Lightweight Wins Come From&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final setup is Cloudreve v4 + fileview, running as two Docker containers.&lt;/p&gt;

&lt;p&gt;Old Stack New Stack&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Core Nextcloud (PHP) Cloudreve v4 (Go single binary)&lt;/li&gt;
&lt;li&gt;Office Preview OnlyOffice Document Server fileview (Java)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RAM Usage ~4 GB ~1.6 GB&lt;/p&gt;

&lt;p&gt;Core Process RAM ~800 MB (php-fpm multiprocess) ~170 MB&lt;/p&gt;

&lt;p&gt;Cloudreve is written in Go. A single binary, ~145 MB idle, ~170 MB under load with 100 GB of data. It’s a Chinese project with a native UI, built-in directory management, share links, a video player (Artplayer), and WebDAV support.&lt;/p&gt;

&lt;p&gt;A pleasant surprise: Cloudreve v4’s official image ships with ffmpeg, LibreOffice, and libvips (which explains the 1.3 GB image size). Video thumbnails and Office file thumbnails work out of the box—no extra installs.&lt;/p&gt;

&lt;p&gt;A honest note on “lightweight”: High-fidelity online Office preview always requires a separate conversion service, no matter which file manager you use. Fileview takes ~1.25 GB, better than OnlyOffice’s 2–3 GB, but not zero cost. If you can live with “click to download, open locally,” the whole stack can run under 200 MB.&lt;/p&gt;

&lt;p&gt;The architecture is straightforward:&lt;br&gt;
  ├─→ :5212  Cloudreve   Main entry: browsing, download, video playback, sharing&lt;br&gt;
  └─→ :8012  fileview    Loaded only when opening Office files&lt;/p&gt;

&lt;p&gt;No Nginx reverse proxy. For internal HTTP use, it’s just extra config and a failure point. Cloudreve’s built-in HTTP server is sufficient. We’ll add HTTPS when exposing it to the public internet—no architectural changes needed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Data Migration: Recon Before You Move&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With 100+ GB to move, the biggest mistake is “start copying immediately.” We spent 20 minutes doing read-only reconnaissance, and it changed our plan.&lt;/p&gt;

&lt;p&gt;What the Recon Revealed&lt;/p&gt;

&lt;p&gt;Running du -sm */ on the Nextcloud data directory showed the distribution across 87 user directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;117 GB   one shared account      ← 93% of total
3.2 GB   userA
1.5 GB   userB
1.2 GB   userC
...
37 MB    userD
37 MB    userE                  ← many accounts at exactly 37 MB
37 MB    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those 37 MB accounts? Default sample files (welcome PDF, sample images) created when Nextcloud provisions a new user. Many accounts were never actually used.&lt;/p&gt;

&lt;p&gt;Drilling into the 117 GB shared account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89 GB   files/              ← real files
26 GB   files_trashbin/     ← trash
1.8 GB  files_versions/     ← version history

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trash and version history accounted for 28 GB—none of which needed migrating.&lt;/p&gt;

&lt;p&gt;Another finding: several accounts that looked like they had a few GB of data only had tens of MB in files/; the rest was trash. Three accounts had zero files.&lt;/p&gt;

&lt;p&gt;Conclusion: Of the apparent 126 GB, only 87 GB needed to move. Of 87 accounts, only 4 had real content.&lt;/p&gt;

&lt;p&gt;We also tallied file types, which informed the preview strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;758  mp4      ← browser-playable, no transcoding needed
671  docx
326  pdf
237  pptx
199  xlsx
143  md
131  xmind    ← no previewer, download only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why You Can’t Just Copy Files&lt;/p&gt;

&lt;p&gt;The naive approach: “just copy files into Cloudreve’s storage directory.” Doesn’t work.&lt;/p&gt;

&lt;p&gt;Cloudreve stores filenames and directory structures in a database. On disk, files have randomized prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uploads/1/some-dir/1_MYEphK73_filename.docx
              ↑ user ID  ↑ random string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copying files directly leaves Cloudreve unaware of them. You must go through the upload interface.&lt;/p&gt;

&lt;p&gt;Cloudreve v4 has a POST /api/v4/workflow/import endpoint that claims to scan existing directories and build indexes. I burned a lot of time here: src requires a relative path (discovered by trial and error), dst formatting never quite worked, and user_id expects a hashed ID, not a numeric one. The closest I got was a log showing Importing 303 physical files, but files landed in a malformed location—and each failure left “imported” markers in the DB, causing retries to be skipped. Orphaned records everywhere.&lt;/p&gt;

&lt;p&gt;I abandoned this and fell back to the simplest, most verifiable approach: rsync to a staging directory + rclone upload via WebDAV. Slower, but every step is inspectable, retryable, and reversible.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. rsync from old server to local staging (gigabit LAN, ~90–100 MB/s)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress2 &lt;span class="nt"&gt;--rsync-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sudo rsync"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  user@old-server:/path/to/nextcloud/data/shared-account/files/ /srv/staging/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  2. rclone upload via WebDAV (observed ~40–75 MB/s)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone copy /srv/staging cr: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfers&lt;/span&gt; 16 &lt;span class="nt"&gt;--ignore-existing&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;".attachments.*/**"&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"**/.DS_Store"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: The WebDAV password must be created separately in Cloudreve—it’s not your login password.&lt;/p&gt;

&lt;p&gt;Total time: rsync 15 min + upload 49 min. 3,199 files, zero failures.&lt;/p&gt;

&lt;p&gt;Post-migration verification: directory-by-directory file counts matched (11 directories), and random MD5 checks on 10 files (including a 1.6 GB video) all matched. Only then did we delete the 88 GB staging directory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Three Gotchas&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real time sinks weren’t setup—they were these three issues.&lt;/p&gt;

&lt;p&gt;Gotcha 1: Misdiagnosing IO Contention as a Performance Bottleneck&lt;/p&gt;

&lt;p&gt;During upload testing, I observed 1 file every 12 seconds. At that rate, 3,000 files would take 10 hours.&lt;/p&gt;

&lt;p&gt;My first instinct: “Cloudreve spawns LibreOffice/ffmpeg per file for thumbnails; CPU is saturated.” I was about to disable thumbnails.&lt;/p&gt;

&lt;p&gt;Then I checked actual resource usage:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cloudreve: MEM 192MiB  CPU 0.06%&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;CPU at 0.06%—the service was practically idle. Thumbnails weren’t the bottleneck.&lt;/p&gt;

&lt;p&gt;The real cause: rsync was saturating disk IO at 100 MB/s, and the upload was fighting for the same disk. After rsync finished, I retested:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;116 files / 161 MB, 4.3 seconds, ~38 MB/s&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;From 12 seconds/file to 27 files/second. No configuration changes—just no longer fighting for IO.&lt;/p&gt;

&lt;p&gt;Lesson: Check resource usage before “optimizing.” If I’d disabled thumbnails, I’d have wasted effort, concluded “that didn’t help,” and gone down a rabbit hole of false leads.&lt;/p&gt;

&lt;p&gt;Gotcha 2: “File Encrypted, Enter Password”—Except It Wasn’t&lt;/p&gt;

&lt;p&gt;This one was fun. After wiring up Office preview, every Word/Excel/PPT file opened with: “File is encrypted, please enter password.” These files opened fine locally and had never been password-protected.&lt;/p&gt;

&lt;p&gt;Checking fileview logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 File size: 91 bytes
✅ File download success - LocalPath: /opt/fileview/data/downloads/xxx.docx
⚠️ Error: Invalid header signature; read 0x3A2265646F63227B,
         expected 0xE11AB1A1E011CFD0
🔒 Office document requires password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;91-byte docx? That header 0x3A2265646F63227B decodes to {"code":.&lt;/p&gt;

&lt;p&gt;It wasn’t a docx—it was JSON. Catting those 91 bytes:&lt;br&gt;
{"code":40071,"msg":"sign expired"}&lt;/p&gt;

&lt;p&gt;Sign expired. But I could manually download the same URL minutes later without issue. Comparing the browser’s request to my manual one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mine:    sign=U14xJjCuRZ2PBwObURMH0Hk3qyn2k14LTO2Ba1gTcLY%3D%3A1786345482
Browser: sign=ZJYidJHcBN4qVby-0Y4K6-yx60DlQbo2KFXDawt1aCw
                                                    ↑ stops here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature itself matched, but the trailing %3D%3A was missing (%3D%3A decodes to =:).&lt;/p&gt;

&lt;p&gt;Digging into fileview’s frontend bundle, I found the URL parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← here&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sign==: split on = produces three parts: ["sign", "", ":"]. Destructuring [c, d] only captures the first two; the third is permanently lost. Reassembly only reconstructs c=d.&lt;/p&gt;

&lt;p&gt;The full causal chain:&lt;/p&gt;

&lt;p&gt;Signature truncated by frontend&lt;br&gt;
  → Cloudreve receives a broken signature, returns 91-byte "sign expired" JSON&lt;br&gt;
  → fileview treats the JSON as a docx&lt;br&gt;
  → detects it’s neither ZIP (docx is a zip) nor OLE2 (legacy doc)&lt;br&gt;
  → code says "conservatively assume encrypted"&lt;br&gt;
  → frontend shows "file encrypted, enter password"&lt;/p&gt;

&lt;p&gt;A URL parsing bug, manifesting as “file encryption.” Highly misleading—without logs, you’d chase file encoding, permissions, or corruption.&lt;/p&gt;

&lt;p&gt;The fix is simple: split only on the first =:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;_i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this patches a bundled JS file inside a container, persistence matters. Mount the patched file as a read-only volume in docker-compose:&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;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;./patched-js/index-XXXX.js:/opt/fileview/frontend/dist/js/index-XXXX.js:ro&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Note: frontend filenames include hashes; after an image upgrade, the hash changes. Document this or the mount will silently break on upgrade.)&lt;/p&gt;

&lt;p&gt;One more detail: fileview caches failure states. After patching, old files still reported “password required” because it served cached results. Clear the cache or re-trigger conversion.&lt;/p&gt;

&lt;p&gt;Gotcha 3: An Unauthenticated SSRF&lt;/p&gt;

&lt;p&gt;Before exposing the service publicly, I ran a security check and found fileview’s /preview/api/netFile endpoint: no authentication, accepts arbitrary URLs.&lt;/p&gt;

&lt;p&gt;Its intended purpose: “download this file from a URL and convert it.” With no source restrictions, it’s an SSRF vector. Testing against an internal server:&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;-X&lt;/span&gt; POST http://server:8012/preview/api/netFile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"networkFileUrl":"http://192.168.x.x/","fileName":"t.docx"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  {"code":0, ...}   accepted
&lt;/h1&gt;

&lt;p&gt;Logs confirmed the outbound request:&lt;/p&gt;

&lt;p&gt;🌐 Starting network file download - URL: &lt;a href="http://192.168.x.x" rel="noopener noreferrer"&gt;http://192.168.x.x&lt;/a&gt;&lt;br&gt;
❗ Download failed - Error: HTTP connect timed out&lt;/p&gt;

&lt;p&gt;Classic SSRF. Error messages (timeout vs. connection refused vs. 404) leak port status.&lt;/p&gt;

&lt;p&gt;Fortunately, fileview ships with a whitelist—no code changes needed:&lt;br&gt;
environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FILEVIEW_NETWORK_SECURITY_TRUSTEDSITES=,127.0.0.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This maps to fileview.network.security.trusted-sites (comma-separated; empty means unrestricted).&lt;/p&gt;

&lt;p&gt;Small pitfall: including the port (e.g., IP:5212) causes even legitimate requests to be rejected—the comparison is host-only, no port.&lt;/p&gt;

&lt;p&gt;Post-fix verification: internal hosts, external addresses, gateways, and cloud metadata endpoints (169.254.169.254) all returned “access denied.” Six common bypass techniques failed—@ userinfo confusion (&lt;a href="http://trusted@target/" rel="noopener noreferrer"&gt;http://trusted@target/&lt;/a&gt;), decimal IPs (&lt;a href="http://3232261131/" rel="noopener noreferrer"&gt;http://3232261131/&lt;/a&gt;), hex IPs, DNS tricks like .nip.io, and file:// protocol attempts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-Public Internet Security Checklist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For internal use, the setup is fine. Before exposing it publicly, these items are mandatory:&lt;/p&gt;

&lt;p&gt;Issue Reality Check&lt;/p&gt;

&lt;p&gt;Preview service port must not be exposed fileview has zero authentication. Beyond SSRF, all its endpoints are open.&lt;/p&gt;

&lt;p&gt;No brute-force protection on login 8+ failed passwords all return “wrong password”—no rate limiting, no lockout, no CAPTCHA. Must be handled at the reverse proxy layer.&lt;/p&gt;

&lt;p&gt;Plain HTTP Passwords and file contents transmitted in cleartext.&lt;/p&gt;

&lt;p&gt;Admin interfaces Authenticated, but best practice: block external access at the proxy to reduce attack surface.&lt;/p&gt;

&lt;p&gt;What’s already safe: Cloudreve’s own authentication is solid (unauthenticated file access returns 401; admin endpoints return 404). User registration is disabled by default.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Rough Edges&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Write-ups tend to focus on wins. Here are the real shortcomings of this setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Video format limitations. Browsers only play mp4 (H.264) / webm. AVI/MKV/FLV/WMV can be uploaded and downloaded, but not streamed—there’s no transcoding pipeline in the image. We got lucky: all 758 videos were mp4/mov.&lt;/li&gt;
&lt;li&gt;No true “team space.” Cloudreve v4 Community Edition embeds user IDs in storage paths, enforcing physical isolation. There’s no shared container concept. To share, you use share links (directory-level; subdirectories are included automatically). Recipients can “save shortcut” to their own file list, approximating a shared space—but there’s no “share directly with account X” workflow.&lt;/li&gt;
&lt;li&gt;Accounts as ownership boundaries. Files belong to the creator. To transfer ownership, you either change the account’s email or re-upload—because storage paths include the user ID.&lt;/li&gt;
&lt;li&gt;No redundancy. This is the biggest gap. 100 GB lives on a single disk. Disk failure = total loss. Local backups help little; an external backup target is essential.&lt;/li&gt;
&lt;li&gt;No preview for niche formats like XMind—download only.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hope this helps someone evaluating a lighter alternative to Nextcloud. Happy to answer questions about the migration or the patches.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>docker</category>
      <category>performance</category>
      <category>software</category>
    </item>
  </channel>
</rss>
