<?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: Tin Ho Chu</title>
    <description>The latest articles on DEV Community by Tin Ho Chu (@tinhochu).</description>
    <link>https://dev.to/tinhochu</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%2F18965%2F8ff5017b-c75b-4bcc-bb67-c1030aba4966.jpg</url>
      <title>DEV Community: Tin Ho Chu</title>
      <link>https://dev.to/tinhochu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tinhochu"/>
    <language>en</language>
    <item>
      <title>Upgrading Appwrite on CapRover from 1.4.13 to 1.9.5</title>
      <dc:creator>Tin Ho Chu</dc:creator>
      <pubDate>Wed, 29 Jul 2026 16:29:57 +0000</pubDate>
      <link>https://dev.to/tinhochu/upgrading-appwrite-on-caprover-from-1413-to-195-m6n</link>
      <guid>https://dev.to/tinhochu/upgrading-appwrite-on-caprover-from-1413-to-195-m6n</guid>
      <description>&lt;h1&gt;
  
  
  Upgrading Appwrite from 1.4.13 to 1.9.5 on CapRover: A Field Guide Through Five Years of Breaking Changes
&lt;/h1&gt;

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

&lt;p&gt;What looked like a routine "bump the Docker tag" upgrade turned into a multi-hour dig through migration internals, CLI argument parsing quirks, a whole new microservice I didn't know existed, and an EJS templating bug that only showed up on long lines. If you're running self-hosted Appwrite on CapRover and haven't upgraded past 1.4.x, here's everything that actually needs to happen — and why the obvious approach doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I've been running Appwrite 1.4.13 self-hosted on CapRover for a while — a backend-as-a-service platform handling auth, databases, and storage for a few projects. It's been solid, but 1.4.13 is old at this point, and I wanted to get current. My plan was simple: bump the CapRover image tag to something newer, let it redeploy, done.&lt;/p&gt;

&lt;p&gt;It was not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  First failure: authentication just breaks
&lt;/h2&gt;

&lt;p&gt;The moment I pointed CapRover at a newer tag, login stopped working. No useful error in the browser, just a broken console. Digging into the container logs gave the first real clue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Appwrite\Extend\Exception: Unknown resource type
at /usr/src/code/app/controllers/general.php:792
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This traced back to Appwrite's custom-domain router. Every incoming request — including requests to your &lt;em&gt;own&lt;/em&gt; API domain — gets matched against a &lt;code&gt;rules&lt;/code&gt; collection that decides whether to serve it as an API call, a redirect, or a function/site deployment. That collection has a &lt;code&gt;type&lt;/code&gt; attribute that determines the routing behavior.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;rules&lt;/code&gt; row predated that attribute entirely. It was created back on 1.4.13, when the schema only had a field called &lt;code&gt;resourceType&lt;/code&gt;. The &lt;code&gt;type&lt;/code&gt; field — and the migration logic that converts &lt;code&gt;resourceType&lt;/code&gt; into it — was added in a &lt;em&gt;later&lt;/em&gt; internal schema version. I'd jumped straight past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson: Appwrite's migrations don't chain automatically
&lt;/h2&gt;

&lt;p&gt;Appwrite tracks an internal schema version per release (V19, V20, V21...) and ships a &lt;code&gt;migrate&lt;/code&gt; CLI command to walk your data through schema changes. I assumed — reasonably, I thought — that running &lt;code&gt;migrate&lt;/code&gt; against a fresh image would walk through every schema change since your last version automatically.&lt;/p&gt;

&lt;p&gt;It doesn't. Each invocation of &lt;code&gt;migrate&lt;/code&gt; takes a single target version and runs &lt;em&gt;only&lt;/em&gt; that version's migration class. There's no chain. Jump from 1.4.13 straight to 1.9.5 in one &lt;code&gt;migrate&lt;/code&gt; call, and you silently skip every intermediate schema change — including, in my case, the exact one that fixes the "Unknown resource type" bug.&lt;/p&gt;

