<?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: Tatsuo Takahashi</title>
    <description>The latest articles on DEV Community by Tatsuo Takahashi (@tspeedcobra).</description>
    <link>https://dev.to/tspeedcobra</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%2F4027299%2F07058bbb-d82a-4578-bc41-140442b5315b.png</url>
      <title>DEV Community: Tatsuo Takahashi</title>
      <link>https://dev.to/tspeedcobra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tspeedcobra"/>
    <language>en</language>
    <item>
      <title>Why Nextcloud Locked Me Out of My Own Server (and How trusted_proxies Fixed It)</title>
      <dc:creator>Tatsuo Takahashi</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:46:40 +0000</pubDate>
      <link>https://dev.to/tspeedcobra/why-nextcloud-locked-me-out-of-my-own-server-and-how-trustedproxies-fixed-it-381e</link>
      <guid>https://dev.to/tspeedcobra/why-nextcloud-locked-me-out-of-my-own-server-and-how-trustedproxies-fixed-it-381e</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I moved a client file-sharing server from &lt;strong&gt;ownCloud on a reserved EC2 instance&lt;/strong&gt; to &lt;strong&gt;Nextcloud on a $10 Lightsail box&lt;/strong&gt;. Two things drove the move: the cost no longer made sense (a 1.7 GB workload sitting on a 24/7 instance, made worse by a reserved-instance commitment and a weakening yen), and the old ownCloud/OS stack had reached end-of-life.&lt;/p&gt;

&lt;p&gt;The migration itself was trivial. Two authentication problems behind the Caddy reverse proxy were not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nextcloud locked me out with "Too many requests."&lt;/strong&gt; Behind the proxy, every request looked like it came from Caddy's container IP (&lt;code&gt;172.18.0.2&lt;/code&gt;), so Nextcloud's brute-force protection saw one IP hammering the login and blocked it. Fixed with &lt;code&gt;trusted_proxies&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic auth and Nextcloud fought over the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/strong&gt; My first fix (&lt;code&gt;header_up -Authorization&lt;/code&gt; inside &lt;code&gt;reverse_proxy&lt;/code&gt;) silently did nothing. Moving it to &lt;code&gt;request_header -Authorization&lt;/code&gt; at the site-block level fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers the cost reasoning, why I picked Nextcloud over object storage or a SaaS, and both fixes in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The background
&lt;/h2&gt;

&lt;p&gt;For a few years, my wife's design business has exchanged deliverables — banners, image assets — with a client through a self-hosted &lt;strong&gt;ownCloud&lt;/strong&gt; instance running on &lt;strong&gt;EC2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It worked, but the economics stopped making sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The actual data was &lt;strong&gt;1.7 GB&lt;/strong&gt;. That's it. Everything else on the disk was years-old junk (an 8-year-old VirtualBox installer, a &lt;code&gt;Documents&lt;/code&gt; folder nobody had touched in 7 years).&lt;/li&gt;
&lt;li&gt;To host 1.7 GB, a &lt;strong&gt;full EC2 instance ran 24/7&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;To make that "cheaper," I'd bought a &lt;strong&gt;Reserved Instance&lt;/strong&gt; — which is a commitment to &lt;em&gt;keep paying for a server I shouldn't have needed at all&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Then the &lt;strong&gt;yen weakened&lt;/strong&gt;. My AWS bill is denominated in USD, so even a "fixed" reservation quietly got more expensive in the currency I actually pay in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I was paying a premium, locked in, in a strengthening currency, to host 1.7 GB of files on an always-on server. Every layer of that sentence is a mistake.&lt;/p&gt;

&lt;p&gt;On top of the cost, the ownCloud version and its OS had reached &lt;strong&gt;end-of-life&lt;/strong&gt;. I spend a lot of my day job on security work, and knowingly exposing an EOL PHP stack to the public internet is exactly the kind of attack surface I'd flag for a client. I wanted it gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nextcloud (and not object storage or a SaaS)
&lt;/h2&gt;

