<?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: Great Sage</title>
    <description>The latest articles on DEV Community by Great Sage (@greatsage_sh).</description>
    <link>https://dev.to/greatsage_sh</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%2F4025220%2F908c5121-b9c3-4918-9093-1f647f2ab1dd.jpg</url>
      <title>DEV Community: Great Sage</title>
      <link>https://dev.to/greatsage_sh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/greatsage_sh"/>
    <language>en</language>
    <item>
      <title>WeTransfer alternative that isn't claimable by the first stranger who opens the URL</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Sun, 09 Aug 2026 00:04:27 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/wetransfer-alternative-that-isnt-claimable-by-the-first-stranger-who-opens-the-url-3ae7</link>
      <guid>https://dev.to/greatsage_sh/wetransfer-alternative-that-isnt-claimable-by-the-first-stranger-who-opens-the-url-3ae7</guid>
      <description>&lt;p&gt;If you've ever self-hosted a file-sharing tool and then found out weeks later that some rando on the internet had already made themselves admin, you know the exact bad feeling I'm describing.&lt;/p&gt;

&lt;p&gt;I ran into this with Pingvin Share, the open-source WeTransfer alternative (expiring links, password protection, reverse shares where someone can upload &lt;em&gt;to&lt;/em&gt; you with no account, OIDC/OAuth/LDAP support). It's a genuinely good project. But the original repo (&lt;code&gt;stonith404/pingvin-share&lt;/code&gt;) got archived on 2025-06-29, and its own README now points people at a maintained fork, &lt;code&gt;smp46/pingvin-share-x&lt;/code&gt; — 221 commits and 24 releases ahead.&lt;/p&gt;

&lt;p&gt;The bigger issue isn't staleness though, it's the signup flow: Pingvin gives admin rights to whoever registers &lt;em&gt;first&lt;/em&gt;, and registration is open by default. Deploy it stock and don't beat the internet to your own signup page, and someone else owns your file-sharing instance. I reproduced this on the stock image — anonymous &lt;code&gt;POST /api/auth/signUp&lt;/code&gt; comes back &lt;code&gt;201&lt;/code&gt; with &lt;code&gt;"isAdmin":true&lt;/code&gt;. There's also a port mismatch worth knowing about if you're deploying this anywhere with a platform-injected port (Railway, Render, etc.) — the app's internal Caddy proxy is hardcoded to port 3000 and doesn't listen on whatever &lt;code&gt;$PORT&lt;/code&gt; your platform hands it, so you can end up with a healthy-looking container serving 404 on every page.&lt;/p&gt;

&lt;p&gt;I maintain a Railway template that runs the maintained fork and handles both of those before the instance is ever reachable — generates and seeds the admin password at boot with registration closed, and remaps the injected port so the proxy actually listens where your platform expects. Full disclosure: I get a kickback if you deploy through it. Link: &lt;a href="https://railway.com/deploy/pingvin-share-x-v122-or-wetransfer-alter?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/pingvin-share-x-v122-or-wetransfer-alter?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather not use it, that's genuinely fine — the non-affiliated path works too: pull &lt;code&gt;ghcr.io/smp46/pingvin-share-x&lt;/code&gt; directly, mount a volume at &lt;code&gt;/opt/app/backend/data&lt;/code&gt;, and just make sure you register the admin account and close signups before you share the URL with anyone. That's the whole fix, template or not.&lt;/p&gt;

&lt;p&gt;One honest tradeoff: it's SQLite-backed, single container, no external DB/cache — great for a small team or personal use, less great if you need horizontal scale or heavy concurrent uploads. For that use case it's solid.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Self-hosting AzuraCast? Whoever opens the setup wizard first becomes admin</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Sat, 08 Aug 2026 16:06:59 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosting-azuracast-whoever-opens-the-setup-wizard-first-becomes-admin-4jlj</link>
      <guid>https://dev.to/greatsage_sh/self-hosting-azuracast-whoever-opens-the-setup-wizard-first-becomes-admin-4jlj</guid>
      <description>&lt;p&gt;I've been messing with self-hosted web radio lately — AzuraCast is the obvious pick if you want Icecast streaming, Liquidsoap playlist automation, a browser-based DJ console, podcasts and listener stats without paying for a hosted service. It's been around for about ten years and it's genuinely good software.&lt;/p&gt;

&lt;p&gt;If you're setting it up on any platform where the app comes up publicly before you've had a chance to click through the wizard yourself (Railway, Render, a VPS with a slow DNS propagation, whatever), there's a footgun worth knowing about before you hit deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup wizard doesn't check who's asking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SetupController::registerAction&lt;/code&gt; serves &lt;code&gt;GET /setup/register&lt;/code&gt; and accepts the POST with no authentication at all, for as long as the &lt;code&gt;users&lt;/code&gt; table is empty. Whoever's account gets created there gets &lt;code&gt;ensureSuperAdministratorRole()&lt;/code&gt; — full admin, no questions asked. I checked this directly against the stock image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET  /setup/register            -&amp;gt; 200   (valid CSRF token in the page)
POST /setup/register            -&amp;gt; 302
azuracast_cli azuracast:account:list
  attacker@example.com   Super Administrator
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a hypothetical bot-scanning-the-internet scenario either — it's the literal next step in the standard deploy instructions most guides give you: "open your URL and finish the setup wizard." If your app is reachable before you are, whoever gets there first owns your instance — your stream config, your DJ accounts, everything.&lt;/p&gt;

&lt;p&gt;The fix, if you're doing this yourself: don't expose the public URL until you've completed the wizard, or front it with something (Cloudflare Access, a temporary firewall rule, whatever you've got) until the first admin account exists. AzuraCast doesn't help you here — this is on the deployer.&lt;/p&gt;

&lt;p&gt;Full disclosure: I maintain a Railway template for AzuraCast (kickback applies if you deploy through it) that seeds the admin account server-side before nginx ever starts serving requests, so there's no window where the setup page is live and unclaimed — verified live, &lt;code&gt;/setup/register&lt;/code&gt; returns 403 from the first request the domain ever answers. It also pins the version instead of tracking &lt;code&gt;:latest&lt;/code&gt; (AzuraCast's DB migrations are one-way, so an unplanned upgrade on redeploy has no way back), and sizes PHP-FPM/MariaDB pool limits from the container's actual memory instead of AzuraCast's fixed defaults, which otherwise assume way more RAM than a small box has.&lt;/p&gt;

