<?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: GDS K S</title>
    <description>The latest articles on DEV Community by GDS K S (@thegdsks).</description>
    <link>https://dev.to/thegdsks</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%2F3592860%2F7dec468f-4f91-4b1d-9d24-99091e204707.jpg</url>
      <title>DEV Community: GDS K S</title>
      <link>https://dev.to/thegdsks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thegdsks"/>
    <language>en</language>
    <item>
      <title>Your deploy box is the target: supply chain defenses after this week's attacks</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Thu, 12 Nov 2026 17:41:00 +0000</pubDate>
      <link>https://dev.to/thegdsks/your-deploy-box-is-the-target-supply-chain-defenses-after-this-weeks-attacks-1o3d</link>
      <guid>https://dev.to/thegdsks/your-deploy-box-is-the-target-supply-chain-defenses-after-this-weeks-attacks-1o3d</guid>
      <description>&lt;h1&gt;
  
  
  Your deploy box is the target: supply chain defenses after this week's attacks
&lt;/h1&gt;

&lt;p&gt;I read the OpenSourceMalware report on Tuesday and then did something simple. I opened my own CI config and listed every secret a single build job could see. The attack it describes starts with a PR from a brand new account labeled "CI diagnostics" that sends environment variables to a remote URL.&lt;/p&gt;

&lt;p&gt;The answer was uncomfortable. A build job on my side holds a registry token, a deploy key and a cloud credential, all in one shell. That is exactly the shopping list attackers wrote down this week.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What it means for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A stolen developer key pushed a dropper into &lt;code&gt;@ethersphere/bee-js&lt;/code&gt; 13.3.0, and the publish workflow shipped it to npm and GitHub&lt;/td&gt;
&lt;td&gt;Nov 9&lt;/td&gt;
&lt;td&gt;Your release pipeline will publish whatever a valid key commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A brand new GitHub account sent CI exfiltration PRs, a prompt injection for reviewers and a Rust crate with a &lt;code&gt;build.rs&lt;/code&gt; stealer&lt;/td&gt;
&lt;td&gt;Nov 10&lt;/td&gt;
&lt;td&gt;Build scripts and PR checks run with your secrets in reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket's hardening guide says most 2025 and 2026 CI compromises came through &lt;code&gt;npm install&lt;/code&gt;, not workflow bugs&lt;/td&gt;
&lt;td&gt;Nov 3&lt;/td&gt;
&lt;td&gt;Install time is the weak spot, so lock it down first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. What the two attacks actually did
&lt;/h2&gt;

&lt;p&gt;OX Security reported on November 9 that attackers likely used a stolen developer access key to commit malicious code to the official bee-js and swarm-desktop repositories. The commit triggered the normal publish workflow, which created the npm release and the GitHub release. No one broke into the registry. The pipeline did its job with a bad commit.&lt;/p&gt;

&lt;p&gt;The dropper then asked a command server for a payload matched to the victim's operating system. It uploaded the local Swarm node's encrypted key and config, then installed a backdoor. On Linux and macOS the backdoor looks up its server address from an Ethereum contract, so the attacker can move it whenever they like. OX counts about 16,800 monthly downloads of exposure. That is small. The pattern is not.&lt;/p&gt;

&lt;p&gt;The second attack, written up by OpenSourceMalware on November 10, came from a GitHub account created about seven hours before the report. It ran four moves in a row. Two PRs tried to send environment variables and signing material out of CI. A third PR added a paragraph to &lt;code&gt;AGENTS.md&lt;/code&gt; telling reviewers, including automated ones, not to flag those uploads. Another PR added a Rust crate, &lt;code&gt;lwd-uri-util&lt;/code&gt;, whose &lt;code&gt;build.rs&lt;/code&gt; gathers &lt;code&gt;.git-credentials&lt;/code&gt;, &lt;code&gt;.aws/credentials&lt;/code&gt; and &lt;code&gt;.npmrc&lt;/code&gt;. Finally, a fake security disclosure sent two maintainers to a cloned sign-in page.&lt;/p&gt;

&lt;p&gt;Read that list again if you deploy code. The target is not your app. The target is the box where your app gets built.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why the build environment is the prize
&lt;/h2&gt;

&lt;p&gt;A typical CI job carries a registry push token, a cloud credential and a deploy SSH key. It also runs arbitrary install scripts from hundreds of transitive packages. One malicious &lt;code&gt;postinstall&lt;/code&gt; or &lt;code&gt;build.rs&lt;/code&gt; inherits all of it.&lt;/p&gt;

&lt;p&gt;The OpenSourceMalware report lists what the Rust stealer wants: GitHub, AWS, Cargo, npm, PyPI, Android signing keys and CI secrets. Its advice for anyone who built the affected branches is to revoke and reissue every secret present in that build environment. That is a long afternoon. The cheaper move is to make sure the build never holds most of them.&lt;/p&gt;

&lt;p&gt;Here is the rule I now apply. A job that installs dependencies gets no deploy credentials. A job that deploys installs nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split install from deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"v*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@&amp;lt;full-40-char-sha&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci --ignore-scripts&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@&amp;lt;full-40-char-sha&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist/&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;   &lt;span class="c1"&gt;# holds the deploy secret, needs approval
&lt;/span&gt;    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@&amp;lt;full-40-char-sha&amp;gt;&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/push-release.sh&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build job sees no secret at all. The deploy job installs nothing, so a poisoned dependency never runs next to the token. The &lt;code&gt;production&lt;/code&gt; environment can require a human approval before the second job starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pins: tags move, digests do not
&lt;/h2&gt;

&lt;p&gt;Socket's guide makes a point I keep repeating to people. Version tags are movable pointers. In the &lt;code&gt;tj-actions/changed-files&lt;/code&gt; attack, pinning by commit SHA kept every repository safe. In the March 2026 &lt;code&gt;trivy-action&lt;/code&gt; incident, Socket reports 75 of 76 version tags were force-pushed.&lt;/p&gt;

&lt;p&gt;A SHA proves the content cannot change. It does not prove where the commit came from, because impostor commits from forks can still resolve. Pin, and read the diff when you bump.&lt;/p&gt;

&lt;p&gt;Images work the same way. A tag like &lt;code&gt;node:22&lt;/code&gt; is a pointer. A digest is content.&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="c"&gt;# resolve once, on a machine you trust&lt;/span&gt;
docker buildx imagetools inspect node:22-slim &lt;span class="o"&gt;|&lt;/span&gt; grep Digest
&lt;span class="c"&gt;# Digest: sha256:&amp;lt;64 hex chars&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# then pin it in the Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# FROM node:22-slim@sha256:&amp;lt;64 hex chars&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do the same for the images you run, not only the ones you build. In a compose file:&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/team/web@sha256:&amp;lt;64 hex chars&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also keep to upstream-only namespaces. Pull &lt;code&gt;library/postgres&lt;/code&gt; or the vendor's own namespace, not a lookalike from a stranger. Even a real namespace can be hijacked, so digests matter more than names. Names only remove the cheapest attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a cooldown
&lt;/h3&gt;

&lt;p&gt;Socket cites a Wiz estimate that a 7 to 14 day cooldown catches 80 to 90 percent of supply chain attacks, and notes Dependabot applies a three day cooldown by default on github.com. Ethersphere reverted the bad npm version only after it shipped. If your lockfile refuses anything published in the last week, you skip most of that window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .npmrc
&lt;/span&gt;&lt;span class="na"&gt;ignore-scripts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;min-release-age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scripts then run only where you allow them. Check your npm version supports the release-age key before you rely on it, since Socket describes it as a setting for &lt;code&gt;.npmrc&lt;/code&gt; and your update bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cut what the token can do
&lt;/h2&gt;

&lt;p&gt;Socket calls the write-scoped &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; the usual escalation step from a foothold to a real incident. Set &lt;code&gt;permissions: {}&lt;/code&gt; at workflow level, as in the example above, and grant per job. Avoid &lt;code&gt;pull_request_target&lt;/code&gt; with untrusted code. Pass PR titles through &lt;code&gt;env:&lt;/code&gt; instead of inlining them in &lt;code&gt;run:&lt;/code&gt; blocks. Run &lt;code&gt;zizmor&lt;/code&gt; in CI to catch the rest.&lt;/p&gt;

&lt;p&gt;Then treat registry tokens the same way. A publish token that only works for one package, with a short life, turns a stolen key into a small problem. The Ethersphere case shows the other side: a long-lived developer key plus an automatic publish workflow equals a release anyone with the key can ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Egress limits, with honest limits
&lt;/h2&gt;

&lt;p&gt;The last layer is the one most people skip. When the stealer cannot reach its server, it collects secrets and has nowhere to put them. The Rust crate uploads to a public webhook endpoint. The Ethersphere dropper called a fresh &lt;code&gt;.xyz&lt;/code&gt; domain registered two days earlier. A deny-by-default outbound list blocks both.&lt;/p&gt;

&lt;p&gt;Socket's advice is to run an egress agent in audit mode first, build the allowlist from real traffic, then enforce. Socket also admits the limit: allowlisting cannot stop exfiltration to hosts you must allow, such as &lt;code&gt;github.com&lt;/code&gt;. Egress is a way to cut damage, not a wall.&lt;/p&gt;

&lt;p&gt;On the server that runs your apps, the same idea applies at runtime. If the web container only talks to your payment API, say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What we do about this
&lt;/h2&gt;

&lt;p&gt;We built outbound allowlists and image pinning into Levelrail for the deploy side. Per the docs, Levelrail pins an image pulled from a registry to its digest at deploy time, &lt;code&gt;APP_IMAGE_VERIFY_MODE=enforce&lt;/code&gt; refuses unsigned or unpinned images on promotion, and a service can declare an &lt;code&gt;egress&lt;/code&gt; allowlist of host and port pairs. Two honest limits from the same docs: without the egress block the outbound traffic is open, and signature checks support cosign key pairs, not keyless signatures. Build-time secrets are opt in per secret for that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed this week
&lt;/h2&gt;

&lt;p&gt;I did five things, in order of effort.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Moved deploy tokens out of any job that runs &lt;code&gt;npm ci&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pinned every action to a full SHA and every base image to a digest.&lt;/li&gt;
&lt;li&gt;Turned on a seven day release-age floor and &lt;code&gt;ignore-scripts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;permissions: {}&lt;/code&gt; as the workflow default.&lt;/li&gt;
&lt;li&gt;Put the deploy job behind an approval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of it takes more than a day. The attacks this week were small: about 16,800 monthly downloads and one wallet-focused crate. The same techniques against a package with millions of installs would look different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Attackers are skipping your code and going for the machine that builds and ships it. Split install from deploy, pin by digest, add a cooldown, shrink token scopes and limit outbound traffic. How many secrets does your slowest, most neglected CI job hold right now?&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources checked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OX Security, Ethersphere compromise, Nov 9 2026: &lt;a href="https://www.ox.security/blog/ethersphere-hijacked-with-crypto-stealing-malware-via-github-npm/" rel="noopener noreferrer"&gt;https://www.ox.security/blog/ethersphere-hijacked-with-crypto-stealing-malware-via-github-npm/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSourceMalware, malicious Rust crate targeting wallet developers, Nov 10 2026: &lt;a href="https://opensourcemalware.com/blog/cryptocurrency-being-targeted-in-multi-faceted-attack" rel="noopener noreferrer"&gt;https://opensourcemalware.com/blog/cryptocurrency-being-targeted-in-multi-faceted-attack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Socket, Hardening GitHub Actions Against Outside Threats, Nov 3 2026: &lt;a href="https://socket.dev/blog/hardening-github-actions-against-outside-threats" rel="noopener noreferrer"&gt;https://socket.dev/blog/hardening-github-actions-against-outside-threats&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Levelrail docs: docs/images.md, docs/app-spec-reference.md&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>supplychain</category>
      <category>devops</category>
      <category>npm</category>
    </item>
    <item>
      <title>I redeployed an app three times while sending 4,000 requests. Two cut over, one is a mystery</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:37:33 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-redeployed-an-app-three-times-while-sending-4000-requests-two-cut-over-one-is-a-mystery-23f8</link>
      <guid>https://dev.to/thegdsks/i-redeployed-an-app-three-times-while-sending-4000-requests-two-cut-over-one-is-a-mystery-23f8</guid>
      <description>&lt;p&gt;"Zero downtime" is one of those phrases that sounds finished until you try to prove it. So I built a small load script and a rule: send steady traffic through the real HTTPS ingress, redeploy the app three times, and count every failed request.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A tiny web app (a whoami container) behind the platform's proxy, over HTTPS with a local certificate.&lt;/li&gt;
&lt;li&gt;About 20 requests per second, for the whole run.&lt;/li&gt;
&lt;li&gt;Three redeploys, each by API with a new image tag.&lt;/li&gt;
&lt;li&gt;Default drain settings: 15 seconds of draining, then a 3 minute hold so you can roll back, and a stability check before the old container goes away.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4,313 requests. 0 failed or non-2xx.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Latency: p50 5 ms, p95 68 ms, p99 208 ms.&lt;/li&gt;
&lt;li&gt;Longest gap between two successful responses: &lt;strong&gt;0.111 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran the same idea on a real 2 vCPU VPS too, at 10 requests per second: three redeploys, 458 requests, no failed responses, longest gap 0.36 seconds. A restart that waits for the app to be healthy took about 12 seconds each time, and roughly 8 of those seconds are the stability check deliberately making sure the new container is not a one-hit wonder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things that did not go to plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The disk guard refused my deploy.&lt;/strong&gt; On the first attempt, the third deploy came back with a 507. The platform has a guard that refuses a deploy when the disk is too full, and my laptop disk was at 90 percent. The guard was right. I lowered its thresholds for the test and moved on. A deploy platform that says no before it fills your disk is doing its job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One redeploy never answered.&lt;/strong&gt; Of the three redeploys, two cut over cleanly. The third was accepted by the API but the new version did not answer within my three minute wait. No request failed, because the old container kept serving, but I do not know why the cutover did not finish. The log ends with a cancelled write at shutdown, and I have not investigated further. It might be the hold window, a slow image pull or a real stall. I would rather tell you that than pretend the test was perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned about testing zero downtime
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Count failures, not vibes. A number like 0 out of 4,313 is the claim. Anything softer is marketing.&lt;/li&gt;
&lt;li&gt;Look at the longest gap, not just the average. Averages hide a one second hole.&lt;/li&gt;
&lt;li&gt;Test on a nearly full disk at least once. Real servers are rarely tidy.&lt;/li&gt;
&lt;li&gt;Keep the unexplained result. It is the next bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The script is in the repo if you want to point it at your own setup. The platform is Levelrail, which I build and run my own sites on: &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;https://github.com/glincker/levelrail&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you check that a deploy really did not drop requests?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>webdev</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I tested my own deploy platform on a real VPS | Levelrail</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:36:58 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-tested-my-own-deploy-platform-on-a-real-vps-and-the-first-backup-failed-56md</link>
      <guid>https://dev.to/thegdsks/i-tested-my-own-deploy-platform-on-a-real-vps-and-the-first-backup-failed-56md</guid>
      <description>&lt;p&gt;I had 15+ sites spread across Coolify on a few VPSs, Vercel, an AWS EC2 container, ECS, RDS and Neon. Every VPS renewal turned into a migration weekend. So I built a self-hosted deploy platform called Levelrail, and before I moved my own sites onto it I ran it on two real servers: an Oracle free-tier ARM VM and a cheap VPS that also runs Coolify.&lt;/p&gt;