&lt;p&gt;I looked at three options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare R2 + presigned URLs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cheapest (~$0/mo at this scale)&lt;/td&gt;
&lt;td&gt;Presigned URLs expire in 7 days max; the client is used to a &lt;em&gt;persistent&lt;/em&gt; folder they can revisit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SaaS (Dropbox / Google Workspace)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$10–20/mo, zero ops&lt;/td&gt;
&lt;td&gt;A completely different UI; the client would have to relearn the workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nextcloud on Lightsail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$10/mo fixed&lt;/td&gt;
&lt;td&gt;I run the box, but the client sees almost no change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deciding factor wasn't cost or ops. It was the &lt;strong&gt;client's experience&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ownCloud and Nextcloud share a lineage — Nextcloud is a fork of ownCloud — so their web UIs are nearly identical. Moving to Nextcloud meant my wife's client could keep opening the same kind of folder, see the same kind of layout, and never really notice the server underneath had changed. Object storage or a SaaS would have meant retraining a client on my schedule, for my cost problem. That's backwards.&lt;/p&gt;

&lt;p&gt;(There's a small irony here: years ago I chose ownCloud &lt;em&gt;over&lt;/em&gt; Nextcloud specifically to avoid Nextcloud's faster release cadence and upgrade churn. That decision is what left me on an EOL stack. The migration target turned out to be the very thing I'd avoided.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration itself was the easy part
&lt;/h2&gt;

&lt;p&gt;With only 1.7 GB of real data, moving it took minutes. ownCloud speaks WebDAV, so &lt;code&gt;rclone&lt;/code&gt; pulls it straight across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone copy owncloud:ClientFolder r2:deliverables &lt;span class="nt"&gt;--progress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I left the dead &lt;code&gt;Documents&lt;/code&gt;/&lt;code&gt;Photos&lt;/code&gt;/&lt;code&gt;.exe&lt;/code&gt; files behind and moved only the live folder. The new Nextcloud runs in Docker Compose behind Caddy, which handles TLS automatically.&lt;/p&gt;

&lt;p&gt;Then the two real problems started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: "Too many requests" — Nextcloud locked me out
&lt;/h2&gt;

&lt;p&gt;Right after the migration, I tried to log in and got hit with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Too many requests
There were too many requests from your network. Retry later or contact your administrator if this is an error.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first reaction was the correct one: &lt;em&gt;wait, why me? I just set this up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cause is a classic reverse-proxy trap. Nextcloud has &lt;strong&gt;brute-force protection&lt;/strong&gt; that counts failed login attempts &lt;strong&gt;per IP address&lt;/strong&gt;. But behind Caddy, Nextcloud doesn't see the real client IPs — every single request arrives from &lt;strong&gt;Caddy's container address on the Docker bridge network, &lt;code&gt;172.18.0.2&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So from Nextcloud's point of view, &lt;em&gt;one&lt;/em&gt; IP (&lt;code&gt;172.18.0.2&lt;/code&gt;) was responsible for every login attempt from everyone. It didn't take much before that single "IP" tripped the threshold, and Nextcloud dutifully blocked it — which is to say, it blocked &lt;em&gt;everybody&lt;/em&gt;, including me.&lt;/p&gt;

&lt;p&gt;The fix is to tell Nextcloud which upstream addresses are trusted proxies, so it looks at the forwarded client IP instead of the proxy's own. In Nextcloud that's the &lt;code&gt;trusted_proxies&lt;/code&gt; config:&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 &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; www-data app &lt;span class="se"&gt;\&lt;/span&gt;
  php occ config:system:set trusted_proxies 0 &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"172.18.0.0/16"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Nextcloud trusts the proxy network, it reads the real client IP from the forwarded headers, and brute-force counting works per &lt;em&gt;actual&lt;/em&gt; client again — not per proxy. The lock-out stopped immediately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you clear an existing lockout while testing, you can also reset the brute-force attempts, but fixing &lt;code&gt;trusted_proxies&lt;/code&gt; is the part that stops it recurring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Gotcha 2: Basic auth vs. Nextcloud's &lt;code&gt;Authorization&lt;/code&gt; header
&lt;/h2&gt;

&lt;p&gt;The access model I wanted matched the old setup, so the client's experience wouldn't change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Office IPs&lt;/strong&gt; (mine and the client's) → straight to the Nextcloud login, no extra step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everywhere else&lt;/strong&gt; (e.g. the client's manager working remotely) → a &lt;strong&gt;Basic auth&lt;/strong&gt; prompt first, &lt;em&gt;then&lt;/em&gt; the Nextcloud login.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basic auth at the proxy is exactly what the old ownCloud setup did at its front Apache, so I expected this to be routine. In Caddy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files.example.com {
    # Remote (non-office) requests must pass Basic auth first
    @remote not remote_ip 203.0.113.10/32 203.0.113.20/32
    basic_auth @remote {
        remote_user &amp;lt;BCRYPT_HASH&amp;gt;
    }

    reverse_proxy app:80
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;authenticated&lt;/em&gt; correctly, but then Nextcloud threw a &lt;code&gt;401&lt;/code&gt;. The reason: &lt;strong&gt;newer Nextcloud interprets an incoming &lt;code&gt;Authorization: Basic ...&lt;/code&gt; header as an attempt to log into its own API.&lt;/strong&gt; It takes the Basic credentials I meant for the proxy, tries to match them against a Nextcloud user, finds none, and rejects the request.&lt;/p&gt;

&lt;p&gt;In other words, the same Basic auth header that got the visitor &lt;em&gt;through the proxy&lt;/em&gt; then confused &lt;em&gt;Nextcloud&lt;/em&gt;. This isn't Caddy-specific — put any front (Apache, nginx, Caddy) in front of this Nextcloud and the app will still try to consume that header. The fix is to &lt;strong&gt;strip the &lt;code&gt;Authorization&lt;/code&gt; header after the proxy consumes it, before it reaches Nextcloud.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first attempt did this inside the &lt;code&gt;reverse_proxy&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reverse_proxy app:80 {
    header_up -Authorization   # ← silently did nothing
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It didn't work. &lt;code&gt;basic_auth&lt;/code&gt; leaves the header on the request, and &lt;code&gt;header_up&lt;/code&gt; runs at a point in the pipeline where the removal didn't take effect against it — the credentials still reached Nextcloud.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;did&lt;/em&gt; work was removing the header at the &lt;strong&gt;site-block level&lt;/strong&gt;, before the request is proxied at all, using &lt;code&gt;request_header&lt;/code&gt; instead of &lt;code&gt;header_up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files.example.com {
    @remote not remote_ip 203.0.113.10/32 203.0.113.20/32
    basic_auth @remote {
        remote_user &amp;lt;BCRYPT_HASH&amp;gt;
    }

    # Strip the Basic credentials BEFORE proxying,
    # so Nextcloud never tries to consume them.
    request_header -Authorization

    reverse_proxy app:80
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters: &lt;code&gt;request_header -Authorization&lt;/code&gt; sits at the site level and rewrites the request &lt;em&gt;before&lt;/em&gt; the proxy stage, so by the time Nextcloud sees the request, the header is gone. Remote visitors now clear Basic auth and then get a clean Nextcloud login — no &lt;code&gt;401&lt;/code&gt;, and nothing new to learn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I didn't just switch to 2FA
&lt;/h3&gt;

&lt;p&gt;The "cleaner" fix would have been to drop Basic auth and turn on Nextcloud's two-factor auth. I deliberately didn't. The client had always logged in with just an ID and password; making them set up an authenticator app — to solve &lt;em&gt;my&lt;/em&gt; migration problem — would be pushing my cost decision onto their workflow. Keeping the experience identical was the whole point of choosing Nextcloud in the first place.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; an always-on Reserved EC2 instance (in a rising currency) → a &lt;strong&gt;flat $10/mo Lightsail box&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; EOL stack retired, TLS auto-managed by Caddy, brute-force protection working correctly per real client IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client experience:&lt;/strong&gt; unchanged. Office logs in directly; remote passes one Basic auth prompt first — exactly like before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I keep coming back to: a Reserved Instance felt like a cost optimization, but it was really a commitment to a design I should have questioned. The cheapest server is often the one you stop needing to run 24/7 at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you run Nextcloud behind a reverse proxy and see "Too many requests," check &lt;code&gt;trusted_proxies&lt;/code&gt; first — it's almost always the proxy IP getting blamed for everyone.&lt;/em&gt;&lt;/p&gt;

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