&lt;p&gt;Link if you want the one-click version: &lt;a href="https://railway.com/deploy/azuracast-v0237-or-web-radio-whose-admin?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/azuracast-v0237-or-web-radio-whose-admin?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you'd rather run it yourself with none of that: upstream repo is &lt;a href="https://github.com/AzuraCast/AzuraCast" rel="noopener noreferrer"&gt;https://github.com/AzuraCast/AzuraCast&lt;/a&gt;, official Docker install docs are on their site — just make sure you're not leaving &lt;code&gt;/setup/register&lt;/code&gt; open to the internet even for five minutes.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>docker</category>
      <category>railway</category>
    </item>
    <item>
      <title>NocoDB self-hosting will quietly delete your bases if you skip this one env var</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Fri, 07 Aug 2026 04:11:32 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/nocodb-self-hosting-will-quietly-delete-your-bases-if-you-skip-this-one-env-var-d2b</link>
      <guid>https://dev.to/greatsage_sh/nocodb-self-hosting-will-quietly-delete-your-bases-if-you-skip-this-one-env-var-d2b</guid>
      <description>&lt;p&gt;I've been packaging Railway templates for self-hosted tools, and NocoDB turned into a longer rabbit hole than I expected — not because the app is bad, it's genuinely a solid open-source Airtable alternative — but because its default self-hosting config has a landmine in it that isn't obvious until you hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landmine: &lt;code&gt;NC_DB&lt;/code&gt; unset means SQLite means data loss
&lt;/h2&gt;

&lt;p&gt;NocoDB stores two very different things. Your actual data — if you connect an external Postgres/MySQL source — lives wherever that source is. But NocoDB's own &lt;em&gt;metadata&lt;/em&gt; (every base you create inside NocoDB itself, views, filters, users, shared links, API tokens) lives in whatever database &lt;code&gt;NC_DB&lt;/code&gt; points at.&lt;/p&gt;

&lt;p&gt;Leave &lt;code&gt;NC_DB&lt;/code&gt; unset and it defaults to a SQLite file inside the container. That's fine on a machine where the container never gets recreated. It's not fine on any platform-as-a-service (Railway, Render, Fly, a Docker Compose stack without a bind mount) where a redeploy gives you a fresh container filesystem. The SQLite file goes with it. Every base, every user account, gone — silently, no error, no warning at deploy time. You just log back in to an empty instance.&lt;/p&gt;

&lt;p&gt;This is an easy thing to miss because NocoDB &lt;em&gt;works&lt;/em&gt; perfectly during setup and testing. You only find out on your first redeploy, which by definition happens after you've already put real data in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second landmine: attachments aren't in the database either
&lt;/h2&gt;