&lt;p&gt;The fix was to invoke migrate once per intermediate version, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container&amp;gt; migrate &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.5.0
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container&amp;gt; migrate &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.6.2
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container&amp;gt; migrate &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.7.4
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container&amp;gt; migrate &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.8.1
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container&amp;gt; migrate &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.9.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas buried in there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;version=X&lt;/code&gt;, not positional &lt;code&gt;X&lt;/code&gt;.&lt;/strong&gt; Appwrite's CLI framework parses arguments as &lt;code&gt;key=value&lt;/code&gt;, not by position. Running &lt;code&gt;migrate 1.5.0&lt;/code&gt; doesn't error — it silently falls back to the default version and runs the &lt;em&gt;wrong&lt;/em&gt; migration. I burned five identical runs before noticing every single one printed &lt;code&gt;Starting data migration to version 1.9.5&lt;/code&gt; regardless of what I typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_APP_DB_ADAPTER&lt;/code&gt; now defaults to &lt;code&gt;postgresql&lt;/code&gt;.&lt;/strong&gt; My install was MariaDB, like every pre-1.9 Appwrite install (multi-adapter support is newer than that). Without explicitly setting &lt;code&gt;_APP_DB_ADAPTER=mariadb&lt;/code&gt;, the new binary would have tried to speak Postgres wire protocol to a MariaDB server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Second failure: the console vanishes
&lt;/h2&gt;

&lt;p&gt;With the database schema finally caught up, the API worked — but the web console 404'd on every path. Turns out the console UI isn't bundled into the main image anymore. Somewhere around the SSR rework, it became its own service (&lt;code&gt;appwrite/new&lt;/code&gt;, a TanStack Start app) that's expected to sit behind a reverse proxy splitting traffic by path: &lt;code&gt;/v1/*&lt;/code&gt; and &lt;code&gt;/.well-known/*&lt;/code&gt; go to the PHP API, everything else goes to the console.&lt;/p&gt;

&lt;p&gt;My 1.4.13-era CapRover deployment only had the one API container — that split had never existed before. I had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the console as its own CapRover app (&lt;code&gt;appwrite/new:0.3.32&lt;/code&gt;, port 3000, three env vars).&lt;/li&gt;
&lt;li&gt;Hand-write a custom Nginx config for the API app replicating that path split, since CapRover's proxy is Nginx, not the Traefik setup Appwrite's official docker-compose file assumes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Third failure: the config that wouldn't stick
&lt;/h2&gt;

&lt;p&gt;Twice, my carefully-written custom Nginx config silently reverted to CapRover's bare default template after I touched other App Config settings — a known CapRover behavior where certain saves regenerate the proxy config from scratch. And once, while debugging, CapRover's build log threw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;Error: Could not find matching close tag for "&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="na"&gt;-&lt;/span&gt;&lt;span class="err"&gt;".&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wasn't a real syntax error in the template — it was long lines getting silently truncated mid-word somewhere in the copy path between chat, terminal, and CapRover's textbox. The fix was almost comedic: reformat the entire config so no line exceeded ~60 characters, since nginx doesn't care about line breaks between tokens. Once every line was short enough to survive whatever was truncating it, it stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What finally worked, end to end
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;_APP_DB_ADAPTER=mariadb&lt;/code&gt; before touching anything else.&lt;/li&gt;
&lt;li&gt;Ran &lt;code&gt;migrate version=X&lt;/code&gt; for every intermediate version between 1.4.13 and 1.9.5, in order, verifying &lt;code&gt;Migration completed&lt;/code&gt; (not just a version number) after each one.&lt;/li&gt;
&lt;li&gt;Deployed a separate &lt;code&gt;appwrite-console&lt;/code&gt; app running &lt;code&gt;appwrite/new&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Split Nginx routing on the API app between &lt;code&gt;/v1/*&lt;/code&gt; → API and everything else → console.&lt;/li&gt;
&lt;li&gt;Fixed &lt;code&gt;_APP_DOMAIN&lt;/code&gt; (previously &lt;code&gt;localhost&lt;/code&gt;, a legacy placeholder that broke asset URLs and CORS on the newer console).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If you're doing this yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't trust a single &lt;code&gt;migrate&lt;/code&gt; call to catch you up across major versions — check &lt;code&gt;Migration::$versions&lt;/code&gt; in the source for the exact intermediate version strings, and run each one explicitly.&lt;/li&gt;
&lt;li&gt;Confirm your database adapter env var explicitly. Don't rely on defaults changing under you.&lt;/li&gt;
&lt;li&gt;If you're self-hosting on anything other than the official docker-compose file (CapRover, a hand-rolled Swarm stack, whatever), expect to manually replicate any new services that landed since your last upgrade — check the current &lt;code&gt;docker-compose.yml&lt;/code&gt; for services you don't have yet.&lt;/li&gt;
&lt;li&gt;When a config "mysteriously" breaks after being fine moments before, check whether something &lt;em&gt;else&lt;/em&gt; you touched regenerated it from a template.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>appwrite</category>
      <category>caprover</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