&lt;p&gt;I expected small bugs. I got four that would have hurt real users. Here they are, in the order I found them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every local backup failed on a fresh install
&lt;/h2&gt;

&lt;p&gt;I created a Postgres database, ran a backup and looked at the history. Every local backup on a default install failed with the same error: the backup folder did not exist.&lt;/p&gt;

&lt;p&gt;On my laptop the folder had existed for months, so I never saw it. On a clean server the platform tried to write into a path nobody had created.&lt;/p&gt;

&lt;p&gt;The fix was small: create the local backup root the first time it is used, and make the location a setting instead of a hardcoded path. The lesson was bigger. &lt;strong&gt;A backup feature that works only on the machine you built it on is not a backup feature.&lt;/strong&gt; I now run every backup and restore on a clean server before I trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The firewall panel showed nothing
&lt;/h2&gt;

&lt;p&gt;I wanted a firewall page that tells you what is actually exposed on a host, not a list of settings. On the VPS it showed an empty table.&lt;/p&gt;

&lt;p&gt;The reason: the host used nftables, and the panel only knew how to read the older iptables rules. It looked fine on every machine I had tested, and showed nothing on the one that mattered.&lt;/p&gt;

&lt;p&gt;Now the panel detects which backend is active, reads it, and says so when it cannot. An empty security panel is worse than no panel, because it looks like good news.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. After a reboot the apps were unreachable for about 30 seconds
&lt;/h2&gt;

&lt;p&gt;Apps are bound to loopback ports and the proxy routes to them. After a reboot, some ports changed, and the proxy kept sending traffic to the old ones until the next refresh.&lt;/p&gt;

&lt;p&gt;On the Oracle ARM VM, Docker brought the containers back about 19 seconds after boot and the control plane answered health checks at 33 seconds. But Docker had handed two apps new loopback ports, and the platform kept the old ones for about 30 seconds until its next reconcile. I added a refresh at boot so routes recover as soon as the containers are back.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The HTTP to HTTPS redirect dropped the port
&lt;/h2&gt;

&lt;p&gt;On a server where the proxy listened on a non-standard port, the redirect sent people to the default HTTPS port and the page failed. Small bug, very confusing for the person on the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the testing taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test on a clean server, not your own. Three of the four bugs only appear where nothing is pre-configured.&lt;/li&gt;
&lt;li&gt;A security or backup panel needs a loud "I cannot tell" state.&lt;/li&gt;
&lt;li&gt;Write the failure into the product: the platform now tells you when a backup has failed instead of waiting for you to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I test now
&lt;/h2&gt;

&lt;p&gt;Before I trust a feature, I run it three ways: on my machine, on a clean cloud VM, and again after a reboot. The reboot is the one people skip. Backups, routes, certificates and firewall state are all things that look fine until the machine restarts and has to rebuild them from nothing. If a feature survives a clean install and a reboot, it has earned a place in the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Control plane memory: about 200 MB RSS right after start (196 MiB on the VPS), and the API was ready in 4.2 seconds from an empty data directory.&lt;/li&gt;
&lt;li&gt;A Next.js 14 app built and deployed in 2 minutes 46 seconds on a 2 vCPU VPS, with a peak of about 514 MB of extra host memory during the build.&lt;/li&gt;
&lt;li&gt;A restart with &lt;code&gt;--wait&lt;/code&gt; takes about 12 seconds. Three redeploys under 10 requests per second served 458 requests with no failed responses and a longest gap of 0.36 seconds.&lt;/li&gt;
&lt;li&gt;Upgrading the platform in place: 2.19 seconds of downtime, a 179 ms database migration, and all 5 apps, 7 databases and 6 domains still there afterwards.&lt;/li&gt;
&lt;li&gt;A new Postgres database was ready in 7.2 seconds, and a restore into a new database took 22 seconds with all 1000 test rows intact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Levelrail is
&lt;/h2&gt;

&lt;p&gt;It is a self-hosted deploy platform. Deploy from Git, a Dockerfile, a compose file or a template. HTTPS and domains, logs, rollbacks, databases with scheduled backups and restore, multiple servers from one control plane, and an audit log. Everything is also available from the CLI, the API and an MCP server. I run my own sites on it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;https://github.com/glincker/levelrail&lt;/a&gt;&lt;br&gt;
Site: &lt;a href="https://levelrail.com" rel="noopener noreferrer"&gt;https://levelrail.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you self-host and have a bug you only found on a clean server, I would like to hear it. What did your first failed backup look like?&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>devops</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I ran Minecraft, WordPress and MySQL on a free 6 GB ARM VM and watched the RAM | Levelrail</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:36:26 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-ran-minecraft-wordpress-and-mysql-on-a-free-6-gb-arm-vm-and-watched-the-ram-36hm</link>
      <guid>https://dev.to/thegdsks/i-ran-minecraft-wordpress-and-mysql-on-a-free-6-gb-arm-vm-and-watched-the-ram-36hm</guid>
      <description>&lt;p&gt;Free tier servers are a great way to find out what your tools really cost. I took an Oracle Cloud ARM VM with 6 GB of RAM, put a self-hosted deploy platform on it, and asked it to run four things at once: a developer toolbox, WordPress, MySQL and a Minecraft server. Then I rebooted it twice and wrote down what happened.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;
        levelrail
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted alternative to Vercel and Coolify. Git push, HTTPS, logs, rollback. No Kubernetes.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/glincker/levelrail/docs/assets/brand/levelrail-default.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Flevelrail%2FHEAD%2Fdocs%2Fassets%2Fbrand%2Flevelrail-default.svg" alt="Levelrail logo" width="56" height="56"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Levelrail&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Self-hosted PaaS: deploy on git push to your own servers, with logs, metrics, HTTPS and rollback.&lt;/strong&gt;&lt;br&gt;
One Go binary, Docker runtime, no Kubernetes. Apache 2.0.&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://github.com/glincker/levelrail/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c3c2050e75627717c5a75f41e71033befdf6dc80b7624c8434e64c5f919d2e67/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f676c696e636b65722f6c6576656c7261696c" alt="License: Apache 2.0"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/levelrail/releases" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/9667d55055fd9d9c1ea62a4edc0ce05cfdae17f8a99f64df238288a25e9f3488/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f676c696e636b65722f6c6576656c7261696c3f696e636c7564655f70726572656c6561736573266c6162656c3d72656c65617365" alt="Latest release"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/levelrail/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/glincker/levelrail/actions/workflows/ci.yml/badge.svg" alt="CI status"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/levelrail/go.mod" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/85c0e2ee73aa61cedfbb7c31131c6a0383daee1daea87f0595fab24ad1c421c0/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f676f2d6d6f642f676f2d76657273696f6e2f676c696e636b65722f6c6576656c7261696c3f7374796c653d666c6174" alt="Go version"&gt;&lt;/a&gt;
&lt;br&gt;
  &lt;a href="https://github.com/glincker/levelrail/docs/mcp.md" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ba6082c73691bccaf564336024813ccc8b95366cc60dc241c8fb155e739e7388/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d43502d7365727665722d3841324245323f7374796c653d666c6174" alt="MCP server"&gt;&lt;/a&gt;
  &lt;a href="https://docs.levelrail.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/83bbe6cf5aa72f59da63597a1954d5b76616ff3c1d57767c2c9c459c02269a32/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d646f63732e6c6576656c7261696c2e636f6d2d626c75653f7374796c653d666c6174" alt="Documentation"&gt;&lt;/a&gt;
  &lt;a href="https://discord.gg/Ar5pcaZB99" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/bd291f58ab0ea785445fd546619eec3007f64ae6e9176dbfbd042ce93ce961d6/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3832393136383839373038303535373537393f7374796c653d666c6174266c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d646973636f726426636f6c6f723d353836354632" alt="Discord"&gt;&lt;/a&gt;
  &lt;a href="https://codespaces.new/glincker/levelrail" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d5502f51d827a2d6e35564672828e99bb078504c4c1644de059284dfb2bfdacc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436f64657370616365732d6f70656e253230646576253230636f6e7461696e65722d3138313731373f7374796c653d666c6174266c6f676f3d676974687562" alt="Open in GitHub Codespaces"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://levelrail.com" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; ·
  &lt;a href="https://docs.levelrail.com" rel="nofollow noopener noreferrer"&gt;Docs&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail#quickstart" rel="noopener noreferrer"&gt;Install&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail#production-ready-with-receipts" rel="noopener noreferrer"&gt;Production ready&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail#levelrail-vs-coolify-dokploy-caprover-and-dokku" rel="noopener noreferrer"&gt;Compare&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail#faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail/docs/roadmap.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail/CHANGELOG.md" rel="noopener noreferrer"&gt;Changelog&lt;/a&gt; ·
  &lt;a href="https://github.com/glincker/levelrail/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;English · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.zh-Hans.md" rel="noopener noreferrer"&gt;简体中文&lt;/a&gt; · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.es.md" rel="noopener noreferrer"&gt;Español&lt;/a&gt; · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.pt-BR.md" rel="noopener noreferrer"&gt;Português (BR)&lt;/a&gt; · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.de.md" rel="noopener noreferrer"&gt;Deutsch&lt;/a&gt; · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.fr.md" rel="noopener noreferrer"&gt;Français&lt;/a&gt; · &lt;a href="https://github.com/glincker/levelrail/docs/i18n/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Flevelrail%2FHEAD%2Fdocs%2Fassets%2Fbrand%2Fsocial%2Fgithub-social-1280x640-light.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Flevelrail%2FHEAD%2Fdocs%2Fassets%2Fbrand%2Fsocial%2Fgithub-social-1280x640-light.jpg" alt="Levelrail: your own Vercel, on your server. The lightweight, self-hosted alternative to Vercel, Heroku and Coolify." width="880"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Levelrail is an open source (Apache 2.0) &lt;strong&gt;self-hosted PaaS&lt;/strong&gt; for developers and small teams who want a Vercel, Heroku or Railway style workflow on servers they own. It runs on any Linux server, from a small VPS to a rack (the one measured host is a 2 vCPU, 8 GB box), and turns plain servers into a private cloud with Git deploys (forge webhooks), HTTPS, databases with verified backups, metrics, logs and alerts, without Kubernetes and without the monitoring stack you would otherwise bolt on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; pre-1.0 (v0.x). Single-server is production ready…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  The cast
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;it-tools&lt;/strong&gt;, a static toolbox app. It used about 2.3 MiB of RAM and answered in 1 ms. Basically a rounding error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress and MySQL 8.4.&lt;/strong&gt; 33 MiB for WordPress, 467 MiB for MySQL. MySQL is the one that always shows up with a suitcase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minecraft&lt;/strong&gt; (Java 21). 2.24 GiB out of a 2.5 GiB limit, 97 percent right after start. No out-of-memory kill. Java will use every byte you hand it and then ask what is for dessert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The install command returned in under a second because the work is asynchronous. All four containers were up, and Minecraft was healthy, about 107 seconds after I submitted the request, image pulls included. Host memory went from 0.7 GB to 3.4 GB and stayed there, with about 2.4 GB still available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching a boot, second by second
&lt;/h2&gt;

&lt;p&gt;After a clean reboot I sampled memory as the machine came back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time after boot&lt;/th&gt;
&lt;th&gt;Used memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;39 s&lt;/td&gt;
&lt;td&gt;1.27 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;3.2 GB (the JVM and MySQL waking up)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;steady&lt;/td&gt;
&lt;td&gt;3.35 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SSH came back 28 seconds after I issued the reboot. Docker started the containers at roughly the 19 second mark, the control plane answered its health check at 33 seconds, and Minecraft was healthy at about 80 seconds. Available memory never dropped below 2.5 GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reboot that fought itself
&lt;/h2&gt;

&lt;p&gt;The first reboot did not go well. The new control plane would not start: &lt;code&gt;Address already in use&lt;/code&gt; on port 80. The old install I had left on the same machine was still enabled, and its sockets grabbed the port first. Two versions of the same platform, fighting over the front door.&lt;/p&gt;

&lt;p&gt;Here is the funny part. While the control plane was not running at all, Docker restarted all four app containers on its own, 9 seconds after boot, because they carry a restart policy. The apps were up and serving before the thing that manages them had started. That is what you want: the apps should not depend on the control plane being alive.&lt;/p&gt;

&lt;p&gt;I disabled the old service and rebooted again for the clean run above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rough edges I found
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;After the reboot Docker assigned new loopback ports to two apps. The platform remembered the old ones for about 30 seconds until its next reconcile, which means a proxy pointed at a stored port would have returned errors for half a minute. I fixed it by refreshing the ports at boot.&lt;/li&gt;
&lt;li&gt;Minecraft logged two scary looking health check errors while it was starting, 35 seconds apart. They fixed themselves, and they made the logs noisy. Noise like that trains you to ignore the real alarm.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A 6 GB free VM is plenty for a small stack. It is not plenty for a stack with a JVM in it unless you set a memory limit. I did.&lt;/li&gt;
&lt;li&gt;Measure the boot, not just the steady state. The peak is at about 60 seconds, and that is when things fall over.&lt;/li&gt;
&lt;li&gt;Restart policies on the apps and a small control plane make reboots boring, which is the goal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform I used is Levelrail, which I build and run my own sites on: &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;https://github.com/glincker/levelrail&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is the most RAM-hungry thing you run at home?&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your logos folder is already out of date</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:36:18 +0000</pubDate>
      <link>https://dev.to/thegdsks/your-logos-folder-is-already-out-of-date-jo8</link>
      <guid>https://dev.to/thegdsks/your-logos-folder-is-already-out-of-date-jo8</guid>
      <description>&lt;p&gt;Open your project and look for a folder named &lt;code&gt;logos&lt;/code&gt;, &lt;code&gt;brands&lt;/code&gt; or &lt;code&gt;partners&lt;/code&gt;. It probably holds a couple of dozen SVGs you downloaded one at a time, with names like &lt;code&gt;github-new.svg&lt;/code&gt;, &lt;code&gt;stripe-2021.svg&lt;/code&gt; and &lt;code&gt;slack_final_v2.svg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That folder was correct on the day you made it. It has been drifting ever since, and nothing in your build will tell you.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. Our template catalog showed a logo for 63 of 442 apps, because someone copied 63 files by hand and nobody wanted to copy the rest. The other 379 tiles showed a gray letter. The marks existed. They just lived somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong with hosted logo files
&lt;/h2&gt;

&lt;p&gt;Copying a file gives you a snapshot. Brands do not stand still, and a snapshot has four ways to go bad.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The brand changes its logo, and your site keeps showing the old one.&lt;/li&gt;
&lt;li&gt;The file was never clean. It has a hard-coded size, no &lt;code&gt;viewBox&lt;/code&gt;, a script tag, or a "mono" version that secretly uses four colors.&lt;/li&gt;
&lt;li&gt;Each file is a one-off, so no two have the same name, the same dark mode story or the same license note.&lt;/li&gt;
&lt;li&gt;Nobody owns it. A designer drops a file in, a developer renames it, and nobody can say where it came from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hosted folder also costs you bytes you cannot see. A sprite sheet of every logo you might need loads on the page that needs only one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: treat logos like a dependency
&lt;/h2&gt;

&lt;p&gt;You already treat React, your date library and your CSS framework this way. Somebody else maintains them, you pin a version, and you upgrade on purpose. Brand logos can work the same way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thesvg.org" rel="noopener noreferrer"&gt;theSVG&lt;/a&gt; is an open catalog of more than 7,600 brand icons with a rule that every icon follows the same path scheme: &lt;code&gt;{slug}/{variant}.svg&lt;/code&gt;. The GitHub logo is always &lt;code&gt;github&lt;/code&gt;. Its dark version is always &lt;code&gt;github/dark.svg&lt;/code&gt;. When the brand changes the mark, the new one lands under the same name.&lt;/p&gt;