&lt;p&gt;Even once you've pinned &lt;code&gt;NC_DB&lt;/code&gt; to a real Postgres, file-type cell attachments (uploaded images, PDFs, whatever) are written to disk at &lt;code&gt;/usr/app/data&lt;/code&gt;, not into the database. If that path isn't on a persistent volume, attachments vanish on redeploy even though your bases and rows survive fine. Two separate things to get right, not one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third: the auth secret
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NC_AUTH_JWT_SECRET&lt;/code&gt; needs to be generated once and then stay pinned. If it regenerates on every boot (which happens if you don't set it explicitly and the image generates a random one at runtime), every session and every issued API token invalidates on restart — annoying for humans, worse if you've got automations hitting the API with a token that just silently stopped working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;p&gt;I maintain a one-click Railway template for NocoDB (disclosure: I get a referral kickback if you deploy through it — full transparency, not hiding that): [deploy link below]. It ships as two services — NocoDB plus its own dedicated Postgres 17 — wired together over Railway's private network from first boot, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NC_DB&lt;/code&gt; pointed at Postgres from the start, on a volume, so metadata survives redeploys&lt;/li&gt;
&lt;li&gt;A second volume mounted at &lt;code&gt;/usr/app/data&lt;/code&gt; for attachments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NC_AUTH_JWT_SECRET&lt;/code&gt; generated once and pinned, not regenerated per boot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NC_PUBLIC_URL&lt;/code&gt; bound to the actual Railway domain so shared views and form links resolve correctly instead of pointing at &lt;code&gt;localhost&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather not use Railway at all, the fix is the same wherever you run it: set &lt;code&gt;NC_DB&lt;/code&gt; to a real database connection string (Postgres or MySQL) before your first deploy, put &lt;code&gt;/usr/app/data&lt;/code&gt; on a persistent volume, and pin &lt;code&gt;NC_AUTH_JWT_SECRET&lt;/code&gt; explicitly instead of letting it default. The &lt;a href="https://docs.nocodb.com" rel="noopener noreferrer"&gt;official NocoDB docs&lt;/a&gt; cover the exact env vars if you're rolling your own docker-compose.&lt;/p&gt;

&lt;p&gt;One gotcha worth knowing regardless of where you host it: whoever signs up first becomes the instance's super admin. Claim it immediately after your first deploy, before you share the URL with anyone.&lt;/p&gt;

&lt;p&gt;Deploy link (Railway, Postgres 17 + volumes included): &lt;a href="https://railway.com/deploy/nocodb-airtable-al-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/nocodb-airtable-al-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions on the setup, connecting external data sources, or the Postgres sizing if anyone's running a bigger install.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>database</category>
      <category>railway</category>
    </item>
    <item>
      <title>Self-hosted Sentry error tracking without the 16GB RAM bill (GlitchTip on Railway)</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:06:49 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosted-sentry-error-tracking-without-the-16gb-ram-bill-glitchtip-on-railway-46if</link>
      <guid>https://dev.to/greatsage_sh/self-hosted-sentry-error-tracking-without-the-16gb-ram-bill-glitchtip-on-railway-46if</guid>
      <description>&lt;p&gt;If you've priced out self-hosting Sentry, you've probably bounced off the same wall I did: the official self-hosted stack is Kafka + ClickHouse + Snuba + Relay + a consumer fleet — something like 12 services, and the docs themselves ask for 16GB of RAM. That's a lot of infrastructure just to catch stack traces for a side project or a small team's app.&lt;/p&gt;

&lt;p&gt;GlitchTip is the answer most people land on eventually: it speaks the same Sentry SDK protocol (drop-in, same &lt;code&gt;dsn&lt;/code&gt;, same client libraries) but runs as three services — app, Postgres, Redis — on hardware a Sentry deploy wouldn't even boot on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody mentions: it's easy to lose your data anyway
&lt;/h2&gt;

&lt;p&gt;I maintain a Railway template for GlitchTip, and packaging it turned up three things that aren't obvious from the docs, all reproducible on the stock image:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Source maps disappear on redeploy, and re-uploading doesn't fix it.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;MEDIA_ROOT&lt;/code&gt; defaults to an ephemeral path, but &lt;code&gt;FileBlob&lt;/code&gt; dedupes uploads by checksum in a Postgres row. So after a redeploy: the file on disk is gone, but the checksum row survives. Re-upload the &lt;em&gt;same&lt;/em&gt; file and it finds the existing row, decides there's nothing to do, and writes nothing. &lt;code&gt;sentry-cli&lt;/code&gt; reports success both times. You find out your source maps are gone the next time a minified stack trace shows up ungrouped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A mounted volume doesn't mean writable.&lt;/strong&gt;&lt;br&gt;
Railway (and most platforms) mount volumes as uid 0. GlitchTip's container runs as uid 5000. Add a volume without fixing ownership and you get &lt;code&gt;PermissionError&lt;/code&gt; on every upload — or worse, depending on the app, a silent failure that never surfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Signup stays open by default.&lt;/strong&gt;&lt;br&gt;
Fine for a local dev instance, less fine for a public URL. Upstream still lets the &lt;em&gt;first&lt;/em&gt; account in even if you close registration after deploy, so you can lock it down without stranding yourself.&lt;/p&gt;

&lt;p&gt;None of these are GlitchTip being badly built — they're the standard gap between "runs on my machine" and "runs on a platform that treats your filesystem as disposable between deploys."&lt;/p&gt;
&lt;h2&gt;
  
  
  What I actually did about it
&lt;/h2&gt;

&lt;p&gt;Wrapper image that: fixes the volume ownership before dropping privileges, keeps &lt;code&gt;MEDIA_ROOT&lt;/code&gt; pointed at the persistent volume so the checksum-dedup issue above never gets a chance to bite, disables signup after the first account, pins the version instead of tracking &lt;code&gt;latest&lt;/code&gt;, and sets &lt;code&gt;CSRF_TRUSTED_ORIGINS&lt;/code&gt; correctly for HTTPS behind Railway's proxy.&lt;/p&gt;

&lt;p&gt;Full writeup with the reproduction commands and before/after output is in the repo if you want to see the actual failure states: &lt;a href="https://github.com/bon5co/glitchtip-railway" rel="noopener noreferrer"&gt;https://github.com/bon5co/glitchtip-railway&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather not deal with any of this yourself, one-click deploy (disclosure: I maintain this template and get a referral kickback if you deploy through it): &lt;a href="https://railway.com/deploy/sentry-compatible-error-tracking-or-glit?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/sentry-compatible-error-tracking-or-glit?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you're not on Railway — the Dockerfile is public, so the fixes apply just as well to a plain &lt;code&gt;docker run&lt;/code&gt; on your own box.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Once it's up, point any Sentry SDK at it like you would the real thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;
&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;key&amp;gt;@&amp;lt;your-glitchtip-host&amp;gt;/&amp;lt;project-id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same client libraries, same &lt;code&gt;sentry-cli&lt;/code&gt; for source map uploads — GlitchTip's whole pitch is protocol compatibility, so nothing on the app side changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs, honestly
&lt;/h2&gt;

&lt;p&gt;GlitchTip isn't Sentry — it's a lighter reimplementation. You don't get session replay, some of the fancier performance-monitoring views, or the AI-assisted triage stuff Sentry's been shipping. If your team lives inside Sentry's dashboard for reasons beyond "catch the exception," this isn't a swap you make blind. But if what you actually want is "get an alert with a stack trace when something breaks," GlitchTip does that for a fraction of the resource footprint, and self-hosting it means your error data — which often includes user info, request bodies, sometimes secrets you forgot to scrub — never leaves your own infrastructure.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>railway</category>
      <category>devops</category>
    </item>
    <item>
      <title>Self-hosting LibreChat on Railway: getting file RAG working without an OpenAI key</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:05:23 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosting-librechat-on-railway-getting-file-rag-working-without-an-openai-key-a44</link>
      <guid>https://dev.to/greatsage_sh/self-hosting-librechat-on-railway-getting-file-rag-working-without-an-openai-key-a44</guid>
      <description>&lt;p&gt;LibreChat is one of the better self-hosted ChatGPT alternatives — multi-provider chat UI, agents, file uploads, the works. But if you've actually tried to self-host it past a weekend demo, two things bite you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uploaded files vanish on redeploy.&lt;/strong&gt; LibreChat writes user uploads to &lt;code&gt;/app/uploads&lt;/code&gt; and agent-generated images to &lt;code&gt;/app/client/public/images&lt;/code&gt; — both inside the container's own filesystem. Redeploy (or just a container restart on most PaaS platforms) and the files are gone, but MongoDB still has message rows pointing at URLs that now 404.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File chat / RAG wants an OpenAI key by default.&lt;/strong&gt; The RAG API LibreChat ships (&lt;code&gt;librechat-rag-api-dev-lite&lt;/code&gt;) does the embeddings via OpenAI unless you configure a local model yourself. A lot of self-host guides just tell you to drop in &lt;code&gt;RAG_OPENAI_API_KEY&lt;/code&gt; and move on — fine if you're already paying OpenAI for everything else, less fine if the whole point was to run something you don't have to meter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is a LibreChat bug exactly — it's what happens when a docker-compose stack built for a VM with a persistent disk gets deployed somewhere that treats the container filesystem as disposable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, if you want to do it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Mount a real volume at &lt;code&gt;/app/uploads&lt;/code&gt;, and symlink &lt;code&gt;/app/client/public/images&lt;/code&gt; into it so both survive a restart.&lt;/li&gt;
&lt;li&gt;Swap the RAG API's embeddings provider to something that runs locally — &lt;code&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/code&gt; on CPU works fine for document chat and costs nothing per query. Set &lt;code&gt;EMBEDDINGS_PROVIDER&lt;/code&gt; accordingly and skip the OpenAI key entirely for the RAG path (you'll still want a provider key for the actual chat models, obviously).&lt;/li&gt;
&lt;li&gt;Pin your image tags. LibreChat, Mongo, Meilisearch and pgvector all move fast enough that &lt;code&gt;latest&lt;/code&gt; on a stack with a live data volume is asking for a migration you didn't plan for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that is upstream LibreChat config — nothing here is proprietary, and the &lt;a href="https://github.com/danny-avila/LibreChat" rel="noopener noreferrer"&gt;official repo&lt;/a&gt; has the docker-compose to build it from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or the one-click version
&lt;/h2&gt;

&lt;p&gt;I maintain a Railway template that does the above out of the box — file RAG runs on the local MiniLM model with no API key needed, uploads persist across redeploys, and every image tag is pinned (LibreChat v0.8.7, Mongo 8.3.7, Meilisearch v1.51.0, pgvector 0.8.6-pg17). Full disclosure: I get a kickback if you deploy through it.&lt;/p&gt;

&lt;p&gt;Deploy: &lt;a href="https://railway.com/deploy/librechat-or-chatgpt-alternative-files-s?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/librechat-or-chatgpt-alternative-files-s?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rough edges worth knowing before you deploy either way: the local embedding model is CPU-only, so it's fine for personal/small-team document volumes but won't keep up with heavy concurrent RAG traffic — if that's your use case you probably do want a real embeddings API. And LibreChat's &lt;code&gt;librechat.yaml&lt;/code&gt; config controls which providers/endpoints show up in the UI, so budget a few minutes to edit that for whichever models you actually want exposed.&lt;/p&gt;

