{"id":25,"date":"2026-03-12T06:45:11","date_gmt":"2026-03-12T14:45:11","guid":{"rendered":"https:\/\/satchnet.io\/?p=25"},"modified":"2026-03-19T05:34:42","modified_gmt":"2026-03-19T13:34:42","slug":"the-great-uptime-kuma-upgrade-a-tale-of-stuck-migrations-and-io-domain-betrayal","status":"publish","type":"post","link":"https:\/\/satchnet.io\/?p=25","title":{"rendered":"The Great Uptime Kuma Upgrade: A Tale of Stuck Migrations and .io Domain Betrayal"},"content":{"rendered":"\n<p><strong>tl;dr:<\/strong> Upgraded Uptime Kuma from v1 to v2, got stuck in a migration bootloop, fixed it with some SQLite surgery, then upgraded Portainer because apparently I hate stability.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Act I: &#8220;Wait, What Version Am I On?&#8221;<\/h2>\n\n\n\n<p>Me: &#8220;I should update Uptime Kuma.&#8221;<\/p>\n\n\n\n<p>UI: &#8220;You&#8217;re on v1.23.17&#8221;<\/p>\n\n\n\n<p>Logs after restarting: &#8220;Uptime Kuma Version: 2.2.1&#8221;<\/p>\n\n\n\n<p>Me: &#8220;&#8230;wat&#8221;<\/p>\n\n\n\n<p>Turns out I&#8217;d already upgraded at some point but the UI was living in the past. Cool. Cool cool cool.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Act II: The Bootloop of Doom<\/h2>\n\n\n\n<p>Tried to start the container. Got this instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;DB] WARN: Aggregate table migration is already in progress, or it was interrupted\n&#91;DB] ERROR: Database migration failed\n&#91;SERVER] ERROR: Failed to prepare your database<\/code><\/pre>\n\n\n\n<p><em>Repeat ad infinitum<\/em><\/p>\n\n\n\n<p>Classic &#8220;migration started, never finished, now refuses to do anything&#8221; situation. The container was crashing faster than my confidence.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Act III: SQLite to the Rescue<\/h2>\n\n\n\n<p>Stopped the container (well, it stopped itself repeatedly, I just made it stay stopped), popped into the database:<\/p>\n\n\n\n<p>bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/data\/compose\/14\/data\nsqlite3 kuma.db<\/code><\/pre>\n\n\n\n<p>Found the culprit:<\/p>\n\n\n\n<p>sql<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM setting WHERE key = 'migrateAggregateTableState';\n-- Returns: \"migrating\"<\/code><\/pre>\n\n\n\n<p>But wait! Did the migration actually fail, or did it just forget to update the flag? Time for some forensics:<\/p>\n\n\n\n<p>sql<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT COUNT(*) FROM stat_minutely;  -- 80 rows\nSELECT COUNT(*) FROM stat_hourly;    -- 54 rows  \nSELECT COUNT(*) FROM stat_daily;     -- 73 rows<\/code><\/pre>\n\n\n\n<p>The migration had <em>completed<\/em> &#8211; it just failed to clear its own flag. Like finishing a marathon and forgetting to cross the finish line.<\/p>\n\n\n\n<p><strong>The fix:<\/strong><\/p>\n\n\n\n<p>sql<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UPDATE setting SET value = '\"migrated\"' WHERE key = 'migrateAggregateTableState';\n```\n\nRestarted the container. No more bootloop. Victory!\n\n## Act IV: Domain Expiry Monitoring (LOL)\n\nGot all excited about v2's new domain expiry monitoring feature. You know, the thing that warns you before your domain registration expires.\n\nChecked the logs:\n```\n&#91;DOMAIN_EXPIRY] WARN: Domain expiry unsupported for '.io' because its RDAP endpoint \nis not listed in the IANA database.<\/code><\/pre>\n\n\n\n<p>My primary domain is <code>satchnet.io<\/code>.<\/p>\n\n\n\n<p>The feature doesn&#8217;t work for <code>.io<\/code> domains.<\/p>\n\n\n\n<p>\ud83d\ude10<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Act V: Portainer Upgrade (Because Why Not)<\/h2>\n\n\n\n<p>Having successfully upgraded one thing, I decided to tempt fate and upgrade Portainer too.<\/p>\n\n\n\n<p>This one was anticlimactic:<\/p>\n\n\n\n<p>bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker pull portainer\/portainer-ce:lts\ndocker stop portainer &amp;&amp; docker rm portainer\ndocker run -d ... # (same flags as before)<\/code><\/pre>\n\n\n\n<p>Migrated cleanly from 2.33.7 to 2.39.0. No drama. No stuck migrations. No existential database crises.<\/p>\n\n\n\n<p>Honestly kind of disappointing after the Uptime Kuma adventure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>LXC snapshots before upgrades<\/strong> &#8211; always, every time, no exceptions<\/li>\n\n\n\n<li><strong>Database migrations can lie<\/strong> &#8211; if it says &#8220;in progress,&#8221; check if it actually finished<\/li>\n\n\n\n<li><strong>Not all upgrades are created equal<\/strong> &#8211; some are smooth (Portainer), some require SQLite wizardry (Uptime Kuma)<\/li>\n\n\n\n<li><strong><code>.io<\/code> domains are second-class citizens<\/strong> in the domain expiry monitoring world<\/li>\n\n\n\n<li><strong>&#8220;If it ain&#8217;t broke, don&#8217;t fix it&#8221;<\/strong> is good advice I will continue to ignore<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">The End<\/h2>\n\n\n\n<p>Everything works now. Uptime Kuma is on v2.2.1, Portainer is on 2.39.0, and I have exactly zero domain expiry monitoring for my <code>.io<\/code> domains.<\/p>\n\n\n\n<p>10\/10 would troubleshoot stuck migrations again.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><em>P.S. &#8211; Huge thanks to Claude for the SQLite forensics. Without the &#8220;check if the tables actually have data&#8221; step, I&#8217;d probably still be in that bootloop.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>tl;dr: Upgraded Uptime Kuma from v1 to v2, got stuck in a migration bootloop, fixed it with some SQLite surgery, then upgraded Portainer because apparently I hate stability.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6,7],"tags":[],"class_list":["post-25","post","type-post","status-publish","format-standard","hentry","category-docker","category-monitoring"],"_links":{"self":[{"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/posts\/25","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/satchnet.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=25"}],"version-history":[{"count":4,"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/posts\/25\/revisions"}],"predecessor-version":[{"id":37,"href":"https:\/\/satchnet.io\/index.php?rest_route=\/wp\/v2\/posts\/25\/revisions\/37"}],"wp:attachment":[{"href":"https:\/\/satchnet.io\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=25"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/satchnet.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=25"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/satchnet.io\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=25"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}