&lt;p&gt;That one rule is what makes updates automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven reasons a library beats your own folder
&lt;/h2&gt;

&lt;p&gt;The first reason is the one that matters most. The other six are what keep it that way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;What it means for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same name, new icon&lt;/td&gt;
&lt;td&gt;A rebrand ships in a version bump, with no code change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One naming scheme&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;default&lt;/code&gt;, &lt;code&gt;mono&lt;/code&gt;, &lt;code&gt;light&lt;/code&gt;, &lt;code&gt;dark&lt;/code&gt; and &lt;code&gt;wordmark&lt;/code&gt; everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checked in CI&lt;/td&gt;
&lt;td&gt;No scripts, no raster data, a real &lt;code&gt;viewBox&lt;/code&gt;, mono files that are truly single color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebrands keep history&lt;/td&gt;
&lt;td&gt;The old mark stays as a &lt;code&gt;-legacy&lt;/code&gt; entry, so nothing vanishes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tree-shaking&lt;/td&gt;
&lt;td&gt;Import one icon and ship one icon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License on every entry&lt;/td&gt;
&lt;td&gt;You can see the rules before you ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signed releases&lt;/td&gt;
&lt;td&gt;npm packages publish with provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The checks are not decoration. Last week we added 27 new self-hosted app icons to theSVG, and the validator rejected our first pull request. Six files carried editor metadata, and one "mono" variant was not single color. The bad files never reached a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check how stale your folder is
&lt;/h2&gt;

&lt;p&gt;You can find out in ten minutes. List every file in the folder and ask three questions about each one.&lt;/p&gt;

&lt;p&gt;First, where did it come from? If the answer is "a Google image search," treat it as untrusted. Second, does it open cleanly? Run it through an SVG linter and look for a missing &lt;code&gt;viewBox&lt;/code&gt;, a fixed width and height, or embedded PNG data. Third, is it still the brand's current mark? Open the company's own site next to your copy and compare.&lt;/p&gt;

&lt;p&gt;Most folders fail at least one of those checks. The failures are rarely dramatic. A slightly different blue, a retired gradient, a wordmark with the old typeface. Visitors rarely notice any single one, but together they make a page look less cared for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in five minutes
&lt;/h2&gt;

&lt;p&gt;Pick the line that matches your stack.&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="c"&gt;# React&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @thesvg/react

&lt;span class="c"&gt;# Astro&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @thesvg/astro

&lt;span class="c"&gt;# Plain JavaScript, tree-shakeable&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;thesvg

&lt;span class="c"&gt;# No install, copy one icon into your repo&lt;/span&gt;
npx @thesvg/cli add github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then use an icon the way you use any component. The &lt;code&gt;variant&lt;/code&gt; prop picks the light, dark, mono or wordmark version, so dark mode is one prop instead of a second file.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Github&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@thesvg/react/github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Footer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Github&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"GitHub"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In Astro, the import path is the same idea.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import Github from "@thesvg/astro/github";
---
&amp;lt;Github size={24} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you only need an image tag, the CDN works with no install at all.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://thesvg.org/icons/github/default.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"32"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"32"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"GitHub"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Let your tools do the updating
&lt;/h2&gt;

&lt;p&gt;The real payoff is the part you stop doing. Add the package to Dependabot or Renovate and a new icon version arrives as a normal pull request. Your logos improve while you work on something else.&lt;/p&gt;

&lt;p&gt;We did this on our own product. The dashboard used a hand-kept list that matched each of 442 app templates to a logo file. We replaced it with a small script that reads the template ids, checks which marks exist in the installed package and writes the mapping. Now a version bump of &lt;code&gt;@thesvg/react&lt;/code&gt; is the whole job. Version 4.0.3 added 27 icons, which unlocked 34 more tiles without adding a single file.&lt;/p&gt;
&lt;h2&gt;
  
  
  What about brands that are not there yet
&lt;/h2&gt;

&lt;p&gt;Tell us, or add them yourself. Every icon starts as a pull request with the SVG and a small data entry, and the same checks run on yours. If a project is real, it belongs in the catalog. The library is open source under the MIT license, with no accounts and no API keys.&lt;/p&gt;

&lt;p&gt;You can also ask your AI editor. The &lt;code&gt;@thesvg/mcp-server&lt;/code&gt; package lets your AI editor look up and insert logos for you, so you never have to leave the file you are editing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it on one file
&lt;/h2&gt;

&lt;p&gt;Do not migrate everything today. Pick one component that imports a logo from your &lt;code&gt;logos&lt;/code&gt; folder, swap it for the library version, and delete the file. If you like the result, do the next one.&lt;/p&gt;

&lt;p&gt;If theSVG saves you a trip to a press kit, a star on GitHub is the best way to help other developers find it.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;
        thesvg
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      The open SVG brand library: 7,600+ brand logos and cloud icons (AWS, Azure, GCP) as official SVG. Search, copy, ship via npm, CDN, CLI, React, Vue, Svelte, Figma, VS Code, Raycast and MCP. Free, MIT, no gatekeeping.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;
    
      
      
      &lt;/a&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Flogo-wordmark.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Flogo-wordmark.svg" alt="theSVG" height="48"&gt;&lt;/a&gt;
    
  
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;The open SVG brand library&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;&lt;b&gt;A HOME FOR EVERY BRAND.&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;
  7,600+ brand logos and cloud icons. Search, copy and ship them as SVG, React, Vue, Svelte, CDN, CLI or MCP.&lt;br&gt;
  Free, open source, no gatekeeping
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f7bdbdf8a4c8423dbabcecbb58ac035501dad96f5de8fd38b4c786bf78c6c013/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f7468657376673f7374796c653d666c61742d73717561726526636f6c6f723d433234313043266c6162656c3d6e706d" alt="npm"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d28a32d2289b9a23d4ed7f7d1ce957bc8bd12834f2e67eaca3d03ab483881f74/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f7468657376673f7374796c653d666c61742d73717561726526636f6c6f723d433234313043266c6162656c3d646f776e6c6f616473" alt="downloads"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/@thesvg/mcp-server" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/11cc2d9981ac974be51139ed193980178afa22cb32b0c22cbcb51fb2d60b9166/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f407468657376672f6d63702d7365727665723f7374796c653d666c61742d73717561726526636f6c6f723d433234313043266c6162656c3d4d4350" alt="MCP server"&gt;&lt;/a&gt;
  &lt;a href="https://www.jsdelivr.com/package/gh/glincker/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ddd4c02e5684e7c53c3bfe2e1db8352be8aa65fb046d4ab591a6efa29039151b/68747470733a2f2f646174612e6a7364656c6976722e636f6d2f76312f7061636b6167652f67682f676c696e636b65722f7468657376672f6261646765" alt="jsDelivr"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4f1e2c86fd38a85a4fb90a74ffc1bddf6e4d8aa7a40e63ffd886bb43ba6f115d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f676c696e636b65722f7468657376673f7374796c653d666c61742d737175617265266c6162656c3d7374617273" alt="stars"&gt;&lt;/a&gt;
  &lt;a href="https://discord.gg/Ar5pcaZB99" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5820380e2e128fe530b2c512fb8890c20e27aed11a04a55a12ee4eeb60e1eda0/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3832393136383839373038303535373537393f7374796c653d666c61742d737175617265266c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d646973636f726426636f6c6f723d353836354632" alt="Discord"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/887df0c07b35dbc0fd697edabea80cd0aac69c1d9c6290e0e2699a6f3d930c35/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f69636f6e732d372532433630302532422d4332343130433f7374796c653d666c61742d737175617265" alt="7,600+ icons"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/07929a65aba7429404604f02ee788a9f1351d9a03fef2af7a2cf1ebfcf88f0d7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f676c696e636b65722f7468657376673f7374796c653d666c61742d737175617265" alt="license"&gt;&lt;/a&gt;
  &lt;a href="https://www.figma.com/community/plugin/1612997159050367763" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4c1c8dadd07a324f1517b623e75393965e23553bbbf27df5b4f075cdf73ed3a9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4669676d612d506c7567696e2d4632344531453f7374796c653d666c61742d737175617265266c6f676f3d6669676d61266c6f676f436f6c6f723d7768697465" alt="Figma"&gt;&lt;/a&gt;
  &lt;a href="https://marketplace.visualstudio.com/items?itemName=glincker.thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a1e38bea3a199f1be821313812b7ddcd8b9d09a1e8f809a43fedc60b86696ab7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5653253230436f64652d457874656e73696f6e2d3030374143433f7374796c653d666c61742d737175617265266c6f676f3d76697375616c73747564696f636f6465266c6f676f436f6c6f723d7768697465" alt="VS Code"&gt;&lt;/a&gt;
  &lt;a href="https://www.raycast.com/thegdsks/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/851575e7a28de939a29c99b66d1ce6bd4d4116a7c5d7d776439f2ca8d2c474ea/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f526179636173742d53746f72652d4646363336333f7374796c653d666c61742d737175617265266c6f676f3d72617963617374" alt="Raycast"&gt;&lt;/a&gt;
  &lt;a href="https://icon-sets.iconify.design/thesvg-color/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/03ab9fe4efeeb02bcde37399182046b3df1d4ba87f6574ccdd1559be61b2a53d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f49636f6e6966792d32253230736574732d3032364339433f7374796c653d666c61742d737175617265" alt="Iconify"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/neovim" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/88938f8fb7c9b2530ed80bc1baa277b565e1d74e441915ed8fb5a0ffd7bb2cfc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e656f76696d2d506c7567696e2d3031393733333f7374796c653d666c61742d737175617265266c6f676f3d6e656f76696d266c6f676f436f6c6f723d7768697465" alt="Neovim"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/alfred" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f142a8f2a414349f30c767e1ed8629d1aec636b3158d12f57b95cc9678f87514/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f416c667265642d576f726b666c6f772d3543314638373f7374796c653d666c61742d737175617265266c6f676f3d616c66726564266c6f676f436f6c6f723d7768697465" alt="Alfred"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/browser" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4679826ee1f5e8ae42dffe17d237c19507ea305f888ab62eb7dec66b6fb841a4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4368726f6d652d436f6d696e67253230536f6f6e2d3432383546343f7374796c653d666c61742d737175617265266c6f676f3d676f6f676c656368726f6d65266c6f676f436f6c6f723d7768697465" alt="Chrome"&gt;&lt;/a&gt;
  &lt;a href="https://skills.sh/glincker/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7884786d99c662ebc92d270db565bdcea9b1fa73d9948695ad252f57b2ea5f22/68747470733a2f2f736b696c6c732e73682f622f676c696e636b65722f746865737667" alt="skills.sh"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/homebrew-thesvg" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7b5a3b756568109aaa99f492de96b126a5cb0268c68c643ecc33541769d7780d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f486f6d65627265772d7468657376672d4642423034303f7374796c653d666c61742d737175617265266c6f676f3d686f6d6562726577266c6f676f436f6c6f723d7768697465" alt="Homebrew"&gt;&lt;/a&gt;
  &lt;a href="https://socket.dev/npm/package/@thesvg/icons" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/37d538f78ff0cc346ce5b437a6e9cfb84fe33951c345f9a7673b91a56ba4ae38/68747470733a2f2f736f636b65742e6465762f6170692f62616467652f6e706d2f7061636b6167652f407468657376672f69636f6e73" alt="Socket score for @thesvg/icons"&gt;&lt;/a&gt;
  &lt;a href="https://socket.dev/npm/package/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/037d154575444f7a2311ce767a520272211f87f032c5ad4d96e9157501eb9ae1/68747470733a2f2f736f636b65742e6465762f6170692f62616467652f6e706d2f7061636b6167652f746865737667" alt="Socket score for thesvg"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/@thesvg/icons" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b860f65740a337d014f8d3b09ac775d44119d4a2a9bce8b31731c0fb0ca5dcec/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f70726f76656e616e63652d76657269666965642d3245413434463f7374796c653d666c61742d737175617265" alt="npm provenance verified"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Browse icons&lt;/strong&gt;&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#install" rel="noopener noreferrer"&gt;Install&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#extensions" rel="noopener noreferrer"&gt;Extensions&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#cdn" rel="noopener noreferrer"&gt;CDN&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#api" rel="noopener noreferrer"&gt;API&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#packages" rel="noopener noreferrer"&gt;Packages&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://thesvg.org/compare" rel="nofollow noopener noreferrer"&gt;Compare&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#community" rel="noopener noreferrer"&gt;Community&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#contributing" rel="noopener noreferrer"&gt;Contribute&lt;/a&gt; &amp;nbsp;•&amp;nbsp;
  &lt;a href="https://github.com/glincker/thesvg#contributors" rel="noopener noreferrer"&gt;Contributors&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;
    
      
      
      &lt;/a&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Fog-image-light.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Fog-image-light.png" alt="theSVG, the open SVG brand library" width="720"&gt;&lt;/a&gt;
    
  
&lt;/p&gt;