&lt;p&gt;Happy to answer questions on the RAG setup specifically — that was the part that took the most trial and error to get working without a key.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>railway</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Self-hosted S3 storage on Railway: MinIO in one click (plus the two gotchas that'll 502 you)</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Wed, 05 Aug 2026 04:04:35 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosted-s3-storage-on-railway-minio-in-one-click-plus-the-two-gotchas-thatll-502-you-4l5a</link>
      <guid>https://dev.to/greatsage_sh/self-hosted-s3-storage-on-railway-minio-in-one-click-plus-the-two-gotchas-thatll-502-you-4l5a</guid>
      <description>&lt;p&gt;If you've ever hit the point where an app needs an S3 bucket — file uploads, backups, an ML&lt;br&gt;
dataset dump, whatever — and you didn't want to open a whole AWS account for it, MinIO is&lt;br&gt;
probably the answer you've already heard of. It's open-source, speaks the real S3 API (boto3,&lt;br&gt;
aws-sdk-js, the &lt;code&gt;aws s3&lt;/code&gt; CLI, &lt;code&gt;mc&lt;/code&gt; — all of it works unmodified), and you own the disk instead of&lt;br&gt;
paying per-GB egress forever.&lt;/p&gt;

&lt;p&gt;The annoying part is always the same: getting it actually running somewhere with a persistent&lt;br&gt;
disk, HTTPS, and credentials that aren't &lt;code&gt;minioadmin/minioadmin&lt;/code&gt; on a public URL. I run mine on&lt;br&gt;
Railway and wrote up the two things that will bite you if you deploy the raw image yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 1 — the start command needs a shell.&lt;/strong&gt; Railway execs your start command as argv, not&lt;br&gt;
through &lt;code&gt;/bin/sh&lt;/code&gt;. So if you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minio server /data &lt;span class="nt"&gt;--address&lt;/span&gt; &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;9000&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--console-address&lt;/span&gt; &lt;span class="s2"&gt;":9001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that &lt;code&gt;${PORT:-9000}&lt;/code&gt; never gets expanded — it reaches MinIO as the literal seven characters, and&lt;br&gt;
you get &lt;code&gt;Unable to split host port :${PORT:-9000}: too many colons in address&lt;/code&gt;. Wrap it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'minio server /data --address ":${PORT:-9000}" --console-address ":9001"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha 2 — Railway's edge proxy routes by &lt;code&gt;$PORT&lt;/code&gt;, not by your domain's target port.&lt;/strong&gt; I set the&lt;br&gt;
domain's target port to 9000, deploy went SUCCESS, healthcheck passed internally — and the public&lt;br&gt;
URL 502'd for a few minutes anyway. Setting &lt;code&gt;PORT=9000&lt;/code&gt; as an actual env var (so both the&lt;br&gt;
container and the edge agree) fixed it on the next request. The container looking healthy from&lt;br&gt;
inside tells you nothing about whether the edge can reach it.&lt;/p&gt;

&lt;p&gt;Everything else is normal MinIO: mount a volume at &lt;code&gt;/data&lt;/code&gt; or you lose every bucket on redeploy,&lt;br&gt;
and the web console lives on 9001 separately from the S3 API if you want it exposed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install minio
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;minio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Minio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;

&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Minio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-domain.up.railway.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to do all this by hand: the official &lt;code&gt;minio/minio&lt;/code&gt; image on Docker Hub, plus the&lt;br&gt;
env vars above, is the whole recipe — nothing here is Railway-exclusive.&lt;/p&gt;

&lt;p&gt;Full disclosure since this is my own template: I maintain a one-click Railway deploy for this&lt;br&gt;
(persistent volume already wired, root credentials auto-generated per deploy instead of the&lt;br&gt;
&lt;code&gt;minioadmin&lt;/code&gt; default, &lt;code&gt;PORT&lt;/code&gt; and the shell-wrapped start command already fixed) and I get a&lt;br&gt;
kickback if you deploy through it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://railway.com/deploy/minio-object-storage-s3?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/minio-object-storage-s3?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth knowing before you commit to it either way: MinIO's web console is a separate port (9001)&lt;br&gt;
from the S3 API, so if you want the browser UI you need to expose a second domain pointed at it —&lt;br&gt;
the template doesn't do that for you automatically, you add it after deploy. And it's a real&lt;br&gt;
always-on process (not serverless) — RAM scales roughly with your object metadata, so it's not&lt;br&gt;
free to leave idle at scale, just cheap.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>cloud</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Every free-LLM list is a screenshot of the past. I built one that re-calls every endpoint.</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Wed, 05 Aug 2026 01:33:24 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/every-free-llm-list-is-a-screenshot-of-the-past-i-built-one-that-re-calls-every-endpoint-4jf8</link>
      <guid>https://dev.to/greatsage_sh/every-free-llm-list-is-a-screenshot-of-the-past-i-built-one-that-re-calls-every-endpoint-4jf8</guid>
      <description>&lt;p&gt;Every list of free LLM endpoints I have ever used was accurate exactly once: on the day someone wrote it. After that it is a screenshot. The endpoint quietly adds a key requirement, or the model ID changes, or the provider starts returning 402 to anonymous traffic, and the list still says "free, no key needed" because nobody re-ran it.&lt;/p&gt;

&lt;p&gt;So I built the boring version. &lt;strong&gt;stillworks&lt;/strong&gt; sends a real chat completion to every endpoint it tracks, on a schedule, and publishes what came back — including the failures, next to the successes.&lt;/p&gt;

&lt;p&gt;Disclosure up front: I built this. It is two days old, it is MIT-licensed, and the repo is &lt;a href="https://github.com/bon5co/stillworks" rel="noopener noreferrer"&gt;bon5co/stillworks&lt;/a&gt;. I am posting it because the measurement is the interesting part, not because it is finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "we check" actually means
&lt;/h2&gt;

&lt;p&gt;Not a HEAD request. Not a &lt;code&gt;/v1/models&lt;/code&gt; listing. A real &lt;code&gt;POST /chat/completions&lt;/code&gt; with &lt;code&gt;{"role":"user","content":"hello"}&lt;/code&gt; and, for the keyless shelf, &lt;strong&gt;no &lt;code&gt;Authorization&lt;/code&gt; header at all&lt;/strong&gt;. If the completion comes back, the row is green and stamped with the time. If it 402s, that is published too.&lt;/p&gt;

&lt;p&gt;Here is the state as of 2026-08-05 01:30 UTC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;75 models&lt;/strong&gt; tracked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 of them answered with no Authorization header&lt;/strong&gt; on the latest sweep. The other 55 need a free-tier key and are labelled as such, in the same table.&lt;/li&gt;
&lt;li&gt;Across the last week of keyless probes: &lt;strong&gt;146 successes out of 194 attempts.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;5 of those 20&lt;/strong&gt; answered &lt;em&gt;every&lt;/em&gt; check we made.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 of the 20 answered less than half the time.&lt;/strong&gt; &lt;code&gt;gpt-oss:20b&lt;/code&gt; on llm7 answered 9 of the last 19. A directory would list that as "available".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number is the whole argument. "Available" is not a boolean, and a list that renders it as one is lying to you slightly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API is the product
&lt;/h2&gt;

&lt;p&gt;The page is a table. The thing I actually use is this:&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;-s&lt;/span&gt; https://stillworks.supercapybara.com/api/llm/up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the keyless endpoints that answered, &lt;strong&gt;ranked&lt;/strong&gt; — first by how much of the last week each one answered, then by latency. Which means &lt;code&gt;models[0]&lt;/code&gt; is the pick and the rest are your fallbacks. Each entry carries everything the next call needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minimax-m2.7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llm7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openai_base_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.llm7.io/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3862&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18/18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-05T01:07:40Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json_schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json_object"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claimed_unproved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking that list is the intended usage, not a workaround:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;shelf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://stillworks.supercapybara.com/api/llm/up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;shelf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;            &lt;span class="c1"&gt;# already ranked, best first
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai_base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-needed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;                            &lt;span class="c1"&gt;# your IP hit its quota — next
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want that loop because of the limitation in the next section.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;?format=env&lt;/code&gt; if you would rather paste three lines into a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# stillworks: verified keyless 22 min ago
&lt;/span&gt;&lt;span class="n"&gt;OPENAI_BASE_URL&lt;/span&gt;=&lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;text&lt;/span&gt;.&lt;span class="n"&gt;pollinations&lt;/span&gt;.&lt;span class="n"&gt;ai&lt;/span&gt;/&lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="n"&gt;OPENAI_API_KEY&lt;/span&gt;=&lt;span class="n"&gt;not&lt;/span&gt;-&lt;span class="n"&gt;needed&lt;/span&gt;
&lt;span class="n"&gt;OPENAI_MODEL&lt;/span&gt;=&lt;span class="n"&gt;openai&lt;/span&gt;-&lt;span class="n"&gt;fast&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  proved / claimed_unproved / failed / absent
&lt;/h2&gt;

&lt;p&gt;Capability flags are the part I am most opinionated about, because this is where every registry I have read hand-waves. Four states, and they are not collapsible into two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;We called it and it worked. A tool call we could dispatch. A reply that parsed and matched the schema we sent. An answer about an image we sent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claimed_unproved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The provider claims the feature. Our call has never demonstrated it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;We called it and it failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;absent from all three&lt;/td&gt;
&lt;td&gt;Never probed. &lt;strong&gt;Not the same as failing.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two real rows show why the distinction earns its keep:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pollinations / &lt;code&gt;openai-fast&lt;/code&gt;&lt;/strong&gt; — 39 of 39 plain chat completions succeeded, 295 ms, the most reliable keyless row on the board. The provider claims tool calling. Our tools probe came back &lt;code&gt;402 Payment Required&lt;/code&gt;. So &lt;code&gt;tools&lt;/code&gt; sits in &lt;code&gt;claimed_unproved&lt;/code&gt;, not in &lt;code&gt;proved&lt;/code&gt;, and not in &lt;code&gt;failed&lt;/code&gt; either — we did not disprove the feature, we just never got to see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ovh-anonymous / &lt;code&gt;Qwen3Guard-Gen-8B&lt;/code&gt;&lt;/strong&gt; — 6 of 6 chat completions succeeded, 464 ms. All four capability probes failed outright: &lt;code&gt;feature 'response_format with provided format' is not currently supported&lt;/code&gt;. Those go in &lt;code&gt;failed&lt;/code&gt;. A row can be a perfectly healthy chat endpoint and a dead end for structured output at the same time.&lt;/p&gt;

&lt;p&gt;Of the 20 keyless models, &lt;strong&gt;8 have &lt;code&gt;tools&lt;/code&gt; actually proved&lt;/strong&gt;. If you filter with &lt;code&gt;?feature=tools,vision&lt;/code&gt; you get only models where a real call demonstrated it — unprobed models are excluded rather than optimistically included, which is the conservative direction and occasionally the annoying one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this cannot tell you
&lt;/h2&gt;

&lt;p&gt;This section is not a disclaimer I bolted on. It is the reason I think the project is worth anything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyless quotas are commonly per-IP.&lt;/strong&gt; Verified from our address is not verified from yours. An endpoint that answers our server can return 402 or 429 to you on the first call. We cannot measure your quota and we do not pretend to. This is why the API ranks and returns a list instead of a single winner — the fallbacks are the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows marked &lt;code&gt;auth: "bearer"&lt;/code&gt; were probed with our own free-tier key.&lt;/strong&gt; That proves the endpoint answered &lt;em&gt;our account&lt;/em&gt;. It says nothing about what your signup's free tier includes, or whether the provider is still handing out accounts at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;These are other people's free services.&lt;/strong&gt; Any of them can add a key requirement or vanish between two probes. The timestamps exist because the answer decays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is small and it is new.&lt;/strong&gt; 75 models, two days of history, one probing IP. The track record has to accumulate before "answered 39/39" means much, and right now some rows have a denominator of 5.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Directories list. We check.
&lt;/h2&gt;

&lt;p&gt;That is the entire pitch and I am not going to dress it up further. If you want a green tick that says "up", plenty of pages will give you one. This one gives you "answered 9 of the last 19 attempts, last verified 22 minutes ago, from our IP, and here is the failure that made the other 10" — and then lets you check the working instead of trusting it.&lt;/p&gt;

&lt;p&gt;Site: &lt;a href="https://stillworks.supercapybara.com" rel="noopener noreferrer"&gt;https://stillworks.supercapybara.com&lt;/a&gt;&lt;br&gt;
Repo (MIT): &lt;a href="https://github.com/bon5co/stillworks" rel="noopener noreferrer"&gt;https://github.com/bon5co/stillworks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding an endpoint is a PR against &lt;code&gt;apps/audit/seed.go&lt;/code&gt; with the base URL, the chat path, and whether it needs a key. The prober verifies it on the next cycle and publishes whatever actually happens, including nothing.&lt;/p&gt;

&lt;p&gt;It is Go, Postgres, and templ, with no JavaScript framework — the table filters and sorts server-side and the client script only enhances. Provider keys live in the environment and go out on probes only; they are never rendered, logged, written to the database, or returned by the API, and there is a test that fails if one appears in a response.&lt;/p&gt;

&lt;p&gt;Happy to be told which endpoints I am missing, or which of my numbers you cannot reproduce from your own IP. The second one is more useful.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>go</category>
      <category>api</category>
    </item>
    <item>
      <title>Self-hosting Hermes Agent on Railway: the dashboard that refuses to open unauthenticated</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Tue, 04 Aug 2026 04:05:31 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosting-hermes-agent-on-railway-the-dashboard-that-refuses-to-open-unauthenticated-11ai</link>
      <guid>https://dev.to/greatsage_sh/self-hosting-hermes-agent-on-railway-the-dashboard-that-refuses-to-open-unauthenticated-11ai</guid>
      <description>&lt;p&gt;Full disclosure up front: I maintain a Railway template for this and get a kickback if you deploy through it. That's the whole pitch — everything below is the actual writeup.&lt;/p&gt;

&lt;p&gt;I've been poking at the self-improving-agent space (OpenClaw and friends) and wanted to try Nous Research's Hermes Agent without handing it API keys to a hosted box. It's genuinely interesting — it accumulates skills over sessions instead of starting cold every time — but self-hosting it on a PaaS like Railway surfaces two gotchas that aren't obvious from the upstream docs, and one design decision that's easy to trip over if you're used to disabling auth "for now."&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The dashboard fails closed, not open
&lt;/h2&gt;