&lt;p&gt;
  Every logo below is served straight from theSVG.
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://thesvg.org/icon/google" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5d3513f39fdfa581a404e7e485ea5a7eada7ff39b7ad7a5f2813f4e151d262ce/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f676f6f676c652f64656661756c742e737667" height="34" alt="Google"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/docker" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/41c6adbdf0c9d4161e57c8787a4c3c6c3f61cae4a4792a6c11cea37c91ddd211/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f646f636b65722f64656661756c742e737667" height="34" alt="Docker"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/kubernetes" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0d2f52f8a339ec61058ed13034a0f3535632c55e8cf85f881292e5e272eb946c/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f6b756265726e657465732f64656661756c742e737667" height="34" alt="Kubernetes"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/react" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1ba3539efc964bec32bd0216f5a446276fdb6b5c6df86ea29733c23120be5474/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f72656163742f64656661756c742e737667" height="34" alt="React"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/vue" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/666b937682efdda3cba45745eb6e75d500b44ec93e160d40410b9a58b9fb1f40/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f7675652f64656661756c742e737667" height="34" alt="Vue"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/svelte" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/de3fe5ff96bb6f39041bd63a4453050c5fb36da14921375665e0d8ca7402ec82/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f7376656c74652f64656661756c742e737667" height="34" alt="Svelte"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/typescript" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/12dd4a02a286b655c4a3dbbf092a0ada9d872745e6faaf9051a1ffb43b4ff0f2/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f747970657363726970742f64656661756c742e737667" height="34" alt="TypeScript"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/tailwindcss" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a6795ee3664620570b9896d742952d37ec98bd28373e86f54023de5c9504bb7c/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f7461696c77696e646373732f64656661756c742e737667" height="34" alt="Tailwind CSS"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/figma" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d6011702e22c990513b836b6fe9f9065d3d23d4d60c3f0f9f96d62d094e8b5a1/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f6669676d612f64656661756c742e737667" height="34" alt="Figma"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/stripe" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4ff5d4e01b815fc29c97de2cc2430167422989cf8c724d17e80c294efadc5413/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f7374726970652f64656661756c742e737667" height="34" alt="Stripe"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://thesvg.org/icon/slack" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/afcc60c4f5d173bbd896539edfcaee7bebf4c11dbe0fbc9065c924ced4a6951b/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f736c61636b2f64656661756c742e737667" height="34" alt="Slack"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/discord" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5ccb737632285354416649ae9b3b3570093a3a8a3ccd55a028b5b1153e47f6a9/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f646973636f72642f64656661756c742e737667" height="34" alt="Discord"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/spotify" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ce30946bf493c6a7dd34c174a6fe96098f49c2fd15dc267ee847d5a38f2ece02/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f73706f746966792f64656661756c742e737667" height="34" alt="Spotify"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/supabase" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e1d0f6e6ebdf181f5a4b68b66998a009bcadb342a36036b115336018e747ffbb/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f73757061626173652f64656661756c742e737667" height="34" alt="Supabase"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/postgresql" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c6238c6f39628c44b32fc85bbb8fa6638c2176fb9e6784c2e720a72d63269cca/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f706f737467726573716c2f64656661756c742e737667" height="34" alt="PostgreSQL"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/mongodb" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c9b4e87dd8a66096f0b5d160a18164e23bbebe1438cd8c325dfd8ed572a0c94e/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f6d6f6e676f64622f64656661756c742e737667" height="34" alt="MongoDB"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/cloudflare" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2ce85696b772e9b0d864115c2fa290eff801ff43fc6d33c37560c4fc90bdc227/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f636c6f7564666c6172652f64656661756c742e737667" height="34" alt="Cloudflare"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/terraform" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/19b2673d7718da5e66ff50f56c08fc849ae6a6fd20765dc31ed22ef48606a07b/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f7465727261666f726d2f64656661756c742e737667" height="34" alt="Terraform"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/aws" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e93e9116c8538250c45e81ed761d390226470836bff52ec55d83f40406e56365/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f616d617a6f6e2d62616467652f64656661756c742e737667" height="34" alt="AWS"&gt;&lt;/a&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;a href="https://thesvg.org/icon/microsoft-azure" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/67753bb4fb5189dd36e56c2cbaef9fb84c7eddacac68e5de097a8d45960b64e2/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f676c696e636b65722f746865737667406d61696e2f7075626c69632f69636f6e732f6d6963726f736f66742d617a7572652f64656661756c742e737667" height="34" alt="Azure"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why theSVG&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Brand logos are scattered across press kits, Figma files, and random GitHub repos. &lt;strong&gt;theSVG&lt;/strong&gt; is the single source for SVG icons - brand logos, cloud architecture diagrams, and a set of interface icons of our own. Searchable, versioned, and available as npm packages, CDN, CLI, API, and MCP server.&lt;/p&gt;

&lt;p&gt;Every developer has hunted for a company logo: a press kit buried three clicks deep, a PNG that blurs on retina, a "mono" file that is secretly four colors, a logo that changed…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Which logo is the most out of date in your project right now? Tell me in the comments, and I will check whether theSVG already has the new version.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Zero Downtime Deploy for Docker Apps, With Rollback</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:11:48 +0000</pubDate>
      <link>https://dev.to/thegdsks/zero-downtime-deploy-for-docker-apps-with-rollback-1909</link>
      <guid>https://dev.to/thegdsks/zero-downtime-deploy-for-docker-apps-with-rollback-1909</guid>
      <description>&lt;p&gt;After this guide you know exactly what stands between a deploy trigger and live traffic on &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;Levelrail&lt;/a&gt;, a self-hosted deploy platform, and you can roll back a bad release in one command. "Zero downtime" holds only if three smaller things hold. Something checks the new version before it gets traffic. The old version is still around if you change your mind. And the platform can tell whether the thing running is the thing you shipped.&lt;/p&gt;

&lt;p&gt;For a hands-on run with measured request counts, see &lt;a href="https://levelrail.com/blog/restart-docker-app-without-downtime" rel="noopener noreferrer"&gt;restart a Docker container without downtime&lt;/a&gt; and &lt;a href="https://levelrail.com/blog/rollback-failed-deploy-docker" rel="noopener noreferrer"&gt;roll back a failed Docker deploy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Checked on 2026-11-01 against the current Levelrail release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero downtime deploy guarantees at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;What it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Readiness gate before cutover&lt;/td&gt;
&lt;td&gt;Traffic hitting a container that is not ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable window&lt;/td&gt;
&lt;td&gt;A container that starts, then crashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digest-truthful deploys&lt;/td&gt;
&lt;td&gt;A redeploy of the same tag silently keeping old content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale-deploy guard&lt;/td&gt;
&lt;td&gt;An older build overwriting a newer one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous release held after cutover&lt;/td&gt;
&lt;td&gt;A slow, cold-start rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-deploy watch&lt;/td&gt;
&lt;td&gt;A bad release that passes readiness, then fails under load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freeze windows&lt;/td&gt;
&lt;td&gt;Friday-afternoon deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The deploy sequence for a Docker app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start new container
      |
readiness probe passes N times in a row, container stays up for the stable window
      |  fail: old container keeps serving, deploy fails with a reason
      |
cut traffic to the new release
      |
drain the old release (at least 15s by default)
      |
hold the previous release for instant rollback
      |
watch the new release for 5 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new container takes traffic only when its readiness probe passed twice in a row (&lt;code&gt;APP_DEPLOY_SUCCESS_THRESHOLD&lt;/code&gt;) and it ran for 10 seconds (&lt;code&gt;APP_DEPLOY_STABLE_WINDOW&lt;/code&gt;) without exiting, restarting or being OOM-killed. If the gate fails, the old release keeps serving and the deploy records a reason such as &lt;code&gt;ReadinessFailed&lt;/code&gt;, &lt;code&gt;ExitedDuringReadiness&lt;/code&gt; or &lt;code&gt;OOMKilledDuringStableWindow&lt;/code&gt;. The &lt;code&gt;recreate&lt;/code&gt; strategy is the exception, since it stops the old release first.&lt;/p&gt;

&lt;p&gt;Set the probe in your app spec and keep the route cheap:&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;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;readiness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/healthz&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5s&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2s&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A readiness check that calls three outside services fails for reasons your release did not cause. For why a passing check can still hide a dead app, read &lt;a href="https://levelrail.com/blog/health-check-passes-dead-container" rel="noopener noreferrer"&gt;health checks that pass on a dead container&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To watch a deploy go through its gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli deploy status web &lt;span class="nt"&gt;--watch&lt;/span&gt;
levelrail-cli apps deploys gate web dep_abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why a floating tag can lie
&lt;/h2&gt;

&lt;p&gt;Deploy &lt;code&gt;nginx:latest&lt;/code&gt; twice and the tag points at different content over time. A platform that compares tags can report success while the old image keeps running. Levelrail resolves the tag to its content digest at deploy time and pins the desired image, like &lt;code&gt;nginx:latest@sha256:3f1c...&lt;/code&gt;. New content gets a new container and goes through the normal cutover. Levelrail tracks builds by the local image ID, so rebuilding the same commit on a newer base image replaces the container.&lt;/p&gt;

&lt;p&gt;The controller then checks that the running image ID matches the pinned reference. On a mismatch the app reports &lt;code&gt;Ready: False&lt;/code&gt; with &lt;code&gt;ImageDigestMismatch&lt;/code&gt;, and the previous release keeps serving.&lt;/p&gt;

&lt;p&gt;To refuse a cached fallback when the registry is down, deploy with &lt;code&gt;--pull&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli apps deploy web &lt;span class="nt"&gt;--pull&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Old builds should not win
&lt;/h2&gt;

&lt;p&gt;Webhooks arrive out of order. Every automatic deploy gets a per-app sequence number and the commit ordering from your git provider. A deploy triggered earlier than one already applied becomes &lt;code&gt;superseded&lt;/code&gt;, and so does a push whose commit predates the deployed one. Manual deploys and explicit rollbacks are never rejected, and they advance the sequence, so an older build in flight cannot undo your rollback. Deploys of one app run one at a time, and queued deploys survive a control plane restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll back a Docker deploy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli apps deploys list web
levelrail-cli apps deploys rollback-to web dep_abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;History rows store the pinned reference, so a rollback deploys exactly the content that row ran, even if the tag moved. It also restores that deploy's literal env values, port, health checks, replicas and resources. Secret and database references keep their current values. The rollback is an ordinary deploy and passes the same readiness gate. The dashboard has a Roll back to this action on each history row.&lt;/p&gt;

&lt;p&gt;After a successful cutover, the previous release stays running. The hold is the longest of &lt;code&gt;APP_DEPLOY_PREVIOUS_RELEASE_HOLD&lt;/code&gt; (3 minutes), the drain timeout and the post-deploy watch window (5 minutes), so with defaults it lasts about 5 minutes. Rolling back inside that window needs no pull and no cold start. Levelrail keeps one previous release, so five deploys in a minute leave the current release plus one.&lt;/p&gt;

&lt;p&gt;The API answers 410 when the old local image no longer exists, and 409 when its tag now points at other content. Levelrail keeps the newest three rollback pins per app (&lt;code&gt;APP_DEPLOY_ROLLBACK_PINS&lt;/code&gt;), and image cleanup never removes a pinned image. Raise the value for a longer safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic rollback after cutover
&lt;/h2&gt;

&lt;p&gt;For five minutes after cutover (&lt;code&gt;APP_DEPLOY_WATCH_WINDOW&lt;/code&gt;), Levelrail watches the release. It trips on an OOM kill, on restarts reaching the limit (default 2), or on a 5xx share of 20% after 20 requests. The mode is &lt;code&gt;notify&lt;/code&gt; by default, which alerts. Set &lt;code&gt;auto&lt;/code&gt; to roll back to the last good release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli apps watch &lt;span class="nb"&gt;set &lt;/span&gt;web &lt;span class="nt"&gt;--mode&lt;/span&gt; auto &lt;span class="nt"&gt;--watch-window&lt;/span&gt; 10m
levelrail-cli apps watch get web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A latency regression is a soft signal that only alerts unless you opt in. A release that is itself an automatic rollback is never rolled back again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze windows
&lt;/h2&gt;

&lt;p&gt;A freeze window starts at every match of a cron expression in a timezone and lasts a set duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli apps freeze &lt;span class="nb"&gt;set &lt;/span&gt;web &lt;span class="nt"&gt;--cron&lt;/span&gt; &lt;span class="s2"&gt;"0 17 * * 5"&lt;/span&gt; &lt;span class="nt"&gt;--duration&lt;/span&gt; 64h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt; Europe/Berlin &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"weekend freeze"&lt;/span&gt;
levelrail-cli apps deploy web &lt;span class="nt"&gt;--image&lt;/span&gt; app:hotfix &lt;span class="nt"&gt;--override-freeze&lt;/span&gt; &lt;span class="nt"&gt;--override-reason&lt;/span&gt; &lt;span class="s2"&gt;"CVE fix"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Levelrail holds push webhooks and replays them when the window ends, and only the newest replays. Manual deploys get a 423 unless they carry an override and a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can go wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause and fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy fails, old release still serves&lt;/td&gt;
&lt;td&gt;The readiness gate failed. Read &lt;code&gt;apps deploys show web&lt;/code&gt; for the reason and the last 200 log lines.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback returns 410&lt;/td&gt;
&lt;td&gt;Image cleanup removed the old image. Redeploy from source and raise &lt;code&gt;APP_DEPLOY_ROLLBACK_PINS&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback returns 409&lt;/td&gt;
&lt;td&gt;The tag moved or the row has no digest. Redeploy a known image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short outage on a deploy&lt;/td&gt;
&lt;td&gt;A pinned host port or a single replica with a writable volume cannot run two releases. Levelrail stops the old one first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy shows Held (frozen)&lt;/td&gt;
&lt;td&gt;A freeze window is active. Wait, or override with a reason.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I have not verified yet
&lt;/h2&gt;

&lt;p&gt;The project's live test ran a good release, a crashing release and a 5xx release against a real ingress on Docker Desktop: 2,617 requests, none failed outside the bad release's own 500s. I have not repeated that on a production VPS. On remote nodes, the running image ID, rollback pins and restart counts do not yet travel over the agent transport, so mismatch detection and some gate checks apply to the control plane's own node.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How long is the previous release kept?
&lt;/h3&gt;

&lt;p&gt;About 5 minutes with defaults. The longest of the hold, the drain timeout and the watch window wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a rollback pull the image again?
&lt;/h3&gt;

&lt;p&gt;Not inside the hold window, and not when the pinned image is still local.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my app drops requests on shutdown?
&lt;/h3&gt;

&lt;p&gt;The old release drains, but it must handle SIGTERM. See &lt;a href="https://levelrail.com/blog/node-sigterm-graceful-shutdown-docker" rel="noopener noreferrer"&gt;graceful shutdown for Node apps in Docker&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where next?
&lt;/h3&gt;

&lt;p&gt;Read the design behind this in &lt;a href="https://levelrail.com/blog/docker-orchestration-without-kubernetes" rel="noopener noreferrer"&gt;Docker orchestration without Kubernetes&lt;/a&gt;. Reference: &lt;a href="https://docs.levelrail.com/deploy-safety" rel="noopener noreferrer"&gt;Deploy safety&lt;/a&gt;, &lt;a href="https://docs.levelrail.com/deploy-failures" rel="noopener noreferrer"&gt;Deploy failures&lt;/a&gt; and &lt;a href="https://docs.levelrail.com/deploying-apps" rel="noopener noreferrer"&gt;Deploying and managing apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Levelrail is open source under Apache 2.0. If this saved you time, a star on &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;the repo&lt;/a&gt; helps other self-hosters find it.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Protecting an MCP server in Go with OAuth 2.1</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sun, 01 Nov 2026 08:24:00 +0000</pubDate>
      <link>https://dev.to/thegdsks/protecting-an-mcp-server-in-go-with-oauth-21-1dhh</link>
      <guid>https://dev.to/thegdsks/protecting-an-mcp-server-in-go-with-oauth-21-1dhh</guid>
      <description>&lt;h1&gt;
  
  
  Protecting an MCP server in Go with OAuth 2.1
&lt;/h1&gt;

&lt;p&gt;By the end of this post you will know how to put OAuth in front of an MCP server written in Go. You will publish the metadata clients need, reject calls without a valid token, and allow each tool only for the scopes you choose.&lt;/p&gt;

&lt;p&gt;Disclosure: I maintain theauth-go, the library used here. The project is about four months old and has 6 GitHub stars, and I point out where that matters. Everything below comes from its repo docs and code. For the normative text, read the &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization spec&lt;/a&gt; (I cite the 2025-11-25 revision) and the RFCs linked below.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;glincker/theauth-go&lt;/a&gt; (MIT licensed Go library). Docs: &lt;a href="https://docs.theauth.dev/go" rel="noopener noreferrer"&gt;docs.theauth.dev/go&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My question is narrow. You wrote an MCP server in Go. A client, perhaps an AI agent acting for a person, connects and asks to run a tool. How does your server know who is asking, and whether this call may run?&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR: the flow in five steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The client calls your MCP server with no token and gets a 401.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;WWW-Authenticate&lt;/code&gt; header in that 401 carries a &lt;code&gt;resource_metadata&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;The client reads your protected resource metadata (RFC 9728) and learns which authorization server to use.&lt;/li&gt;
&lt;li&gt;The client gets an access token from that server (authorization code with PKCE S256).&lt;/li&gt;
&lt;li&gt;The client retries with &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. Your server checks the token, then checks the scope for the tool you are running.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How does MCP authorization work?
&lt;/h2&gt;