&lt;p&gt;Since Nous Research's June 2026 hardening pass, the Hermes dashboard refuses to bind to a non-loopback address without a registered auth provider. If you leave the password variable empty expecting an open dashboard for local testing, you don't get one — you get no dashboard at all, and &lt;code&gt;HERMES_DASHBOARD_INSECURE&lt;/code&gt; is accepted and silently ignored. It fails safe, which I'd rather have than the alternative, but it means "just don't set a password" isn't a valid quick-start path anymore. The fix I ended up with: generate a password on first boot if none is supplied, persist it to the volume so it survives redeploys, and print it to the deploy logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HERMES_DASHBOARD_BASIC_AUTH_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pw_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;HERMES_DASHBOARD_BASIC_AUTH_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pw_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nv"&gt;HERMES_DASHBOARD_BASIC_AUTH_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-dc&lt;/span&gt; &lt;span class="s1"&gt;'A-Za-z0-9'&lt;/span&gt; &amp;lt; /dev/urandom | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 24&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HERMES_HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HERMES_DASHBOARD_BASIC_AUTH_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pw_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[railway] generated dashboard password: &lt;/span&gt;&lt;span class="nv"&gt;$HERMES_DASHBOARD_BASIC_AUTH_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Railway injects its own PORT, and Hermes has to follow it, not its own default
&lt;/h2&gt;