&lt;p&gt;A normal API has a known login page. An MCP client does not know your server in advance. It might be a desktop app connecting to a URL a user pasted in. Your server has to say, in a machine readable way, "here is who can give you a token for me."&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt; protected resource metadata is for. Your MCP server publishes a small document at a well-known path. In theauth-go's docs, a resource identifier of &lt;code&gt;mcp.example.com/api/mcp&lt;/code&gt; (served over HTTPS) gets this URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp.example.com/.well-known/oauth-protected-resource/api/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note where &lt;code&gt;/.well-known/&lt;/code&gt; goes: between the host and the path, not at the end. A resource with no path uses the bare &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt;. This is the detail that bites people who build the URL by appending.&lt;/p&gt;

&lt;p&gt;The authorization server has its own document under &lt;a href="https://www.rfc-editor.org/rfc/rfc8414" rel="noopener noreferrer"&gt;RFC 8414&lt;/a&gt;. For an issuer of &lt;code&gt;auth.example.com/tenant1&lt;/code&gt;, clients fetch &lt;code&gt;/.well-known/oauth-authorization-server/tenant1&lt;/code&gt;. theauth-go also serves &lt;code&gt;/.well-known/openid-configuration/tenant1&lt;/code&gt; with the same document.&lt;/p&gt;

&lt;p&gt;Here is the whole exchange:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP client                    MCP server (resource)        Authorization server
    | POST /api/mcp  (no token)       |                              |
    |--------------------------------&amp;gt;|                              |
    |  401 WWW-Authenticate: Bearer   |                              |
    |  resource_metadata=".../.well-known/oauth-protected-resource/api/mcp"
    |&amp;lt;--------------------------------|                              |
    | GET resource metadata           |                              |
    |--------------------------------&amp;gt;|                              |
    | GET authorization server metadata (RFC 8414) -----------------&amp;gt;|
    | authorization code + PKCE (S256), ask for scopes -------------&amp;gt;|
    |&amp;lt;----------------------------------------------- access token  |
    | POST /api/mcp  Authorization: Bearer &amp;lt;token&amp;gt;                   |
    |--------------------------------&amp;gt;| check token and scope        |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first step is the one to remember. The 401 points the client at your metadata, so a client that knows nothing about you can find everything else. In theauth-go's &lt;code&gt;mcpresource&lt;/code&gt; module every challenge includes that URL. The host in it comes from the incoming request, so it stays correct behind a proxy that sets the standard forwarding headers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which authorization server can issue the tokens?
&lt;/h2&gt;

&lt;p&gt;Any compliant one. If you use theauth-go, the same library can host it. Its authorization server speaks OAuth 2.1 with PKCE S256, and also supports DPoP (RFC 9449), token exchange (RFC 8693) and the device grant (RFC 8628). You need none of those for a first deployment.&lt;/p&gt;

&lt;p&gt;The snippet below comes from the README quick start. I left out imports and the agent parts, and &lt;code&gt;encryptionKey&lt;/code&gt; is a 32 byte &lt;code&gt;[]byte&lt;/code&gt; you load yourself. It declares your MCP server as a protected resource with its scopes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;BaseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AS_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;EncryptionKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;encryptionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// 32 bytes, load from your secrets manager&lt;/span&gt;
    &lt;span class="n"&gt;AuthorizationServer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthorizationServerConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Issuer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AS_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProtectedResource&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Identifier&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_RESOURCE"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Scopes&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use a persistent storage backend (the README lists SQLite, Postgres and MySQL) instead of memory for anything beyond a demo. The resource identifier and the scopes are the contract between the two sides.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do I verify tokens in a Go MCP server?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pkg.go.dev/github.com/glincker/theauth-go/mcpresource" rel="noopener noreferrer"&gt;&lt;code&gt;mcpresource&lt;/code&gt;&lt;/a&gt; is a separate Go module with zero dependencies outside the standard library, so a resource server does not import the whole library. The snippet is the repo's &lt;code&gt;examples/mcp-server&lt;/code&gt;, abridged. I dropped imports and &lt;code&gt;main&lt;/code&gt;, and replaced the example's small &lt;code&gt;envOr&lt;/code&gt; helper with plain &lt;code&gt;os.Getenv&lt;/code&gt; calls.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcpresource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_RESOURCE"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;mcpresource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithJWKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_JWKS_URI"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;mcpresource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithIntrospection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_INTROSPECT_URI"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCP_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Diagnostics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[mcpresource %s] %s (code: %s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&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;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Principal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"no principal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"subject"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"actor"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"actor_chain"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActorChain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three details matter here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcpresource.New&lt;/code&gt; takes your resource URI first. The options say how to verify tokens: a JWKS URL for signed tokens, and an introspection endpoint with the resource's client credentials.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Diagnostics()&lt;/code&gt; reports configuration problems at startup. Log them there rather than discover them in production.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;v.Middleware&lt;/code&gt; is the line that makes the router protected. A missing or bad token gets a 401 with the metadata pointer.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Principal&lt;/code&gt; has &lt;code&gt;Subject&lt;/code&gt; (the human), &lt;code&gt;Actor&lt;/code&gt; (who is calling) and &lt;code&gt;ActorChain&lt;/code&gt;. The package docs say &lt;code&gt;ActorChain&lt;/code&gt; lists the agents between the human and the caller, so log both. A tool call from an agent is never just "user 42". It is "agent X acting for user 42".&lt;/p&gt;
&lt;h2&gt;
  
  
  How do I protect an MCP tool with scopes in Go?
&lt;/h2&gt;

&lt;p&gt;Authentication answers who. Scopes answer what. The &lt;code&gt;mcpresource&lt;/code&gt; source documents this guard, mounted after the middleware:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequireScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"files:write"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tools/save"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A principal missing the scope gets a 403. The response carries &lt;code&gt;WWW-Authenticate: Bearer error="insufficient_scope"&lt;/code&gt;, a &lt;code&gt;scope&lt;/code&gt; parameter listing what is required, and the &lt;code&gt;resource_metadata&lt;/code&gt; URL again. Because the response names the scopes, a client can ask for exactly those on re-authorization. That suits an agent that should request more access only when a call needs it.&lt;/p&gt;

&lt;p&gt;If you prefer one table for all tools, the package example checks it by hand. I abridged it to the handler body, after reading the principal &lt;code&gt;p&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;toolScope&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"logs.read"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"logs:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"deploy.create"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"deploy:create"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;toolScope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tool not permitted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusForbidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;My rule either way: an unknown tool gets denied. Default to no. The &lt;a href="https://docs.theauth.dev/go/guides/protect-mcp-tools" rel="noopener noreferrer"&gt;Protect MCP tools guide&lt;/a&gt; covers this in more depth.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;p&gt;The first limit is age. theauth-go's first commit was in June 2026, and &lt;code&gt;mcpresource&lt;/code&gt; ships in the same repo. Six stars means little outside use. Run it against your own client before you trust it, and pin the version.&lt;/p&gt;

&lt;p&gt;The second is the scope of my claim. I have not run a conformance test of every MCP client against it, so I will not say every client works. I can only point to the RFC behaviors the code and docs describe.&lt;/p&gt;

&lt;p&gt;Third, you can do this without my library. The pieces are standard: serve the metadata document, return the right challenge, and check bearer tokens with a JWT library and your authorization server's keys. If you already run an authorization server such as Hydra or Dex, you may only need the resource side, written by hand or with a small module like mcpresource. Wanting less code to own is a reason to use a library, not a requirement.&lt;/p&gt;

&lt;p&gt;Fourth, the example checks tokens but does not decide your policy. Which scopes exist, which agent may get which, and when a human must approve are design decisions the library cannot make for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;MCP authorization comes down to three moves. Publish protected resource metadata so clients can find your authorization server. Check the bearer token on every request. Check a scope per tool, and answer 403 with the scopes you need when it is missing. The &lt;code&gt;mcpresource&lt;/code&gt; module covers the second and third with one middleware line and one guard per tool.&lt;/p&gt;

&lt;p&gt;If you try it, start with &lt;code&gt;examples/mcp-server&lt;/code&gt; in the repo, point it at a throwaway authorization server, and tell me what broke. What would you want a resource server library to check that this one does not?&lt;/p&gt;

&lt;p&gt;If you want to read the code or try it, the repo is here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;
        theauth-go
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted Go authentication library. Embeds in net/http, chi, Gin, Echo or Fiber. OAuth 2.1 authorization server, MCP authorization (RFC 9728), passkeys, SAML, SCIM, TOTP, magic links, DPoP. SQLite, Postgres or MySQL. MIT, no hosted service.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;theauth-go&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;OAuth 2.1 and sign-in for Go MCP servers and agents. It mounts into your own &lt;code&gt;net/http&lt;/code&gt; server, your users and tokens stay in your own database, and there is no hosted service. MIT licensed.&lt;/p&gt;

&lt;p&gt;Building an MCP server in Go means implementing OAuth 2.1, protected resource metadata (RFC 9728), PKCE and token validation yourself. This library does that part, so your server accepts tokens from MCP clients and you keep control of identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked against a real client.&lt;/strong&gt; The official &lt;code&gt;modelcontextprotocol/go-sdk&lt;/code&gt; client (v1.8.0) completes the full flow against it in &lt;a href="https://github.com/glincker/theauth-go/integration/mcp-interop" rel="noopener noreferrer"&gt;&lt;code&gt;integration/mcp-interop&lt;/code&gt;&lt;/a&gt;: discovery, dynamic client registration, PKCE S256, token exchange and an authenticated tool call. Login and consent are simulated in that test, so it proves the protocol path, not a production deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it is for:&lt;/strong&gt; Go developers building MCP servers, agent backends or internal tools who want to own their auth instead of renting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · building &lt;a href="https://glincker.com" rel="noopener noreferrer"&gt;Glincker&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An MCP server that cannot say who is calling and what they may do is just an open port with a nice name.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>mcp</category>
      <category>oauth</category>
      <category>authentication</category>
    </item>
    <item>
      <title>I built a self-hosted AI list that checks itself weekly</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sat, 31 Oct 2026 06:35:15 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-built-a-self-hosted-ai-list-that-checks-itself-weekly-3m0a</link>
      <guid>https://dev.to/thegdsks/i-built-a-self-hosted-ai-list-that-checks-itself-weekly-3m0a</guid>
      <description>&lt;p&gt;Self-hosted AI moves fast. A list that was right in March has dead links by June, and nobody notices until they try to install something.&lt;/p&gt;

&lt;p&gt;So I built a list that checks itself. awesome-selfhosted-ai has 166 open-source projects you can run on your own hardware: local LLM runtimes, chat UIs, agents, MCP servers, RAG and vector databases, speech, image generation, document OCR, voice agents and more.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/awesome-selfhosted-ai" rel="noopener noreferrer"&gt;
        awesome-selfhosted-ai
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Curated list of self-hosted open-source AI: local LLMs, chat UIs, agents, MCP servers, RAG, speech and image generation. Health-checked weekly, dead projects removed automatically.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Awesome Self-Hosted AI &lt;a href="https://awesome.re" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/9d49598b873146ec650fb3f275e8a532c765dabb1f61d5afa25be41e79891aa7/68747470733a2f2f617765736f6d652e72652f62616467652e737667" alt="Awesome"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Open-source AI you can run on your own hardware or server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;303 projects across local LLMs, chat interfaces, agents, MCP servers, RAG, speech and image generation. A weekly job checks every project against the GitHub API, and anything archived, deleted or silent for over a year moves to &lt;a href="https://github.com/glincker/awesome-selfhosted-ai/UNMAINTAINED.md" rel="noopener noreferrer"&gt;UNMAINTAINED.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;303&lt;/strong&gt; projects | &lt;strong&gt;302&lt;/strong&gt; active in the last 90 days | &lt;strong&gt;10.5M&lt;/strong&gt; combined stars | &lt;strong&gt;0&lt;/strong&gt; unmaintained | refreshed 2026-10-31&lt;/p&gt;
&lt;p&gt;See the &lt;a href="https://github.com/glincker/awesome-selfhosted-ai/LIVE.md" rel="noopener noreferrer"&gt;live health table&lt;/a&gt; for stars, last push and license per project.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#agents-and-automation" rel="noopener noreferrer"&gt;Agents and Automation&lt;/a&gt; (27)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#chat-interfaces" rel="noopener noreferrer"&gt;Chat Interfaces&lt;/a&gt; (11)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#coding-assistants" rel="noopener noreferrer"&gt;Coding Assistants&lt;/a&gt; (22)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#document-ai-and-ocr" rel="noopener noreferrer"&gt;Document AI and OCR&lt;/a&gt; (10)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#finetuning-and-training" rel="noopener noreferrer"&gt;Fine-Tuning and Training&lt;/a&gt; (10)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#home-and-personal-ai" rel="noopener noreferrer"&gt;Home and Personal AI&lt;/a&gt; (13)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#image-and-video-generation" rel="noopener noreferrer"&gt;Image and Video Generation&lt;/a&gt; (12)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#llm-runtimes-and-inference" rel="noopener noreferrer"&gt;LLM Runtimes and Inference&lt;/a&gt; (37)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#mcp-servers-and-gateways" rel="noopener noreferrer"&gt;MCP Servers and Gateways&lt;/a&gt; (19)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#model-gateways-and-routers" rel="noopener noreferrer"&gt;Model Gateways and Routers&lt;/a&gt; (11)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#observability-and-evals" rel="noopener noreferrer"&gt;Observability and Evals&lt;/a&gt; (20)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#rag-and-vector-databases" rel="noopener noreferrer"&gt;RAG and Vector Databases&lt;/a&gt; (24)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#search-and-knowledge" rel="noopener noreferrer"&gt;Search and Knowledge&lt;/a&gt; (13)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#speech-and-audio" rel="noopener noreferrer"&gt;Speech and Audio&lt;/a&gt; (15)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/glincker/awesome-selfhosted-ai#vision-and-multimodal" rel="noopener noreferrer"&gt;Vision and&lt;/a&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/awesome-selfhosted-ai" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;How it stays current&lt;/p&gt;

&lt;p&gt;A weekly GitHub Action reads every project's repo through the GitHub API and records stars, last push, license and status. If a project is archived, deleted, or has had no push for a year, it moves to a separate Unmaintained file on its own. If it comes back to life, it comes back to the list. Nothing depends on me remembering to prune.&lt;/p&gt;

&lt;p&gt;What is in it&lt;/p&gt;

&lt;p&gt;16 categories, from LLM runtimes to voice agents and home AI.&lt;br&gt;
A live table with stars, last push, license and a colored status badge per project.&lt;br&gt;
A source-available section for popular tools whose license adds terms beyond a standard open-source one (n8n, Open WebUI, Dify and similar). The license is shown, so you can check it before commercial use.&lt;br&gt;
Rising projects that are under a year old, which older lists tend to miss.&lt;br&gt;
Rules I kept&lt;/p&gt;

&lt;p&gt;Open source (or clearly labeled source-available), self-hostable, not archived, pushed to within 90 days, at least 500 stars. One project per pull request, and the README is generated from one data file, so a pull request is a small JSON change that CI validates.&lt;/p&gt;

&lt;p&gt;Honest note&lt;/p&gt;

&lt;p&gt;I used AI tools to help find candidates and draft descriptions. A script verifies every repo against the GitHub API, and I review the list. If you spot a wrong license or a dead project, open an issue and it gets fixed.&lt;/p&gt;