&lt;p&gt;Railway sets &lt;code&gt;PORT=8080&lt;/code&gt; on every service at runtime — it doesn't show up in the variables list, it's just there. Hermes's dashboard defaults to 9119. Point Railway's domain at 9119 (Hermes's own default) and you get a clean 502 with nothing wrong in the logs — the app is listening on a port nothing is routing to. The entrypoint has to read &lt;code&gt;$PORT&lt;/code&gt; and set &lt;code&gt;HERMES_DASHBOARD_PORT&lt;/code&gt; from it before the dashboard starts, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The volume isn't optional, it's the entire value proposition
&lt;/h2&gt;

&lt;p&gt;"Self-improving" means skills and session state live on disk under &lt;code&gt;HERMES_HOME&lt;/code&gt; (&lt;code&gt;/opt/data&lt;/code&gt; by default). No persistent volume means every redeploy wipes what the agent has learned, which defeats the point of running it at all rather than just calling an API.&lt;/p&gt;

&lt;p&gt;None of this is a knock on Hermes itself — it's the normal gap between "runs great with &lt;code&gt;docker run&lt;/code&gt; on your own box" and "runs on a platform that injects its own port and tears down containers on redeploy." If you'd rather do it yourself: the image is &lt;code&gt;nousresearch/hermes-agent&lt;/code&gt;, and my wrapper (entrypoint + Dockerfile, MIT, nothing hidden) is at &lt;a href="https://github.com/bon5co/hermes-agent-railway" rel="noopener noreferrer"&gt;bon5co/hermes-agent-railway&lt;/a&gt; if you want to see exactly what it does or run it on your own infra instead of Railway.&lt;/p&gt;

&lt;p&gt;If you'd rather skip wiring it up yourself, the Railway template does all three of the above automatically — one variable on the form (the dashboard password, pre-filled with a generated secret), everything else baked in: &lt;a href="https://railway.com/deploy/hermes-agent-or-openclaw-alternative-not?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory" rel="noopener noreferrer"&gt;https://railway.com/deploy/hermes-agent-or-openclaw-alternative-not?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=inventory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth knowing before you commit to it either way: this is a fast-moving project and the agent's behavior/skill format has already changed shape once in a few months, so don't treat whatever it learns as long-term stable state — export anything you care about.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cloud</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Self-hosting Gitea on Railway: the two gotchas that break clone URLs and SSH</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Tue, 04 Aug 2026 02:09:52 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosting-gitea-on-railway-the-two-gotchas-that-break-clone-urls-and-ssh-5918</link>
      <guid>https://dev.to/greatsage_sh/self-hosting-gitea-on-railway-the-two-gotchas-that-break-clone-urls-and-ssh-5918</guid>
      <description>&lt;p&gt;Self-hosting Gitea on Railway: the two gotchas that break clone URLs and SSH&lt;/p&gt;

&lt;p&gt;If you want a private git server without paying for GitHub Enterprise or wrestling with GitLab's resource footprint, Gitea is the obvious pick — it's a single Go binary bundling git hosting, issues, PRs, CI hooks and a web UI, and it runs comfortably on the smallest box you've got. I run a one-click Railway template for it and wanted to write up the two things that actually trip people up when they deploy it there, because neither is obvious from the defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full disclosure up front:&lt;/strong&gt; I maintain the Railway template linked below and get a kickback if you deploy through it. If you'd rather not use it, the whole setup is just &lt;code&gt;docker run -p 3000:3000 -p 22:22 gitea/gitea:latest&lt;/code&gt; with a volume at &lt;code&gt;/data&lt;/code&gt; — everything below applies either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: clone URLs break without ROOT_URL
&lt;/h2&gt;

&lt;p&gt;Gitea generates the clone URL you see in the UI (and the one it puts in CI webhook payloads) from &lt;code&gt;GITEA__server__ROOT_URL&lt;/code&gt;. Leave it unset on a platform like Railway and it defaults to whatever internal hostname the container sees itself as — not your public domain. Result: the UI looks fine, but every "copy clone URL" button hands people a URL that doesn't resolve. Set &lt;code&gt;GITEA__server__ROOT_URL=https://&amp;lt;your-domain&amp;gt;/&lt;/code&gt; before you push anything through it, not after — some of it (webhook config, in particular) gets baked in rather than computed live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: SSH clone needs its own TCP proxy
&lt;/h2&gt;

&lt;p&gt;Railway's HTTP domain only forwards port 3000. &lt;code&gt;git clone https://...&lt;/code&gt; works fine over that. &lt;code&gt;git clone git@...&lt;/code&gt; does not, because Gitea's SSH daemon listens on 22 and nothing is routing to it — you have to add a second TCP proxy pointed at 22 explicitly in Railway's networking settings. Easy to miss because the web UI and HTTP clone both work perfectly without it, so there's no error until someone tries to clone over SSH and gets connection refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite is fine solo, not forever
&lt;/h2&gt;

&lt;p&gt;The template ships with Gitea's built-in SQLite backend, which is genuinely fine for a solo dev or a small team — no extra service to run, no connection string to manage. It's single-writer though, so if you've got CI hammering it with webhook-triggered pushes from multiple people at once, you'll eventually feel it. Gitea supports pointing at an external Postgres with a couple of env vars whenever you outgrow it; I also maintain a plain Postgres 17 template if you want a target for that migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;

&lt;p&gt;One-click, volume already wired to &lt;code&gt;/data&lt;/code&gt; so repo data survives redeploys: &lt;a href="https://railway.com/deploy/gitea-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto" rel="noopener noreferrer"&gt;https://railway.com/deploy/gitea-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions on memory sizing or the SSH proxy setup in the comments — the SSH part especially trips people up on any platform that proxies by domain rather than raw TCP.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>docker</category>
      <category>git</category>
    </item>
    <item>
      <title>Self-hosted Google Analytics alternative: the ClickHouse mistake that logs you out on every redeploy</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Sun, 02 Aug 2026 04:04:21 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/self-hosted-google-analytics-alternative-the-clickhouse-mistake-that-logs-you-out-on-every-redeploy-4oa0</link>
      <guid>https://dev.to/greatsage_sh/self-hosted-google-analytics-alternative-the-clickhouse-mistake-that-logs-you-out-on-every-redeploy-4oa0</guid>
      <description>&lt;p&gt;Cookie banners are a bad trade. You annoy every visitor to keep a GA property that mostly tells&lt;br&gt;
you what you already knew. If you've been eyeing the self-hosted analytics space — Plausible,&lt;br&gt;
Umami, Swetrix — you've probably noticed they're not interchangeable: Plausible and Umami are&lt;br&gt;
intentionally minimal (pageviews, referrers, a Postgres or SQLite backend, done). Swetrix goes&lt;br&gt;
further — funnels, custom events, error tracking, performance monitoring, session replay, no&lt;br&gt;
consent banner required — and it pays for that with a heavier backend: ClickHouse instead of a&lt;br&gt;
row-store.&lt;/p&gt;

&lt;p&gt;That heavier backend is also where the actual self-hosting trap lives, and it's not the one&lt;br&gt;
people expect.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap: ClickHouse isn't just your event store, it's your login too
&lt;/h2&gt;

&lt;p&gt;Most people assume the risk of a lightly-configured analytics stack is "I might lose some&lt;br&gt;
traffic history." With Swetrix Community Edition that's not the failure mode. Swetrix has no&lt;br&gt;
second database — &lt;code&gt;initialise_selfhosted.js&lt;/code&gt; creates &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;refresh_token&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;salt&lt;/code&gt; directly in ClickHouse, and the API reads accounts from the same place. There's no&lt;br&gt;
Postgres quietly holding your login while ClickHouse only holds pageviews.&lt;/p&gt;

&lt;p&gt;So a ClickHouse container with no persistent volume doesn't lose your dashboards on the next&lt;br&gt;
deploy — it loses your account. I reproduced this directly: spin up ClickHouse without a mounted&lt;br&gt;
volume, restart the container, and the next query is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;
&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;UNKNOWN_TABLE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing. No first-user prompt, no error banner telling you why — the app just looks freshly&lt;br&gt;
installed. On a platform like Railway, where a redeploy is a completely normal Tuesday (a config&lt;br&gt;
change, a restart, a plan upgrade), an unmounted ClickHouse means your Swetrix instance quietly&lt;br&gt;
resets itself at some point, and you won't know until you try to log in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second trap, and I hit it myself first
&lt;/h2&gt;

&lt;p&gt;Swetrix's web app calls its own backend at &lt;code&gt;${BASE_URL}/backend&lt;/code&gt;, so whatever sits in front of&lt;br&gt;
it has to strip that prefix before forwarding to the API. The natural-looking nginx config is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$api_upstream&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="nv"&gt;$api_upstream&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks fine and is wrong. The moment the upstream is a &lt;em&gt;variable&lt;/em&gt;, nginx skips its usual&lt;br&gt;
location-prefix rewrite and forwards the literal request URI instead. Every &lt;code&gt;/backend/*&lt;/code&gt; call&lt;br&gt;
lands on the API's root path, not on the route it's supposed to hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /backend/v1/auth/register
{"message":"Cannot POST /","statusCode":404}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know this one's an easy trap because my own first draft of the gateway config had it. Fixed&lt;br&gt;
with an explicit rewrite instead of leaning on the variable-upstream shortcut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;^/backend/(.*)&lt;/span&gt;$ &lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register returns 201, login 200.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tax you're already paying: ClickHouse pinned to one thread
&lt;/h2&gt;

&lt;p&gt;Swetrix's own self-hosting config ships &lt;code&gt;concurrent_threads_soft_limit_num=1&lt;/code&gt;, tuned for a 1GB&lt;br&gt;
VPS. If you copy that onto a real container with real vCPUs, every dashboard query serialises&lt;br&gt;
onto a single thread regardless of what you're paying for. I measured the same aggregation query&lt;br&gt;
three times each way on a 4-vCPU container: 3.89s median pinned to one thread vs 2.16s reading&lt;br&gt;
the actual cgroup quota — 1.8x, for free, just by not hardcoding a number meant for a $5 VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;I maintain a Railway template for Swetrix CE (v5.3.1, pinned) that fixes all three of the above —&lt;br&gt;
ClickHouse and Redis both on persistent volumes, the gateway rewrite, and thread count read from&lt;br&gt;
the container's actual quota instead of a hardcoded &lt;code&gt;1&lt;/code&gt;. Full disclosure: I get a kickback if you&lt;br&gt;
deploy through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy link:&lt;/strong&gt; &lt;a href="https://railway.com/deploy/swetrix-ce-v531-or-google-analytics-alte?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto" rel="noopener noreferrer"&gt;https://railway.com/deploy/swetrix-ce-v531-or-google-analytics-alte?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather run it yourself with zero affiliation to me, the fixes above are just as valid on&lt;br&gt;
raw Docker/Compose — mount a volume on ClickHouse's data directory, fix the gateway rewrite, and&lt;br&gt;
set &lt;code&gt;concurrent_threads_soft_limit_num&lt;/code&gt; from &lt;code&gt;nproc&lt;/code&gt; instead of copying the upstream default.&lt;br&gt;
Upstream repo: &lt;a href="https://github.com/Swetrix/swetrix" rel="noopener noreferrer"&gt;https://github.com/Swetrix/swetrix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it doesn't fit:&lt;/strong&gt; if you want the absolute simplest possible self-hosted analytics —&lt;br&gt;
pageviews and referrers, one container, no ClickHouse to think about — Umami or Plausible are&lt;br&gt;
honestly the better call. Swetrix earns its extra weight only if you actually want funnels,&lt;br&gt;
custom events, error tracking or session replay in the same tool. Know which one you're actually&lt;br&gt;
buying before you commit a database engine to it.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>backend</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>NocoDB self-hosted: the Airtable alternative, and the one setting that decides if it survives a redeploy</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Sat, 01 Aug 2026 20:10:04 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/nocodb-self-hosted-the-airtable-alternative-and-the-one-setting-that-decides-if-it-survives-a-85k</link>
      <guid>https://dev.to/greatsage_sh/nocodb-self-hosted-the-airtable-alternative-and-the-one-setting-that-decides-if-it-survives-a-85k</guid>
      <description>&lt;h1&gt;
  
  
  NocoDB self-hosted: the Airtable alternative, and the one setting that decides if it survives a redeploy
&lt;/h1&gt;

&lt;p&gt;If Airtable's per-editor pricing has ever made you wince, NocoDB is the usual answer: point it at a database and it gives you a spreadsheet-style UI on top — grid, gallery, kanban, and form views, plus a REST API auto-generated for every table. It's genuinely good at that job. It's also an "app layer," not a passive viewer — worth knowing before you commit real workflows to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does to your data
&lt;/h2&gt;

&lt;p&gt;NocoDB doesn't just display your tables, it wraps them. Every table it creates gets its own metadata columns (row ordering, internal tracking), and by default a "deleted" row isn't gone — it's flagged, not removed. If you ever point an external BI tool or a raw SQL query at the same database, you'll see rows the NocoDB UI told you didn't exist anymore. Dropdown/select field options live in NocoDB's metadata too, not as a database constraint, so anything writing to the table directly (an API script, a n8n workflow) can insert values NocoDB never intended. None of this is a bug — it's the tradeoff of "spreadsheet UI over an arbitrary SQL schema." Know it going in, and it's fine; find out after loading production data into it, less fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deployment trap that's actually avoidable
&lt;/h2&gt;

&lt;p&gt;Separate from the app-layer tradeoffs above, there's a purely operational mistake that's on you, not NocoDB: &lt;strong&gt;&lt;code&gt;NC_DB&lt;/code&gt; decides where your metadata lives, and if you leave it unset it defaults to SQLite inside the container.&lt;/strong&gt; That's not just "your bases" — it's every user account, every view, every shared link, every API token. On a platform like Railway where a redeploy gets a fresh container filesystem, an unset &lt;code&gt;NC_DB&lt;/code&gt; means your entire instance — logins included — resets to zero on the next push. It's the same shape of trap PocketBase deployers hit with an unmounted volume, except here it also takes out auth, not just data.&lt;/p&gt;

&lt;p&gt;The fix is boring: point &lt;code&gt;NC_DB&lt;/code&gt; at a real Postgres instance from the first boot, and give that Postgres its own volume so its data directory survives too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying it without hitting that
&lt;/h2&gt;

&lt;p&gt;I maintain a Railway template for this — full disclosure, I get a kickback if you deploy through it: &lt;a href="https://railway.com/deploy/nocodb-airtable-al-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto" rel="noopener noreferrer"&gt;https://railway.com/deploy/nocodb-airtable-al-1?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two services: NocoDB itself, and a Postgres 17 instance wired in as &lt;code&gt;NC_DB&lt;/code&gt; from boot, both on persistent volumes (Postgres's data directory, and a second volume at &lt;code&gt;/usr/app/data&lt;/code&gt; for file attachments — those are written to disk, not the database, so they need their own mount). The auth secret is generated once and pinned rather than left to regenerate on restart, which would otherwise invalidate every session and API token you'd issued.&lt;/p&gt;

&lt;p&gt;If you'd rather not go through the template, it's the same idea on any Docker host:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nocodb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nocodb/nocodb:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NC_DB=pg://postgres:5432?u=nocodb&amp;amp;p=&amp;lt;password&amp;gt;&amp;amp;d=nocodb&lt;/span&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;nc_data:/usr/app/data&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=nocodb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=&amp;lt;password&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=nocodb&lt;/span&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;pg_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nc_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whichever way you run it, the first account you create becomes the super admin — do that immediately after boot, before anyone else can beat you to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits, where it doesn't
&lt;/h2&gt;

&lt;p&gt;Good fit: an internal admin panel over a database your team already has, a lightweight CRM/tracker with kanban and form views, or just needing a REST API in front of tables without writing one. It's also genuinely nice for connecting to an &lt;em&gt;external&lt;/em&gt; Postgres/MySQL/SQL Server you already run and browsing it without copying data anywhere.&lt;/p&gt;

&lt;p&gt;Worse fit: if you need the app layer to stay invisible — clean external SQL reports, automations that write straight to the tables, or a schema you designed first and want NocoDB to just mirror without touching — its metadata injection and soft-delete behavior will fight you. If that's the actual requirement, a database-mirror tool that doesn't own the schema (or, for a pure spreadsheet feel without the DB semantics, Baserow) is the more honest choice. NocoDB is the right tool when you want the app layer; it's the wrong one when you're trying to avoid having one.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>PocketBase in one command: a Firebase-sized backend that's just a SQLite file</title>
      <dc:creator>Great Sage</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:08:30 +0000</pubDate>
      <link>https://dev.to/greatsage_sh/pocketbase-in-one-command-a-firebase-sized-backend-thats-just-a-sqlite-file-48ed</link>
      <guid>https://dev.to/greatsage_sh/pocketbase-in-one-command-a-firebase-sized-backend-thats-just-a-sqlite-file-48ed</guid>
      <description>&lt;h1&gt;
  
  
  PocketBase in one command: a Firebase-sized backend that's just a SQLite file
&lt;/h1&gt;

&lt;p&gt;If you've ever reached for Firebase or Supabase for a side project and immediately hit the "wait, why do I need three dashboards and a billing alert for a todo app" wall — PocketBase is worth ten minutes of your time.&lt;/p&gt;

&lt;p&gt;It's a single Go binary. No separate database server, no message broker, nothing to wire together. It ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLite as the datastore (yes, really — and for anything under a few hundred concurrent writers it's genuinely fine, Litestream-style backups aside)&lt;/li&gt;
&lt;li&gt;Built-in auth (email/password, OAuth2 providers, API keys)&lt;/li&gt;
&lt;li&gt;Realtime subscriptions over SSE&lt;/li&gt;
&lt;li&gt;File storage with image thumbnailing baked in&lt;/li&gt;
&lt;li&gt;An admin UI at &lt;code&gt;/_/&lt;/code&gt; for managing collections without touching SQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is one process. That's the pitch and also the honest limitation: it's not built for horizontal scaling or huge concurrent write volume. If you're building the next Discord, don't. If you're building an MVP, an internal tool, a mobile app backend, or literally 80% of "I just need auth + a database + file uploads" projects, it removes an entire category of infra decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying it
&lt;/h2&gt;

&lt;p&gt;I maintain a Railway template for it — full disclosure, I get a kickback if you deploy through it: &lt;a href="https://railway.com/deploy/pocketbase-regular-update?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto" rel="noopener noreferrer"&gt;https://railway.com/deploy/pocketbase-regular-update?referralCode=Z1xivh&amp;amp;utm_medium=integration&amp;amp;utm_source=template&amp;amp;utm_campaign=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's the stock &lt;code&gt;ghcr.io/muchobien/pocketbase&lt;/code&gt; image with a volume mounted at &lt;code&gt;/pb_data&lt;/code&gt; so your SQLite file and uploads survive a redeploy (the one thing people forget when they run this in a bare container — no volume means your whole database vanishes on every push), plus the admin password generated as a random secret instead of a default you have to remember to change.&lt;/p&gt;

&lt;p&gt;If you'd rather not go through the template, it's the same image on any Docker host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; pb_data:/pb_data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PB_ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PB_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;something-real&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8090:8090 &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/muchobien/pocketbase:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, once it's up, the admin dashboard is at &lt;code&gt;https://&amp;lt;your-domain&amp;gt;/_/&lt;/code&gt; — define your collections there (or via the REST API), and every collection automatically gets a CRUD + realtime API for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually fits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prototyping something that needs auth + a database + file uploads without spinning up three services&lt;/li&gt;
&lt;li&gt;A self-hosted Firebase/Supabase alternative when you don't need Supabase's Postgres-specific features (RLS, extensions like pgvector)&lt;/li&gt;
&lt;li&gt;Backend for a mobile app where you want realtime sync without hand-rolling websockets&lt;/li&gt;
&lt;li&gt;Internal admin tools where the built-in dashboard is the whole UI you need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where it doesn't: anything that needs multi-writer horizontal scaling, or a use case that genuinely needs Postgres under the hood. SQLite is the right tool until it very suddenly isn't — know your write volume before you commit.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