&lt;p&gt;Link: &lt;a href="https://github.com/glincker/awesome-selfhosted-ai" rel="noopener noreferrer"&gt;https://github.com/glincker/awesome-selfhosted-ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is missing? Tell me which project should be in it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>selfhosted</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Five Go auth libraries I would look at in 2026</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:22:06 +0000</pubDate>
      <link>https://dev.to/thegdsks/five-go-auth-libraries-i-would-look-at-in-2026-1g12</link>
      <guid>https://dev.to/thegdsks/five-go-auth-libraries-i-would-look-at-in-2026-1g12</guid>
      <description>&lt;h1&gt;
  
  
  Five Go auth libraries I would look at in 2026
&lt;/h1&gt;

&lt;p&gt;Disclosure first: I maintain theauth-go, one of the five Go auth libraries below. The project is about four months old and has 6 GitHub stars. The other four have years of history and far more stars. I'll give a snapshot of each, say which job it fits, and tell you plainly when another one is the better pick.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;glincker/theauth-go&lt;/a&gt; (MIT licensed Go library). Docs: &lt;a href="https://docs.theauth.dev/go" rel="noopener noreferrer"&gt;docs.theauth.dev/go&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;"Go auth library" covers at least five different jobs, and most bad comparisons mix them. A social login helper and an OAuth 2.0 server SDK share little except the word auth. So I picked five libraries with five distinct shapes instead of ranking them. Treat this as a list to look at, not a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Go auth library fits which job? The short version
&lt;/h2&gt;

&lt;p&gt;Stats come from the GitHub API, pulled on 2026-10-30. Stars move, so treat them as a snapshot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Created&lt;/th&gt;
&lt;th&gt;Last push&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;What it is for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ory/fosite" rel="noopener noreferrer"&gt;ory/fosite&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2,631&lt;/td&gt;
&lt;td&gt;2015-12&lt;/td&gt;
&lt;td&gt;2025-11-20&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Building an OAuth2 and OpenID Connect server, no user management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/aarondl/authboss" rel="noopener noreferrer"&gt;aarondl/authboss&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;4,201&lt;/td&gt;
&lt;td&gt;2015-01&lt;/td&gt;
&lt;td&gt;2026-07-10&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Modular web sign-in, confirm and recover flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/markbates/goth" rel="noopener noreferrer"&gt;markbates/goth&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6,620&lt;/td&gt;
&lt;td&gt;2014-10&lt;/td&gt;
&lt;td&gt;2026-02-11&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Social login against a list of providers (client side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/go-webauthn/webauthn" rel="noopener noreferrer"&gt;go-webauthn/webauthn&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1,357&lt;/td&gt;
&lt;td&gt;2021-12&lt;/td&gt;
&lt;td&gt;2026-10-29&lt;/td&gt;
&lt;td&gt;BSD-3&lt;/td&gt;
&lt;td&gt;WebAuthn and passkeys only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;glincker/theauth-go&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2026-06&lt;/td&gt;
&lt;td&gt;2026-10&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Sign-in, an OAuth 2.1 server and MCP authorization in one library&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. When to use fosite: you need to be an OAuth2 server
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ory/fosite#readme" rel="noopener noreferrer"&gt;fosite README&lt;/a&gt; calls it a security first OAuth2 and OpenID Connect framework for Go, built on the IETF RFCs. The protocol machinery comes in the box. You supply the storage and the story about who your users are. The &lt;a href="https://pkg.go.dev/github.com/ory/fosite" rel="noopener noreferrer"&gt;pkg.go.dev page&lt;/a&gt; has the API.&lt;/p&gt;

&lt;p&gt;Pick fosite when your product issues tokens to other people's apps. It has a history back to 2015 and a README that names the RFCs it follows. For that job it is the more established choice.&lt;/p&gt;

&lt;p&gt;Know the tradeoff. Fosite is a toolkit, so expect to write the glue: storage, consent screens, login. It won't hand you a sign-in page. Its last push in the snapshot was November 2025, which isn't stale for a protocol library, but check the issue tracker for the features you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. When to use authboss: classic web sign-in
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aarondl/authboss#readme" rel="noopener noreferrer"&gt;Authboss&lt;/a&gt; describes itself as a modular authentication system for the web. You enable the modules you need and leave out the rest. Its README table of contents covers use cases such as resetting a password, and it says the library should integrate with or without any web framework. Package docs are on &lt;a href="https://pkg.go.dev/github.com/aarondl/authboss/v3" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pick authboss for a server-rendered site where users sign up, confirm an email and recover a lost password. You get those flows written once by someone else. It has been around since 2015.&lt;/p&gt;

&lt;p&gt;You will write its storage and user interfaces yourself. Its README lists CSRF protection and request throttling as things your app must bring. If you also need an OAuth server or passkeys, you will be bolting on other pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. When to use goth: users arrive from other providers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/markbates/goth#readme" rel="noopener noreferrer"&gt;Goth&lt;/a&gt; is the oldest and most starred library here. Its README lists a long set of providers, including Google, GitHub, Apple, Okta and OpenID Connect with auto discovery. If yours is missing, you implement its &lt;code&gt;Provider&lt;/code&gt; and &lt;code&gt;Session&lt;/code&gt; interfaces.&lt;/p&gt;

&lt;p&gt;Pick goth when the whole job is "Sign in with X" and you already have your own user table. It stays out of your storage and session design, which is a feature if you have opinions there.&lt;/p&gt;

&lt;p&gt;Goth is client side by design. It talks to other providers. It does not make you one, and it does not manage passwords or MFA. The last push in the snapshot is February 2026, so test the provider you need before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. When to use go-webauthn: passkeys are the job
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/go-webauthn/webauthn#readme" rel="noopener noreferrer"&gt;go-webauthn README&lt;/a&gt; says the library handles Web Authentication for Go apps that want multi-factor, passwordless or usernameless sign-in. It also says the library is conformance tested against the relevant tools. The protocol itself is specified in the &lt;a href="https://www.w3.org/TR/webauthn-3/" rel="noopener noreferrer"&gt;W3C Web Authentication spec&lt;/a&gt;. The last push came the day before my snapshot.&lt;/p&gt;

&lt;p&gt;Pick it when you already have an auth system and want to add passkeys properly. It does one thing, and that narrowness is the point. You wire the ceremonies into your own session handling.&lt;/p&gt;

&lt;p&gt;If you wanted a full system, this is a building block. That is fine, as long as you know it going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. When to use theauth-go: one embedded library
&lt;/h2&gt;

&lt;p&gt;This one is mine, so read with that in mind. The &lt;a href="https://github.com/glincker/theauth-go#readme" rel="noopener noreferrer"&gt;theauth-go README&lt;/a&gt; describes a self-hosted, MIT licensed library that mounts into your own server as an &lt;code&gt;http.Handler&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It covers sign-in for people: passwords, magic links, social login, passkeys, TOTP, SAML SSO and SCIM. It also covers sign-in for software: an OAuth 2.1 authorization server with PKCE (&lt;a href="https://datatracker.ietf.org/doc/html/rfc7636" rel="noopener noreferrer"&gt;RFC 7636&lt;/a&gt;), DPoP (&lt;a href="https://datatracker.ietf.org/doc/html/rfc9449" rel="noopener noreferrer"&gt;RFC 9449&lt;/a&gt;) and token exchange, plus a resource server module for MCP.&lt;/p&gt;

&lt;p&gt;The code below shortens the README quick start. The real one also sets up the SQLite database and runs the migration before &lt;code&gt;theauth.New&lt;/code&gt;, and it hard codes a localhost base URL where I read an environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CoreStorage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/auth/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// sign-up, sign-in, magic link, sessions&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET /me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequireAuth&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;})))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Storage options are memory, SQLite, Postgres and MySQL, and feature support differs per backend. Releases carry cosign signatures, an SBOM and build provenance attestations (the README badge says SLSA 3). Levelrail, another project I maintain, imports it in production code.&lt;/p&gt;

&lt;p&gt;Pick it if you want sign-in and an OAuth server in one module, or you need MCP authorization from Go. Otherwise, read the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  What theauth-go does not do
&lt;/h2&gt;

&lt;p&gt;I would rather you hear this from me.&lt;/p&gt;

&lt;p&gt;The library is young. The repo's first commit was in June 2026, and six stars suggests few people outside my own projects have run it. It has had far less outside use than libraries that date from 2014 and 2015.&lt;/p&gt;

&lt;p&gt;I have no head to head benchmark against anyone, so I make no speed claims. The one performance number I trust is narrow. A session lookup as one joined query took 30 microseconds against 58 to 67 for two queries, on one Apple M4 Max, using &lt;code&gt;BenchmarkSessionLookup&lt;/code&gt;. That compares two designs inside my own code and says nothing about other libraries.&lt;/p&gt;

&lt;p&gt;It also doesn't replace a standalone identity server. Casdoor, Zitadel and Authelia run as separate servers. If you want that shape, you are shopping in a different aisle (the next article in this series maps it).&lt;/p&gt;

&lt;p&gt;Finally, breadth has a cost. One library doing sign-in, OAuth server and SCIM means more surface for me to maintain than fosite's single job. If you need only one slice, the focused library is the safer pick. That means fosite for an OAuth server, go-webauthn for passkeys and goth for social login.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do I choose between these Go auth libraries?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your job is&lt;/th&gt;
&lt;th&gt;Look at first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Issue OAuth2 or OIDC tokens, bring your own users&lt;/td&gt;
&lt;td&gt;fosite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email and password flows on a server rendered site&lt;/td&gt;
&lt;td&gt;authboss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only "Sign in with X"&lt;/td&gt;
&lt;td&gt;goth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add passkeys to an existing system&lt;/td&gt;
&lt;td&gt;go-webauthn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign-in plus OAuth 2.1 plus MCP in one embedded module&lt;/td&gt;
&lt;td&gt;theauth-go, with the age caveat above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Which of these Go auth libraries handles passwords and email confirmation?
&lt;/h3&gt;

&lt;p&gt;Authboss. It covers sign-up, email confirmation and password recovery for server-rendered sites. Goth and go-webauthn don't manage passwords.&lt;/p&gt;
&lt;h3&gt;
  
  
  Can I use fosite as a sign-in library?
&lt;/h3&gt;

&lt;p&gt;No. Fosite issues tokens as an OAuth2 and OpenID Connect server and leaves users, storage and login screens to you.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is theauth-go ready to replace the older libraries?
&lt;/h3&gt;

&lt;p&gt;Not on age. For a single job like passkeys or social login, the focused library is the safer pick.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;None of these is "the" Go auth library. The question hides five questions. Decide which side of the wire you are on, how much of the user lifecycle you want to own, and how much age you need in your dependency. Then read the README of the one that fits and try its example before you believe me or anyone else.&lt;/p&gt;

&lt;p&gt;Which job was yours, and did one of these fit? I would like to hear where the gaps are.&lt;/p&gt;

&lt;p&gt;If you want to read the code or try it, the repo is here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;
        theauth-go
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted Go authentication library. Embeds in net/http, chi, Gin, Echo or Fiber. OAuth 2.1 authorization server, MCP authorization (RFC 9728), passkeys, SAML, SCIM, TOTP, magic links, DPoP. SQLite, Postgres or MySQL. MIT, no hosted service.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;theauth-go&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;OAuth 2.1 and sign-in for Go MCP servers and agents. It mounts into your own &lt;code&gt;net/http&lt;/code&gt; server, your users and tokens stay in your own database, and there is no hosted service. MIT licensed.&lt;/p&gt;

&lt;p&gt;Building an MCP server in Go means implementing OAuth 2.1, protected resource metadata (RFC 9728), PKCE and token validation yourself. This library does that part, so your server accepts tokens from MCP clients and you keep control of identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked against a real client.&lt;/strong&gt; The official &lt;code&gt;modelcontextprotocol/go-sdk&lt;/code&gt; client (v1.8.0) completes the full flow against it in &lt;a href="https://github.com/glincker/theauth-go/integration/mcp-interop" rel="noopener noreferrer"&gt;&lt;code&gt;integration/mcp-interop&lt;/code&gt;&lt;/a&gt;: discovery, dynamic client registration, PKCE S256, token exchange and an authenticated tool call. Login and consent are simulated in that test, so it proves the protocol path, not a production deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it is for:&lt;/strong&gt; Go developers building MCP servers, agent backends or internal tools who want to own their auth instead of renting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · building &lt;a href="https://glincker.com" rel="noopener noreferrer"&gt;Glincker&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Picking an auth library means picking which job you refuse to write yourself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>authentication</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Go auth libraries: pick by the side of the wire you are on</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:21:46 +0000</pubDate>
      <link>https://dev.to/thegdsks/go-auth-libraries-pick-by-the-side-of-the-wire-you-are-on-4582</link>
      <guid>https://dev.to/thegdsks/go-auth-libraries-pick-by-the-side-of-the-wire-you-are-on-4582</guid>
      <description>&lt;h1&gt;
  
  
  Go auth libraries: pick by the side of the wire you are on
&lt;/h1&gt;

&lt;p&gt;Which side of the wire are you on? Are you calling someone else's API, checking a token someone else issued, or issuing tokens yourself? Go auth libraries cluster by that answer, and OAuth threads go wrong when people skip it. This article sorts the main libraries into four groups and ends with a decision flow you can follow in a minute.&lt;/p&gt;

&lt;p&gt;I maintain theauth-go, a four month old library with 6 GitHub stars, so weigh my opinions accordingly. The article is not really about it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;glincker/theauth-go&lt;/a&gt; (MIT licensed Go library). Docs: &lt;a href="https://docs.theauth.dev/go" rel="noopener noreferrer"&gt;docs.theauth.dev/go&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Go auth library should I use?
&lt;/h2&gt;

&lt;p&gt;Someone asks "which auth library should I use" and gets answers that solve three different problems. Name your side first, then the shortlist gets short.&lt;/p&gt;

&lt;p&gt;Stats are from the GitHub API on 2026-10-30.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Side of the wire&lt;/th&gt;
&lt;th&gt;You are...&lt;/th&gt;
&lt;th&gt;Look at&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Calling an API with OAuth credentials&lt;/td&gt;
&lt;td&gt;golang/oauth2, grokify/goauth&lt;/td&gt;
&lt;td&gt;5,908 and 74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;Checking tokens or ID tokens from a provider&lt;/td&gt;
&lt;td&gt;coreos/go-oidc&lt;/td&gt;
&lt;td&gt;2,503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddable server&lt;/td&gt;
&lt;td&gt;Issuing tokens or signing in users inside your own process&lt;/td&gt;
&lt;td&gt;fosite, theauth-go, others&lt;/td&gt;
&lt;td&gt;2,631 and 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone server&lt;/td&gt;
&lt;td&gt;Running identity as a separate service&lt;/td&gt;
&lt;td&gt;Hydra, Dex, Zitadel&lt;/td&gt;
&lt;td&gt;17,639, 11,227, 15,484&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which Go library calls an OAuth API for me?
&lt;/h2&gt;

&lt;p&gt;If your Go program needs a token to call GitHub, Google or a SaaS API, you are a client. The anchor is &lt;a href="https://github.com/golang/oauth2#readme" rel="noopener noreferrer"&gt;golang/oauth2&lt;/a&gt; (&lt;a href="https://pkg.go.dev/golang.org/x/oauth2" rel="noopener noreferrer"&gt;pkg.go.dev docs&lt;/a&gt;). Its README calls it a client implementation of the OAuth 2.0 spec, defined in &lt;a href="https://datatracker.ietf.org/doc/html/rfc6749" rel="noopener noreferrer"&gt;RFC 6749&lt;/a&gt;. The repo has 5,908 stars, dates from 2014-04, was last pushed 2026-09-08 and uses a BSD-3 license.&lt;/p&gt;

&lt;p&gt;On top of that sits a layer of convenience. &lt;a href="https://github.com/grokify/goauth#readme" rel="noopener noreferrer"&gt;grokify/goauth&lt;/a&gt; is the one I want to say something kind about. The project is small (74 stars, from 2017-06, last pushed 2026-10-30, MIT) and does real work. Its README describes a way to create authenticated &lt;code&gt;*http.Client&lt;/code&gt; instances from a single JSON configuration file, with pre-configured endpoints for 40+ OAuth 2.0 providers. It lists six grant types, PKCE (&lt;a href="https://datatracker.ietf.org/doc/html/rfc7636" rel="noopener noreferrer"&gt;RFC 7636&lt;/a&gt;) and DPoP (RFC 9449) support, and command line tools for token generation.&lt;/p&gt;

&lt;p&gt;That shape helps if you juggle credentials for a dozen SaaS APIs in one program and want one config format. Its last push was the day of my snapshot. If your problem is "I call twelve vendor APIs", look at goauth before you write your own token plumbing.&lt;/p&gt;

&lt;p&gt;None of this is a server. These libraries exist so your program can get and use tokens, not issue them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: sign-in with social providers sits on the border. Goth (markbates/goth, 6,620 stars) is a client side library for "Sign in with X" in a web app. It consumes other providers. It is not a server you run for others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How do I verify OIDC ID tokens in Go?
&lt;/h2&gt;

&lt;p&gt;If a provider issued the token and your service only needs to trust it, you are a verifier. The library is &lt;a href="https://github.com/coreos/go-oidc#readme" rel="noopener noreferrer"&gt;coreos/go-oidc&lt;/a&gt; (&lt;a href="https://pkg.go.dev/github.com/coreos/go-oidc/v3/oidc" rel="noopener noreferrer"&gt;pkg.go.dev docs&lt;/a&gt;). Its README describes an OpenID Connect client that works with golang.org/x/oauth2. The quick start creates a provider by discovery, then an ID token verifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;oidc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://accounts.google.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="n"&gt;idTokenVerifier&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;oidc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ClientID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clientID&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The README snippet is cut short here. The full version also builds the &lt;code&gt;oauth2.Config&lt;/code&gt;. The repo has 2,503 stars, dates from 2015-06, was last pushed 2026-10-19 and uses Apache-2.0. Its README says over 1000 open source projects import it. The ID token format itself comes from the &lt;a href="https://openid.net/specs/openid-connect-core-1_0.html" rel="noopener noreferrer"&gt;OpenID Connect Core spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Choose this when your service sits behind an identity provider (Google, Okta, a Kubernetes cluster) and the work is checking signatures, issuer and audience. You do not need an auth server for that. Adding one means extra moving parts for no gain.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which Go library lets me run my own OAuth server?
&lt;/h2&gt;

&lt;p&gt;Now you are the provider. Users sign in to you, or other apps ask you for tokens. Embeddable means the code runs in your binary, mounts on your router, and uses your database.&lt;/p&gt;

&lt;p&gt;The long standing option for the protocol half is &lt;a href="https://github.com/ory/fosite#readme" rel="noopener noreferrer"&gt;fosite&lt;/a&gt; (2,631 stars, from 2015-12, last push 2025-11-20, Apache-2.0). Its README describes an OAuth2 and OpenID Connect framework. Fosite is a toolkit, so storage and user handling are yours.&lt;/p&gt;

&lt;p&gt;For web sign-in flows there is &lt;a href="https://github.com/aarondl/authboss#readme" rel="noopener noreferrer"&gt;authboss&lt;/a&gt; (4,201 stars). For passkeys alone there is &lt;a href="https://github.com/go-webauthn/webauthn#readme" rel="noopener noreferrer"&gt;go-webauthn&lt;/a&gt; (1,357 stars).&lt;/p&gt;

&lt;p&gt;Then there is theauth-go, which tries to put sign-in, an OAuth 2.1 server and MCP authorization behind one &lt;code&gt;http.Handler&lt;/code&gt;. Here is the sign-in half, shortened from its README quick start (I left out the database setup, and the README hard codes a localhost base URL where I read an environment variable):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CoreStorage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/auth/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The honest comparison: fosite has a decade of use and one focused job. Theauth-go has months of use and a wide job. If you only need the OAuth server half, read fosite's README first. If you want the pieces pre-assembled and can accept a young dependency, mine is worth a trial in a sandbox.&lt;/p&gt;
&lt;h2&gt;
  
  
  When should I run a standalone auth server?
&lt;/h2&gt;

&lt;p&gt;Sometimes the right answer is not a library. You run a service, and your Go apps talk to it over HTTP. These stats come from the same 2026-10-30 snapshot. I copied the shape column from my own research notes and did not recheck it against each README.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ory/hydra" rel="noopener noreferrer"&gt;ory/hydra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17,639&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;OAuth2 and OIDC provider, separate service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/dexidp/dex" rel="noopener noreferrer"&gt;dexidp/dex&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;11,227&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;OIDC federation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/zitadel/zitadel" rel="noopener noreferrer"&gt;zitadel/zitadel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;15,484&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;Full identity platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick this side when a mix of languages shares one identity system, when a team other than yours operates it, or when you want a project with that kind of adoption behind it. Zitadel is AGPL-3.0, which matters to some companies and not to others, so read it before you plan around it. Any standalone server is one more thing to deploy, upgrade and keep available.&lt;/p&gt;
&lt;h2&gt;
  
  
  A decision flow for choosing a Go auth library
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are you calling an API with your own credentials?
  yes -&amp;gt; client side: golang/oauth2, or grokify/goauth for multiple vendors
  no  -&amp;gt; continue

Did another party issue the token you need to check?
  yes -&amp;gt; verifier: coreos/go-oidc
  no  -&amp;gt; continue

Do you need to issue tokens or sign users in?
  yes -&amp;gt; Must it run as its own service, or serve a mix of languages?
           yes -&amp;gt; standalone server: Hydra, Dex, Zitadel
           no  -&amp;gt; embeddable: fosite (protocol only), authboss (web flows),
                  go-webauthn (passkeys only), or theauth-go (all in one, young)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Real systems hit more than one box. A service can verify tokens with go-oidc at the edge, call vendors through oauth2 behind it, and embed a server for its own users. Use the flow to pick a starting library per job, not one answer for the whole app.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the lines blur
&lt;/h2&gt;

&lt;p&gt;Two places trip people up. First, goth and goauth both help with "Sign in with Google", but goth targets web app login while goauth centers on authenticated API clients. Check which one matches your flow.&lt;/p&gt;

&lt;p&gt;Second, an embeddable server often includes a verifier. If you run your own authorization server and also host an API, your API still has to check tokens. In theauth-go that is a separate zero dependency module called &lt;code&gt;mcpresource&lt;/code&gt;, so a resource server skips the whole library.&lt;/p&gt;
&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Do I need an auth server to verify tokens from Google or Okta?
&lt;/h3&gt;

&lt;p&gt;No. A provider already issued the token, so you are a verifier. go-oidc checks signature, issuer and audience, and you run no server of your own.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which Go library is for calling several vendor APIs?
&lt;/h3&gt;

&lt;p&gt;Start with golang/oauth2. If you juggle credentials for several providers, goauth offers one JSON config and pre-configured endpoints for 40+ OAuth 2.0 providers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is a library or a standalone server better?
&lt;/h3&gt;

&lt;p&gt;It depends on who operates it. A standalone server suits mixed languages or a separate team. A library keeps everything in one binary, with fewer services to run.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Before you compare feature tables, name your side of the wire. Calling an API points you to oauth2 or goauth. Trusting a provider points you to go-oidc. Issuing tokens points you to fosite, authboss, go-webauthn, a standalone server, or an all in one like mine. Star counts tell you how many people have looked. They say nothing about fit, so read the README and run the example.&lt;/p&gt;

&lt;p&gt;If your case falls between two of these boxes, which one and why? That is the part I want to hear about.&lt;/p&gt;

&lt;p&gt;If you want to read the code or try it, the repo is here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;
        theauth-go
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted Go authentication library. Embeds in net/http, chi, Gin, Echo or Fiber. OAuth 2.1 authorization server, MCP authorization (RFC 9728), passkeys, SAML, SCIM, TOTP, magic links, DPoP. SQLite, Postgres or MySQL. MIT, no hosted service.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;theauth-go&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;OAuth 2.1 and sign-in for Go MCP servers and agents. It mounts into your own &lt;code&gt;net/http&lt;/code&gt; server, your users and tokens stay in your own database, and there is no hosted service. MIT licensed.&lt;/p&gt;

&lt;p&gt;Building an MCP server in Go means implementing OAuth 2.1, protected resource metadata (RFC 9728), PKCE and token validation yourself. This library does that part, so your server accepts tokens from MCP clients and you keep control of identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked against a real client.&lt;/strong&gt; The official &lt;code&gt;modelcontextprotocol/go-sdk&lt;/code&gt; client (v1.8.0) completes the full flow against it in &lt;a href="https://github.com/glincker/theauth-go/integration/mcp-interop" rel="noopener noreferrer"&gt;&lt;code&gt;integration/mcp-interop&lt;/code&gt;&lt;/a&gt;: discovery, dynamic client registration, PKCE S256, token exchange and an authenticated tool call. Login and consent are simulated in that test, so it proves the protocol path, not a production deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it is for:&lt;/strong&gt; Go developers building MCP servers, agent backends or internal tools who want to own their auth instead of renting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/theauth-go" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · building &lt;a href="https://glincker.com" rel="noopener noreferrer"&gt;Glincker&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The right Go auth library is whichever one matches the side of the wire you actually stand on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>authentication</category>
      <category>oauth</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Self-Hosted Vercel and Heroku Alternatives, Compared</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Fri, 30 Oct 2026 22:23:44 +0000</pubDate>
      <link>https://dev.to/thegdsks/self-hosted-vercel-and-heroku-alternatives-compared-4o3l</link>
      <guid>https://dev.to/thegdsks/self-hosted-vercel-and-heroku-alternatives-compared-4o3l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Levelrail is open source (Apache 2.0). Read the code, star it and try it on GitHub: &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;github.com/glincker/levelrail&lt;/a&gt;, or &lt;a href="https://levelrail.com/install/" rel="noopener noreferrer"&gt;install it in one command&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Searching for a self-hosted Vercel alternative turns up a long list, and the names do different things. Some copy the Heroku &lt;code&gt;git push&lt;/code&gt; workflow. Some are dashboards over Docker. Some also want you to run a cluster. This post sorts them out, says what each is good at, and ends with a pick by use case.&lt;/p&gt;

&lt;p&gt;I build Levelrail, so I put it first in the picks and say why. The rest are worth your time too, and I have kept the claims about them to what their own repositories and docs say. For a per-feature view, the &lt;a href="https://levelrail.com/compare/coolify/" rel="noopener noreferrer"&gt;Coolify comparison&lt;/a&gt; has sources for each row, and the &lt;a href="https://levelrail.com/alternatives/" rel="noopener noreferrer"&gt;alternatives page&lt;/a&gt; covers the wider field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hosted options first
&lt;/h2&gt;

&lt;p&gt;You may not need to self-host. Here is what the hosted platforms do.&lt;/p&gt;

&lt;p&gt;Vercel deploys from GitHub, GitLab or Bitbucket, and every push to the connected branch triggers a new deployment. It has a CLI and lets you provision databases from its marketplace. Its design centers on front-end frameworks and its own functions runtime.&lt;/p&gt;

&lt;p&gt;Heroku is where &lt;code&gt;git push heroku main&lt;/code&gt; came from. You push to a remote, it builds, and it creates a new release. Its docs list limits worth knowing, such as the app having to sit at the root of the repository.&lt;/p&gt;

&lt;p&gt;Railway builds a Docker image from a repository or deploys an image you supply, and gives you a dashboard, volumes with backups and private networking inside a project.&lt;/p&gt;

&lt;p&gt;All three run the servers for you, and that is a fair reason to stay. People leave when cost grows with usage, when data location matters, or when they want one machine they control to hold everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-hosted options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Coolify
&lt;/h3&gt;

&lt;p&gt;Coolify is Apache 2.0 licensed and installs with a single &lt;code&gt;curl&lt;/code&gt; command on any server you can reach over SSH. It deploys from GitHub, GitLab, Bitbucket or Gitea with deploy on push and pull request previews, builds with Nixpacks, Railpack, a Dockerfile, Compose or a prebuilt image, and ships over 360 one-click services. It has managed databases, database and storage backups, automatic HTTPS and multi-server management.&lt;/p&gt;

&lt;p&gt;Coolify is the most widely known option here and the template catalog is the draw. It fits solo developers and small teams who want a friendly dashboard and lots of ready-made services. If you already run it, the &lt;a href="https://levelrail.com/migrate/coolify/" rel="noopener noreferrer"&gt;Coolify importer&lt;/a&gt; reads your setup without changing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dokploy
&lt;/h3&gt;

&lt;p&gt;Dokploy installs with one &lt;code&gt;curl&lt;/code&gt; command on a VPS. It runs PostgreSQL, MySQL, MariaDB, MongoDB, libsql and Redis, takes automated database backups to external storage, uses Traefik for routing and Docker Swarm for multi-node scaling. It shows CPU, memory, storage and network use per resource and can ship logs and metrics to tools such as Grafana.&lt;/p&gt;

&lt;p&gt;The repository is Apache 2.0 with a carve-out: content under a &lt;code&gt;/proprietary&lt;/code&gt; directory is under a separate license, so read that file before you depend on it. It fits people who already like Swarm and want a dashboard on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  CapRover
&lt;/h3&gt;

&lt;p&gt;CapRover has a web GUI and a CLI, runs on Docker Swarm, uses an Nginx template for load balancing, issues free Let's Encrypt certificates, and installs databases such as MariaDB, MySQL and MongoDB from a dropdown. Its README lists NetData monitoring.&lt;/p&gt;

&lt;p&gt;The license is Apache 2.0 with an appendix that restricts changes to its paid features, so read the LICENSE file if you plan to fork. It fits people who want a simple Swarm-based panel for a handful of apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dokku
&lt;/h3&gt;

&lt;p&gt;Dokku is MIT licensed and calls itself a Docker powered mini-Heroku. You install it on a fresh Ubuntu or Debian machine with a bootstrap script, add your SSH key, and deploy with &lt;code&gt;git push dokku main&lt;/code&gt;. It builds with Heroku buildpacks by default and also takes a Dockerfile or an image. You drive it with &lt;code&gt;dokku&lt;/code&gt; commands on the host, and services such as PostgreSQL arrive as plugins.&lt;/p&gt;

&lt;p&gt;The core project has no dashboard, which suits people who live in a terminal. It fits solo developers and homelabs who want the Heroku workflow on one box with the fewest moving parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Levelrail
&lt;/h3&gt;

&lt;p&gt;Levelrail is Apache 2.0 and ships as one Go binary that runs your apps in Docker. Levelrail needs no Kubernetes. The install is one command:&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;-fsSL&lt;/span&gt; https://levelrail.com/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploys start when you push to GitHub, GitLab, Bitbucket or Gitea, with preview environments per pull request. HTTPS comes from an embedded Caddy ingress, with Let's Encrypt behind one toggle. Databases (Postgres, MySQL, MariaDB, MongoDB, Redis and more) get scheduled backups that the platform verifies automatically. Metrics, logs and alerts come included, so you do not install a monitoring stack next to it. A deploy only cuts traffic over once the new version is ready, and a bad release rolls back to the previous image with one command. There is also an MCP server, so an AI assistant can inspect and operate the platform.&lt;/p&gt;

&lt;p&gt;The tradeoff: the community is smaller than Coolify's or Dokku's, so there are fewer third party guides to lean on. Pick it if you care more about what happens after the deploy (rollback, backups you can trust, alerts) than about the size of the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Facts side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Install&lt;/th&gt;
&lt;th&gt;Dashboard&lt;/th&gt;
&lt;th&gt;Databases and backups&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coolify&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; script&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Managed databases, backups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dokploy&lt;/td&gt;
&lt;td&gt;Apache 2.0, with a proprietary directory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; script&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Six engines, backups to external storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CapRover&lt;/td&gt;
&lt;td&gt;Apache 2.0, with a paid-features appendix&lt;/td&gt;
&lt;td&gt;Setup per its guide&lt;/td&gt;
&lt;td&gt;Yes, plus a CLI&lt;/td&gt;
&lt;td&gt;Installed from a dropdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dokku&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Bootstrap script&lt;/td&gt;
&lt;td&gt;Core is CLI only&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Levelrail&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; script&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Scheduled, automatically verified backups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources are each project's GitHub repository and docs, read on 2026-10-30. Where a project's page did not state something, I left it out rather than guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you pick
&lt;/h2&gt;

&lt;p&gt;A solo developer leaving Vercel or Heroku to run real apps with a database should start with Levelrail. One command installs it, a push deploys, and logs, metrics, alerts and rollback are there on day one without extra setup. If you only want the Heroku flow on one box and like the terminal, Dokku is lighter.&lt;/p&gt;

&lt;p&gt;A small team or agency with client apps should also look at Levelrail first, for the backups and rollback that matter more as clients multiply. Coolify is a strong second if the one-click catalog is what saves you time.&lt;/p&gt;

&lt;p&gt;For a homelab, Dokku suits shell people, and Coolify suits anyone who wants a dashboard and a big catalog of services to try. Levelrail runs on a small machine too and gives you metrics without extra containers.&lt;/p&gt;

&lt;p&gt;If you already run Swarm, Dokploy and CapRover are both built on it, so they fit what you know.&lt;/p&gt;

&lt;p&gt;If you need a global edge network for a front end, keep Vercel for that site. A single server will not replace it, and mixing the two is fine.&lt;/p&gt;

&lt;p&gt;Weighing Levelrail against Coolify specifically? Read the &lt;a href="https://levelrail.com/compare/coolify/" rel="noopener noreferrer"&gt;Coolify comparison&lt;/a&gt;. For the Heroku side, see &lt;a href="https://levelrail.com/heroku-alternative-self-hosted/" rel="noopener noreferrer"&gt;Heroku alternatives you can self-host&lt;/a&gt;. To try it, follow the &lt;a href="https://levelrail.com/blog/install-levelrail-five-minutes/" rel="noopener noreferrer"&gt;five-minute install&lt;/a&gt; or the &lt;a href="https://levelrail.com/install/" rel="noopener noreferrer"&gt;install page&lt;/a&gt;, and the &lt;a href="https://docs.levelrail.com" rel="noopener noreferrer"&gt;docs&lt;/a&gt; cover the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the best self-hosted alternative to Vercel?
&lt;/h3&gt;

&lt;p&gt;It depends on what you deploy. For a front end with preview deployments, Coolify and Levelrail both build from a Git repository and deploy on push. For a mix of front ends, APIs and databases on one server, with logs, metrics and rollback built in, Levelrail is the one this site recommends. You will not match Vercel's global edge network on a single server, so decide whether you need that.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best self-hosted alternative to Heroku?
&lt;/h3&gt;

&lt;p&gt;Dokku is the closest to the Heroku workflow: git push to a remote, Heroku buildpacks by default, and Dockerfile or image deploys too. If you want a web dashboard, health checks and rollback on top of that workflow, look at Levelrail or Coolify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run a self-hosted platform on a cheap VPS?
&lt;/h3&gt;

&lt;p&gt;Levelrail boots on 1 vCPU, 1 GB of RAM and 10 GB of disk, and 2 vCPU with 2 GB is more comfortable once you build images on the same box. For the others, check each project's installation guide for its stated requirements, since they are not all listed on the repository pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it worth leaving Vercel or Heroku for a self-hosted platform?
&lt;/h3&gt;

&lt;p&gt;If your bill is small and you value not running servers, stay. Moving makes sense when the bill grows with traffic or seats, when data must stay on hardware you control, or when you already pay for a server that has spare room. You take on updates, backups and uptime, so pick a platform that automates those.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://levelrail.com/blog/self-hosted-vercel-heroku-alternatives/" rel="noopener noreferrer"&gt;levelrail.com/blog/self-hosted-vercel-heroku-alternatives&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>devops</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 Self-Hosted Apps Worth Running on Your Own Server</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Fri, 30 Oct 2026 22:23:31 +0000</pubDate>
      <link>https://dev.to/thegdsks/5-self-hosted-apps-worth-running-on-your-own-server-50je</link>
      <guid>https://dev.to/thegdsks/5-self-hosted-apps-worth-running-on-your-own-server-50je</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Levelrail runs all five on one server with HTTPS, backups and rollback. It is open source (Apache 2.0): &lt;a href="https://github.com/glincker/levelrail" rel="noopener noreferrer"&gt;star it on GitHub&lt;/a&gt; or &lt;a href="https://levelrail.com/install/" rel="noopener noreferrer"&gt;install it in one command&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most "best self hosted apps" lists are a wall of 80 logos. This one has five, picked because each replaces a hosted tool people pay for every month, and each one is small enough to run on a single cheap server.&lt;/p&gt;

&lt;p&gt;Every app below is a one-click template in Levelrail, a self-hosted platform that deploys apps to your own Linux server from a git push or a template. Each section ends with the install command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run them on one server, not five
&lt;/h2&gt;

&lt;p&gt;The hard part of self-hosting is not the first install. The fifth is harder, when you have five apps, five upgrade schedules and no idea which volume holds what.&lt;/p&gt;

&lt;p&gt;Levelrail gives you one place for that: HTTPS on your own domains, logs per app, deploy history with rollback, and backups of app volumes to an S3-compatible bucket. If you have not installed it yet, &lt;a href="https://levelrail.com/install/" rel="noopener noreferrer"&gt;the install guide&lt;/a&gt; takes minutes, and &lt;a href="https://levelrail.com/blog/install-levelrail-five-minutes/" rel="noopener noreferrer"&gt;the five-minute walkthrough&lt;/a&gt; shows the real output.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.levelrail.com/template-catalog" rel="noopener noreferrer"&gt;template catalog&lt;/a&gt; lists the templates. A &lt;a href="https://docs.levelrail.com/templates-status" rel="noopener noreferrer"&gt;smoke run on 2026-10-25&lt;/a&gt; brought up the five below, and every one passed. If you already run another platform, the &lt;a href="https://levelrail.com/compare/coolify/" rel="noopener noreferrer"&gt;comparison with Coolify&lt;/a&gt; lists the differences with sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Uptime Kuma: know before your users do
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnnvk50g5gutfzuubxfd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnnvk50g5gutfzuubxfd.webp" alt="The Uptime Kuma dashboard listing monitors with their status and a response time chart" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshot: Uptime Kuma dashboard, from the project README (MIT license). Source: github.com/louislam/uptime-kuma&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It replaces hosted uptime monitoring subscriptions.&lt;/p&gt;

&lt;p&gt;Uptime Kuma checks HTTP, TCP, DNS and ping targets on a schedule and shows a dashboard. It suits anyone with more than one site or service who currently finds out about outages from a customer.&lt;/p&gt;

&lt;p&gt;A concrete use: add every app you host here, plus the ones you do not. Put Uptime Kuma on the same server as the apps and you will learn what breaks the setup: if the server dies, the checks die with it. Point a second check at it from somewhere else.&lt;/p&gt;

&lt;p&gt;Watch out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory is small. The template recommends 256 MB.&lt;/li&gt;
&lt;li&gt;Back up the &lt;code&gt;data&lt;/code&gt; volume, which holds the SQLite database and uploads.&lt;/li&gt;
&lt;li&gt;Upgrades matter. The template pins the 1.23 line. Version 2 migrates the database to a layout you cannot roll back, so back up first and upgrade on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it. This template ships with an install form, so you can set a domain in the same command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli templates &lt;span class="nb"&gt;install &lt;/span&gt;uptime-kuma &lt;span class="nt"&gt;--domain&lt;/span&gt; &lt;span class="nv"&gt;kuma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Umami: analytics without a cookie banner
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The Umami logo, shown only to identify the project. Visit umami.is for screenshots.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It replaces a paid analytics subscription for small sites.&lt;/p&gt;

&lt;p&gt;Umami counts visits, pages and referrers without tracking cookies. It suits people who want to know which post got read, not to profile anyone.&lt;/p&gt;

&lt;p&gt;A concrete use: one Umami instance can hold every site you run. Paste one script tag per site and you have a single dashboard instead of one login per project.&lt;/p&gt;

&lt;p&gt;Watch out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs with a Postgres container next to it. The template recommends 512 MB for the app.&lt;/li&gt;
&lt;li&gt;Back up &lt;code&gt;umami_db_data&lt;/code&gt;. That volume is your whole history.&lt;/li&gt;
&lt;li&gt;Migrations run on boot and only go forward. Keep the &lt;code&gt;postgresql-&lt;/code&gt; variant of the image tag and upgrade one release at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli templates deploy umami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. n8n: automation you can see
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The n8n logo, shown only to identify the project. n8n is under the Sustainable Use License, so we link instead of copying its screenshots: n8n.io.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It replaces hosted workflow automation with per-task pricing.&lt;/p&gt;

&lt;p&gt;n8n is a visual editor where you connect services with nodes. It suits people who keep writing small scripts to move data between tools and would rather draw it.&lt;/p&gt;

&lt;p&gt;A concrete use: a workflow that watches a form, writes the row to a sheet and posts to chat. You can run the same flow a thousand times without watching a task counter.&lt;/p&gt;

&lt;p&gt;Watch out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The template recommends 512 MB.&lt;/li&gt;
&lt;li&gt;Back up &lt;code&gt;n8n_data&lt;/code&gt;. It holds the database and the encryption key. If you lose the key, every saved credential becomes unreadable.&lt;/li&gt;
&lt;li&gt;Read the release notes before changing the image tag, because nodes get removed between versions.&lt;/li&gt;
&lt;li&gt;The default template stores data in a SQLite file. For heavier workflow volumes there is an &lt;code&gt;n8n-postgres&lt;/code&gt; template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli templates deploy n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Vaultwarden: a password manager you host
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The Vaultwarden logo, shown only to identify the project. Vaultwarden is AGPL-3.0, so we link instead of copying screenshots: github.com/dani-garcia/vaultwarden.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It replaces a hosted password manager subscription.&lt;/p&gt;

&lt;p&gt;Vaultwarden is a small server that works with the Bitwarden clients, so you keep the browser extension and phone apps and move only the server. It suits individuals and families who want the vault on hardware they control.&lt;/p&gt;

&lt;p&gt;A concrete use: a shared family vault for the streaming logins and the router password, behind your own domain.&lt;/p&gt;

&lt;p&gt;This is the one app on the list where a mistake costs the most, so be strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serve it over HTTPS only. The clients expect it.&lt;/li&gt;
&lt;li&gt;Back up &lt;code&gt;vaultwarden_data&lt;/code&gt;, which holds the database, attachments and keys, and keep a copy offline.&lt;/li&gt;
&lt;li&gt;Tag bumps are safe and frequent, since the web vault has to match the server. Do not let it fall far behind.&lt;/li&gt;
&lt;li&gt;The template recommends 256 MB, the lowest of the five.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli templates deploy vaultwarden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Gitea: your own git server
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvobbr90v0kxxw4p76jg3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvobbr90v0kxxw4p76jg3.webp" alt="The Gitea home dashboard showing the activity feed and repository list" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshot: Gitea home page, from the project README (MIT license). Source: github.com/go-gitea/gitea&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It replaces a hosted git service for private repositories.&lt;/p&gt;

&lt;p&gt;Gitea is a light git host with issues, pull requests and a package registry. It is for solo developers and small teams who want private code on their own disk, or a place to mirror what lives elsewhere.&lt;/p&gt;

&lt;p&gt;A concrete use: keep client work or infrastructure repos here, and connect it to Levelrail as a git provider so a push deploys the app. Levelrail lists Gitea next to GitHub, GitLab and Bitbucket in its git integrations.&lt;/p&gt;

&lt;p&gt;Watch out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The default template runs Gitea with a MariaDB container and recommends 512 MB. If you prefer one database engine everywhere, use &lt;code&gt;gitea-postgres&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Back up both &lt;code&gt;gitea_db_data&lt;/code&gt; and &lt;code&gt;gitea_data&lt;/code&gt;. The repositories live in the second one.&lt;/li&gt;
&lt;li&gt;Gitea migrates its database on boot and does not support downgrades. Move one minor version at a time and read the breaking changes first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;levelrail-cli templates deploy gitea
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template publishes the git port as 2222 by default, so open it in your firewall only if you want SSH clones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What all five need from you
&lt;/h2&gt;

&lt;p&gt;The apps differ, but the chores are the same. Pick a backup target before you store anything you care about, and test a restore once. The docs on &lt;a href="https://docs.levelrail.com/backups-and-storage" rel="noopener noreferrer"&gt;backups and storage&lt;/a&gt; cover app volume backups, and every section above names the volume to protect.&lt;/p&gt;

&lt;p&gt;Upgrade on purpose, one version at a time, with a backup taken first. And keep a rollback in mind: &lt;a href="https://levelrail.com/blog/rollback-failed-deploy-docker/" rel="noopener noreferrer"&gt;this walkthrough&lt;/a&gt; shows how a bad release gets undone.&lt;/p&gt;

&lt;p&gt;On sizing, the five templates recommend 2 GB of memory in total. A 4 GB server runs them with room left for the platform. Browse the &lt;a href="https://levelrail.com/templates/" rel="noopener noreferrer"&gt;templates page&lt;/a&gt; if you want more than five.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What should I self-host first?
&lt;/h3&gt;

&lt;p&gt;Start with something small that you will notice if it breaks, like an uptime monitor. Uptime Kuma is a good first pick: its template recommends 256 MB of memory and it keeps everything in one data volume. Move on to a password manager or analytics once you trust your backups.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much server do I need to self-host these apps?
&lt;/h3&gt;

&lt;p&gt;The memory recommended by the five templates adds up to 2 GB: 256 MB each for Uptime Kuma and Vaultwarden, 512 MB each for Umami, n8n and Gitea. Add room for the platform itself, which boots on 1 GB of RAM, so a 4 GB server is a comfortable size for all five.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is self-hosting a password manager safe?
&lt;/h3&gt;

&lt;p&gt;It is safe only if you keep it patched, serve it over HTTPS and back up its data volume somewhere other than the same server. Vaultwarden stores its database, attachments and keys in one volume. Keep an offline copy of that backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Docker knowledge to self-host these?
&lt;/h3&gt;

&lt;p&gt;Not to get them running. Each app is a ready-made template with its image, database and volumes defined, so you install it from the dashboard or one CLI command. You will want to understand volumes and backups before you trust it with real data.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://levelrail.com/blog/best-self-hosted-apps-worth-running/" rel="noopener noreferrer"&gt;levelrail.com/blog/best-self-hosted-apps-worth-running&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>opensource</category>
      <category>docker</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
