<?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: octave Nkurunziza</title>
    <description>The latest articles on DEV Community by octave Nkurunziza (@octave_nkurunziza_afb0512).</description>
    <link>https://dev.to/octave_nkurunziza_afb0512</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%2F1847891%2F35ab1087-2f87-4c90-843e-cc2e9de399da.png</url>
      <title>DEV Community: octave Nkurunziza</title>
      <link>https://dev.to/octave_nkurunziza_afb0512</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/octave_nkurunziza_afb0512"/>
    <language>en</language>
    <item>
      <title>Fixed vs pay-as-you-go hosting: find your break-even point</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:06:41 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/fixed-vs-pay-as-you-go-hosting-find-your-break-even-point-4eo8</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/fixed-vs-pay-as-you-go-hosting-find-your-break-even-point-4eo8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/fixed-vs-pay-as-you-go-hosting-break-even?utm_source=devto&amp;amp;utm_campaign=fixed-vs-pay-as-you-go-hosting-break-even" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a two-line formula that tells you when a fixed plan beats pay-as-you-go, why memory (not CPU) sets the floor of most bills, and three common apps priced on six platforms (prices from official pages, 21 Oct 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The short answer:&lt;/strong&gt; a fixed plan wins when your app is small, steady and fits a plan size exactly. Paying for real CPU use wins when traffic is spiky and CPU sits idle. Sleeping when idle beats both for apps nobody uses at night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; developers and founders picking a host for an API, a side project or a small SaaS. About 12 minutes to read.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your app costs $7 a month on one platform, $6.09 on another and $2.19 on a third. All three pages say they're cheap, and all three are right for someone. The trouble is that "usage-based" and "fixed" hide very different meters, and picking the wrong one means paying every month for capacity you never touch, or getting a surprise bill after a traffic spike.&lt;/p&gt;

&lt;p&gt;In this guide you'll learn what each pricing model actually charges for, price three common apps on six platforms, and get a two-line formula to find the point where switching models saves you money. It matters because hosting is a bill you pay forever: a $5 difference is $60 a year per service, and most apps run more than one service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Try it on Hubfly space&lt;/a&gt;: the free tier gives you 360 hours a month on the shared tier, no card needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways hosts charge you
&lt;/h2&gt;

&lt;p&gt;Every platform in this guide falls into one of three groups. Knowing which one you're on tells you what to watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed plans (you pay for a size).&lt;/strong&gt; You pick a box, such as 0.5 CPU and 512 MB, and pay a flat monthly price whether the app is busy or idle. &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;Render&lt;/a&gt; ($7 for its "less than 1 CPU" / 512 MB instance), &lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt; ($7 Basic dyno, 0.5 GB) and &lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;DigitalOcean App Platform&lt;/a&gt; ($5 for 1 vCPU / 512 MiB) work this way. Why it matters: the bill is easy to predict, but if your app needs 600 MB you jump to the next size and pay for CPU you didn't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pay for what's allocated, while it runs.&lt;/strong&gt; You still choose a size, but you're billed by the hour or second only while the app is running. Stop it, or let it sleep, and compute billing stops. &lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt; bills started Machines "by the second while running" ($3.69 per 30 days for shared-cpu-1x with 512 MB in its cheapest regions). &lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly space&lt;/a&gt; works the same way: $0.004 per vCPU-hour and $0.002 per GB-hour of allocated memory, billed only while the container is awake. Why it matters: you still pick a size, so oversizing still costs you, but idle hours can be free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pay for what's consumed.&lt;/strong&gt; You set a maximum, and the platform meters the CPU and memory your app actually uses. &lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; is the clearest example: $0.00000772 per vCPU-second and $0.00000386 per GB-second, which is about $20.29 per vCPU and $10.14 per GB of RAM over a 730-hour month. The Hobby plan has a $5 minimum that includes $5 of usage. Why it matters: an app that averages 5% of a CPU pays for 5%, so you never pay for headroom you don't use.&lt;/p&gt;

&lt;p&gt;A fourth group, request-based platforms like Vercel and Cloudflare Workers, bills per request and execution time. It's great for front ends and functions, but most apps that need a database and a long-running process end up on one of the first three, so this guide focuses on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meter nobody talks about: memory
&lt;/h2&gt;

&lt;p&gt;"Pay only for what you use" sounds like your bill drops to almost nothing when the app is quiet. It doesn't, and the reason is memory.&lt;/p&gt;

&lt;p&gt;CPU use rises and falls with traffic. Memory doesn't. A Node or Python app loads its code, libraries and caches at startup and holds them until it stops. So a small API can hold hundreds of megabytes of RAM at 3 a.m. with zero requests.&lt;/p&gt;

&lt;p&gt;On Railway, 0.5 GB held for a whole month costs 0.5 × $10.14 = &lt;strong&gt;$5.07&lt;/strong&gt;, even if CPU is close to zero. That's already past the $5 Hobby minimum before you've used a single CPU cycle. So when you compare platforms, put your app's idle memory into the math first. It sets your floor.&lt;/p&gt;

&lt;p&gt;The same logic applies to allocated platforms in a different way: on Fly.io and Hubfly space you pay for the memory you &lt;em&gt;reserve&lt;/em&gt;, not what you use, so reserving 2 GB for an app that needs 500 MB is wasted money every hour it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find your break-even point in two lines
&lt;/h2&gt;

&lt;p&gt;You don't need a spreadsheet. If you're comparing a fixed plan against consumption pricing, solve this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consumption cost = (average vCPU used × CPU price per month)
                 + (average GB of RAM held × RAM price per month)

break-even CPU   = (fixed plan price − RAM cost) ÷ CPU price per month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example: Render's $7 plan vs Railway, for an app that holds 512 MB.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Railway RAM cost: 0.5 GB × $10.14 = $5.07&lt;/li&gt;
&lt;li&gt;Room left before you hit $7: $7 − $5.07 = $1.93&lt;/li&gt;
&lt;li&gt;Break-even CPU: $1.93 ÷ $20.29 = &lt;strong&gt;0.095 vCPU&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app averages less than about 10% of one CPU, Railway is cheaper than Render's $7 plan. Above that, the fixed plan wins, as long as Render's smallest instance is enough for your peaks.&lt;/p&gt;

&lt;p&gt;To get your own numbers, check your current host's metrics page for average CPU (not peak) and average memory over a normal week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three apps, six platforms
&lt;/h2&gt;

&lt;p&gt;All prices below come from each vendor's official pricing page on &lt;strong&gt;21 Oct 2026&lt;/strong&gt;, for compute only. Bandwidth, databases and disks are left out so the comparison stays like-for-like; see the notes after each table. A "month" is 730 hours unless the vendor states otherwise (Fly.io quotes 30 days).&lt;/p&gt;

&lt;h3&gt;
  
  
  App 1: a small API that must stay on
&lt;/h3&gt;

&lt;p&gt;A customer-facing API: 512 MB of RAM, averages 0.05 vCPU, can't sleep because a cold start would make users wait.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;What you buy&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;How it's worked out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly space&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Dedicated 0.5 vCPU / 512 MB, 24/7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2.19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(0.5 × $0.004 + 0.5 × $0.002) × 730 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;shared-cpu-1x / 512 MB (iad, ewr)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$3.69&lt;/strong&gt; per 30 days&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1 vCPU / 512 MiB, fixed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; (Hobby)&lt;/td&gt;
&lt;td&gt;Measured use&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6.09&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.05 × $20.29 + 0.5 × $10.14 = $6.09, above the $5 minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;Render&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"Less than 1 CPU" / 512 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$7.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Basic dyno, 0.5 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$7.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this shows:&lt;/strong&gt; for a small, steady app, allocation pricing with low hourly rates wins, and fixed plans are close behind. Consumption pricing gives you the least advantage here because the app holds its memory all month. Note that Fly.io lists cheaper prices only for its lowest-priced regions, and DigitalOcean includes 50 GiB of outbound transfer in that $5, which matters if your API sends a lot of data.&lt;/p&gt;

&lt;h3&gt;
  
  
  App 2: an internal tool used a few hours a week
&lt;/h3&gt;

&lt;p&gt;An admin dashboard or staging app: about 50 hours awake a month, 512 MB while running, cold starts are fine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;How it's worked out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly space&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shared tier (0.3 vCPU / 256 MB), auto-sleep&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt; if it fits 256 MB&lt;/td&gt;
&lt;td&gt;Free within 360 shared-tier hours a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Measured use, sleeping when idle&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.42&lt;/strong&gt; of usage&lt;/td&gt;
&lt;td&gt;(0.05 × $0.00000772 + 0.5 × $0.00000386) × 180,000 s. Fits Railway's Free plan ($1 a month after the trial); $5 on Hobby because of the minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;shared-cpu-1x / 512 MB, auto-stop&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;about $0.26&lt;/strong&gt; + stopped disk&lt;/td&gt;
&lt;td&gt;$3.69 ÷ 720 h × 50 h; stopped Machines pay $0.15 per GB-month of rootfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1 vCPU / 512 MiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The app stays on all month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;Render&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free instance, sleeps when idle&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;750 free instance hours per workspace a month; spins down after 15 min without inbound traffic, about 1 min to wake. A paid 0.5 CPU / 512 MB instance doesn't sleep, so it's $7.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://devcenter.heroku.com/articles/eco-dyno-hours" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Eco dyno, 0.5 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flat $5 for a pool of 1,000 hours shared by all your Eco apps; sleeps after 30 min of inactivity. Basic ($7) stays on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this shows:&lt;/strong&gt; for apps that are idle most of the month, the ability to sleep matters far more than the price per hour. Every platform that sleeps keeps usage under $1, though Railway Hobby still bills its $5 minimum and Heroku Eco is a flat $5. Plans that stay on charge full price for an app used 7% of the time. On Hubfly space, a shared-tier container's cost breakdown in the dashboard reads "Shared tier is free of charge": $0.00 an hour and $0.00 a month. If the tool needs more than 256 MB, a dedicated 0.5 vCPU / 512 MB container costs $0.003 an hour while awake: about $0.15 for 50 hours with Auto Sleep on, or $2.19 left on around the clock.&lt;/p&gt;

&lt;h3&gt;
  
  
  App 3: a busy API with steady load
&lt;/h3&gt;

&lt;p&gt;A production API that holds 1 GB of RAM and averages 0.6 vCPU all day, every day.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;What you buy&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;How it's worked out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly space&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Dedicated 1 vCPU / 1 GB, 24/7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(1 × $0.004 + 1 × $0.002) × 730 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;shared-cpu-1x / 1 GB (iad, ewr)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$6.70&lt;/strong&gt; per 30 days&lt;/td&gt;
&lt;td&gt;List price. Check Fly's CPU quota rules before running a steady 60% load on a shared CPU; a dedicated-CPU performance-1x Machine comes with 2 GB and is $33.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1 vCPU / 1 GiB (not fixed CPU)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$12.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Measured use&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$22.32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.6 × $20.29 + 1 × $10.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;Render&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1 CPU / 2 GB (the smallest instance is too small)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$25.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Standard-2X, 1 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$50.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this shows:&lt;/strong&gt; once CPU stays busy, paying per vCPU-second of real use stops being a discount. At 0.6 vCPU, Railway costs almost as much as Render's next size up. Using the break-even formula against Render's $25 plan: ($25 − $10.14) ÷ $20.29 = 0.73 vCPU. Above 73% average CPU, the fixed plan is cheaper. Allocated pricing (Hubfly space, Fly.io) wins clearly here, as long as a shared CPU meets your load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each model wins
&lt;/h2&gt;

&lt;p&gt;Be honest about your app's shape before you pick a host. Here's where the other platforms do better than Hubfly space, or better than each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Railway wins on spiky traffic and odd sizes.&lt;/strong&gt; If your app idles at 0.05 vCPU and jumps to 2 vCPU for an hour a day, consumption pricing bills that hour and nothing else, without you resizing anything. It also gives you &lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;hard spending limits, preview environments and built-in database backups&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fly.io wins on global reach and scale-to-zero for any size.&lt;/strong&gt; You can stop any Machine, not just a small free tier, and its &lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;reservation blocks&lt;/a&gt; cut 40% off for a year's commitment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean wins on bundled bandwidth.&lt;/strong&gt; Each plan &lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;includes outbound transfer&lt;/a&gt; (50 GiB on the $5 plan), and overage is $0.02 per GiB. For a download-heavy app that's easier to budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render and Heroku win on predictability and maturity.&lt;/strong&gt; A fixed $7 or $25 is easy to put in a budget, and both have long track records, managed Postgres and large ecosystems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hubfly space wins on low hourly rates and a free tier that sleeps.&lt;/strong&gt; You pay $2.92 a month for a full vCPU and $1.46 per GB of RAM when running around the clock, with no seat or project fees. The trade-offs: you pay for what you allocate, so size containers carefully; and the free shared tier is small (0.3 vCPU / 256 MB).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;1. You sized for the peak, not the average.&lt;/strong&gt; On fixed and allocated platforms, a 2 GB container for an app that needs 500 MB means paying four times too much, every hour. Fix: watch memory for a week, then set the size to your normal peak plus about 25%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your "sleeping" app never sleeps.&lt;/strong&gt; Platforms decide an app is idle in different ways. Railway, for example, &lt;a href="https://docs.railway.com/reference/app-sleeping" rel="noopener noreferrer"&gt;watches outbound traffic&lt;/a&gt;, so a database connection pool, a health-check pinger or a queue poller can keep it awake and billing. Fix: close idle connections, remove external uptime pings on non-critical apps, and confirm on the metrics page that the app actually stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The bill jumps after a traffic spike.&lt;/strong&gt; That's consumption pricing doing its job, but it can still hurt. Fix: set a spending alert or limit before you need it. On Hubfly space you can set a monthly budget for each project in its settings, with alerts as you approach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway: match the model to your app's shape
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Small, steady, always on: allocated or fixed pricing. Look for the lowest hourly rate that fits your memory.&lt;/li&gt;
&lt;li&gt;Idle most of the time: anything that sleeps. The price per hour barely matters.&lt;/li&gt;
&lt;li&gt;Spiky and unpredictable: consumption pricing, with a hard limit set.&lt;/li&gt;
&lt;li&gt;Busy and steady: allocated pricing at low hourly rates, or a fixed plan sized exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read next: &lt;a href="https://blog.hubfly.space/heroku-alternatives-2026-prices?utm_source=devto&amp;amp;utm_campaign=fixed-vs-pay-as-you-go-hosting-break-even" rel="noopener noreferrer"&gt;Best Heroku alternatives in 2026: 7 options and what they cost&lt;/a&gt; and &lt;a href="https://blog.hubfly.space/heroku-vs-render-vs-railway-vs-hubfly-pricing?utm_source=devto&amp;amp;utm_campaign=fixed-vs-pay-as-you-go-hosting-break-even" rel="noopener noreferrer"&gt;Heroku vs Render vs Railway vs Hubfly space: 4 prices for one app + Postgres&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move an existing app over:&lt;/strong&gt; follow &lt;a href="https://blog.hubfly.space/migrate-from-railway?utm_source=devto&amp;amp;utm_campaign=fixed-vs-pay-as-you-go-hosting-break-even" rel="noopener noreferrer"&gt;How to move your app from Railway to Hubfly space, step by step&lt;/a&gt;, including copying your Postgres data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run your whole stack in one place:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/deploy-docker-compose-as-is-8-platforms-prices?utm_source=devto&amp;amp;utm_campaign=fixed-vs-pay-as-you-go-hosting-break-even" rel="noopener noreferrer"&gt;deploy a docker-compose.yml as-is&lt;/a&gt; with web, Postgres and Redis on private networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price your own app:&lt;/strong&gt; use the two rates on the &lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly space pricing page&lt;/a&gt; ($0.004 per vCPU-hour, $0.002 per GB-hour) with the formula above. Storage is $0.011 per GB-month on the Standard tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Price your app on Hubfly space:&lt;/strong&gt; &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;start free on the shared tier&lt;/a&gt; (360 hours a month, no card), then switch a container to dedicated when it needs to stay on.&lt;/p&gt;

</description>
      <category>hosting</category>
      <category>devops</category>
      <category>pricing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to get a managed PostgreSQL database for about $2 a month</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:05:13 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/how-to-get-a-managed-postgresql-database-for-about-2-a-month-3bi8</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/how-to-get-a-managed-postgresql-database-for-about-2-a-month-3bi8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/managed-postgresql-database-2-dollars-a-month?utm_source=devto&amp;amp;utm_campaign=managed-postgresql-database-2-dollars-a-month" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Running Postgres yourself means patching it, guarding the port and rotating passwords, or leaving your data on an app volume one bad redeploy from trouble.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a PostgreSQL 18 database that Hubfly space runs for you, with TLS on every connection, an IP allowlist, pgvector ready to install, and a connection string written straight into your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; side projects, small SaaS apps and AI features that need Postgres but not a database admin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time needed:&lt;/strong&gt; about 10 minutes. The database itself is ready in about a minute and a half.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; about $2.02 a month for the smallest size. You can stop it and pay about $0.11 a month for storage.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this guide you'll create a managed PostgreSQL database on Hubfly space, add vector search, lock it to your network and connect an app without copying a password anywhere. You'll know exactly what Hubfly space runs for you, what it still leaves to you, and what it costs to the cent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hubfly space runs, and what you still own
&lt;/h2&gt;

&lt;p&gt;Managed Databases are a new project type on Hubfly space. One managed project can hold several databases (PostgreSQL, MySQL or SQLite through libSQL) and Realtime servers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hubfly space does this&lt;/th&gt;
&lt;th&gt;You still do this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Creates the server and its storage, picks where it runs&lt;/td&gt;
&lt;td&gt;Choose the size and grow storage when you need it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generates and encrypts passwords, refuses connections without TLS&lt;/td&gt;
&lt;td&gt;Decide who may connect (IP allowlist, one user per app)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start, stop, restart, resize, version updates, config changes with automatic rollback&lt;/td&gt;
&lt;td&gt;Keep your own copies of the data (see the limits below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics, logs, sessions, query insights and alerts&lt;/td&gt;
&lt;td&gt;Read them and act on them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You get an &lt;code&gt;admin&lt;/code&gt; user that can create databases and users and read and write everything. It is not a superuser, so anything that needs superuser rights won't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a managed project
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;, give it a name like &lt;code&gt;shop-data&lt;/code&gt;, and pick &lt;strong&gt;Managed service&lt;/strong&gt;. There's no region to choose: Hubfly space places the database for you. Open &lt;strong&gt;Advanced Options&lt;/strong&gt; and set a monthly budget if you want a spending alarm on this project.&lt;/p&gt;

&lt;p&gt;The project opens on a page with three cards: Database, Realtime and Cache (Cache is marked Coming soon). Click &lt;strong&gt;Create a database&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Pick the engine, size and name
&lt;/h2&gt;

&lt;p&gt;The form has one section per decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engine and version:&lt;/strong&gt; PostgreSQL 18 is the default (17 and 16 are also offered).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; &lt;strong&gt;Nano&lt;/strong&gt; (0.25 vCPU, 512 MiB) is enough for a prototype or a small app. PostgreSQL needs at least 512 MiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; 10 GiB by default, from 1 to 500 GiB. It can grow later but never shrink, so start small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network &amp;amp; access:&lt;/strong&gt; public access is on, and the allowlist is empty. You'll lock it down in Step 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance name:&lt;/strong&gt; for example &lt;code&gt;shop-db&lt;/code&gt;. It becomes part of the hostname.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The price panel updates as you change the form and splits the cost into compute, storage and management:&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create database&lt;/strong&gt;. The page lists nine provisioning steps as they run, from "Reserving capacity" to "Registering hostname". Expect about a minute and a half until the status turns &lt;strong&gt;Running&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The header shows your hostname and port, such as &lt;code&gt;shop-db-xxxx.db.hubfly.app:30010&lt;/code&gt;. Note the port: it is assigned to your instance and it is &lt;strong&gt;not&lt;/strong&gt; 5432. Copy it from the page rather than guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Get your connection string
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Connect&lt;/strong&gt;. It shows the host, port, database (&lt;code&gt;app&lt;/code&gt;), user (&lt;code&gt;admin&lt;/code&gt;) and a hidden password. &lt;strong&gt;Reveal&lt;/strong&gt; shows the password for a minute, and every reveal is written to the access log, so you can see who looked at it.&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Connection string and snippets&lt;/strong&gt; you can copy a ready-made string for URI, &lt;code&gt;.env&lt;/code&gt;, psql, Prisma, Drizzle, Node (pg), Python, Go (pgx), JDBC or PHP (PDO). All of them include &lt;code&gt;sslmode=require&lt;/code&gt;, because the database refuses unencrypted connections. To also verify the server's identity, download the instance's CA certificate from the same page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add vector search with pgvector
&lt;/h2&gt;

&lt;p&gt;If you're building search, recommendations or an AI feature, you can keep your embeddings next to the rest of your data. Open &lt;strong&gt;Extensions&lt;/strong&gt;, find &lt;strong&gt;vector&lt;/strong&gt; (pgvector 0.8.7), click &lt;strong&gt;Install&lt;/strong&gt; and confirm. PostGIS, pg_trgm, pgcrypto, uuid-ossp and pg_stat_statements are on the same list.&lt;/p&gt;

&lt;p&gt;Now open the &lt;strong&gt;SQL editor&lt;/strong&gt;. It opens in read-only mode every time, which protects you from a stray &lt;code&gt;DELETE&lt;/code&gt;. Turn &lt;strong&gt;Read-only&lt;/strong&gt; off, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'red running shoes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[0.9,0.1,0.0]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blue trail shoes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'[0.8,0.2,0.1]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wool winter hat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'[0.0,0.1,0.9]'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[1,0,0]'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[1,0,0]'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each statement gets its own result tab. The query returns the two shoes, closest first:&lt;/p&gt;

&lt;p&gt;Real embeddings have hundreds of dimensions instead of 3, but the query is the same. The editor works through Hubfly space, so it needs no allowlist entry and no local client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Lock it to your network
&lt;/h2&gt;

&lt;p&gt;An empty allowlist means anyone with the password can try to connect from anywhere. Open &lt;strong&gt;Network&lt;/strong&gt;, click &lt;strong&gt;Add entry&lt;/strong&gt;, and add your office or server range, for example &lt;code&gt;203.0.113.0/24&lt;/code&gt; with the label &lt;code&gt;office&lt;/code&gt;. &lt;strong&gt;Add my IP&lt;/strong&gt; fills in the address you're browsing from. Click &lt;strong&gt;Save allowlist&lt;/strong&gt;. The change applies in about 10 seconds and the status changes to "Public, limited to 1 allowed range".&lt;/p&gt;

&lt;p&gt;You can add up to 64 addresses or CIDR ranges. Hubfly space enforces the list in its proxy, before a connection reaches Postgres.&lt;/p&gt;

&lt;p&gt;One catch: connected Cell apps reach the database through the same public endpoint, so the allowlist applies to them too. After you restrict the list, add the addresses your apps connect from and confirm they still connect. Keep &lt;strong&gt;Public access&lt;/strong&gt; on while apps use the connection string, because it points at the public endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Connect your app without copying passwords
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Apps&lt;/strong&gt; and click &lt;strong&gt;Connect an app&lt;/strong&gt;. Pick the Cell project that runs your app, keep the variable name &lt;code&gt;DATABASE_URL&lt;/code&gt;, and leave &lt;strong&gt;Create a dedicated user&lt;/strong&gt; ticked with &lt;strong&gt;Read &amp;amp; write&lt;/strong&gt;. Click &lt;strong&gt;Connect app&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hubfly space creates a separate database user for the app and writes the full connection string into the project as a &lt;strong&gt;Secret&lt;/strong&gt; variable:&lt;/p&gt;

&lt;p&gt;Why a dedicated user? If you ever rotate the admin password, this app keeps working. If you disconnect the app, its user is dropped. &lt;strong&gt;Redeploy the app&lt;/strong&gt; after connecting: apps read variables at deploy time. Deleting the database later also removes &lt;code&gt;DATABASE_URL&lt;/code&gt; from the project, so reconnect or redeploy the app with a new database first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Managed Databases use the same rates as dedicated Cell resources, plus a 30% management fee on CPU and memory while the database runs. Monthly figures use 730 hours.&lt;/p&gt;

&lt;p&gt;For Nano with 10 GiB of storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute: 0.25 vCPU × $0.004 + 0.5 GiB × $0.002 = $0.002 per hour, plus the 1 GiB system disk at $0.00001536 per GB-hour = $0.00201536&lt;/li&gt;
&lt;li&gt;Management: 30% × $0.002 = $0.0006 per hour&lt;/li&gt;
&lt;li&gt;Storage: 10 GiB × $0.00001536 per GB-hour = $0.0001536 per hour&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $0.00276896 per hour × 730 = about $2.02 a month&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;vCPU / memory&lt;/th&gt;
&lt;th&gt;Per month (10 GiB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nano&lt;/td&gt;
&lt;td&gt;0.25 / 512 MiB&lt;/td&gt;
&lt;td&gt;$2.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;0.5 / 1 GiB&lt;/td&gt;
&lt;td&gt;$3.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;1 / 2 GiB&lt;/td&gt;
&lt;td&gt;$7.72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A stopped database pays only for storage: about $0.11 a month for 10 GiB. The database is billed from your account balance. It isn't part of the free tier's 360 hours. If your balance runs out, Hubfly space stops the database and starts it again after you top up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No automatic backups yet.&lt;/strong&gt; There are no snapshots, replicas or point-in-time recovery. One-click exports live on the &lt;strong&gt;Backups &amp;amp; exports&lt;/strong&gt; page. If it shows "Exports are not available for this instance yet.", take your own dumps with your usual Postgres tools over the TLS connection and keep them somewhere you control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One server in one location, chosen by Hubfly space.&lt;/strong&gt; No high availability. A restart or version update briefly drops connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is NOT for
&lt;/h2&gt;

&lt;p&gt;Skip it for now if losing the last few hours of data would hurt your business, or if you need replicas, a specific region for data-residency rules, or superuser access. Pick a provider with point-in-time recovery until automatic backups arrive.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"cannot execute CREATE TABLE in a read-only transaction" (error 25006).&lt;/strong&gt; The SQL editor starts in read-only mode. Switch &lt;strong&gt;Read-only&lt;/strong&gt; off before writing, and switch it back when you're done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your client or app times out.&lt;/strong&gt; Check the port (it isn't 5432) and the allowlist. Once the list has entries, only those ranges can connect, your laptop and your apps included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your app still can't see &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/strong&gt; Apps read variables at deploy time. Redeploy after you connect, disconnect or rotate credentials.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run untrusted or AI-generated code next to it with &lt;a href="https://blog.hubfly.space/sandbox-ai-agents-container-per-task?utm_source=devto&amp;amp;utm_campaign=managed-postgresql-database-2-dollars-a-month" rel="noopener noreferrer"&gt;one container per task&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add live updates with Realtime, a Pusher-compatible WebSocket server you create from the same managed project (&lt;a href="https://www.hubfly.space/docs/" rel="noopener noreferrer"&gt;docs home&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Store product images on &lt;a href="https://www.hubfly.space/docs/storage/assets-and-images/" rel="noopener noreferrer"&gt;Storage&lt;/a&gt; and resize them with a URL.&lt;/li&gt;
&lt;li&gt;Ship every push automatically with &lt;a href="https://blog.hubfly.space/deploy-on-push-github-actions-webhook?utm_source=devto&amp;amp;utm_campaign=managed-postgresql-database-2-dollars-a-month" rel="noopener noreferrer"&gt;GitHub Actions and a deploy webhook&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Read the full &lt;a href="https://www.hubfly.space/docs/databases/" rel="noopener noreferrer"&gt;Managed Databases guide&lt;/a&gt; for MySQL, libSQL tokens and configuration changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start free
&lt;/h2&gt;

&lt;p&gt;Create a free account (no card needed, 360 hours of shared compute a month for your app), add balance when you're ready for the database, and have Postgres with pgvector running in about 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space: start free →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deploy your own MCP server with HTTPS and an access key</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:20:04 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/deploy-your-own-mcp-server-with-https-and-an-access-key-fli</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/deploy-your-own-mcp-server-with-https-and-an-access-key-fli</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/deploy-mcp-server-https-access-key?utm_source=devto&amp;amp;utm_campaign=deploy-mcp-server-https-access-key" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; your own MCP server on a public HTTPS address, locked with an access key, and connected to Claude Code, Cursor and Codex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; developers who built (or want to build) an MCP server and are tired of it only working on &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 15 minutes. &lt;strong&gt;Cost:&lt;/strong&gt; free on the shared tier (360 compute hours a month, no card); about $2.25 a month always on (dedicated 0.5 vCPU, 512 MB).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your MCP server works great on your laptop. Then you open Cursor on your other machine, or a teammate asks for it, and it's gone. Local servers die when your laptop sleeps, and you end up copying the same setup to every computer.&lt;/p&gt;

&lt;p&gt;In this guide you'll put one MCP server online, behind HTTPS and an access key, and point every AI tool you use at the same address. One server, one place to update it, and nobody else can call your tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space →&lt;/strong&gt;&lt;/a&gt; (free tier, no card needed)&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code ─┐
Cursor ──────┼──► https://your-server.eu1.hubfly.app/mcp ──► access key check ──► your MCP server (Python)
Codex ───────┘                                              (at the edge)         (Hubfly space Cell container)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small Python MCP server with two tools, &lt;code&gt;add_note&lt;/code&gt; and &lt;code&gt;list_notes&lt;/code&gt;. Swap in your own tools later; the deploy steps stay the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write the server
&lt;/h2&gt;

&lt;p&gt;Install the official Python SDK on your machine to try it locally first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"mcp==2.3.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;server.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MCPServer&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Save a short note.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved note #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_notes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return every saved note.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable-http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;transport="streamable-http"&lt;/code&gt;&lt;/strong&gt; makes the server speak MCP over HTTP instead of stdin/stdout. Remote clients can only reach an HTTP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;host="0.0.0.0"&lt;/code&gt;&lt;/strong&gt; makes it listen on every network interface. The SDK's default is &lt;code&gt;127.0.0.1&lt;/code&gt;, which only accepts connections from inside the container itself, so the platform's proxy can't reach it and you get a 502 error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server answers on &lt;code&gt;/mcp&lt;/code&gt;, so locally it lives at &lt;code&gt;http://127.0.0.1:8000/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on the SDK version:&lt;/strong&gt; version 2 of the &lt;code&gt;mcp&lt;/code&gt; package renamed &lt;code&gt;FastMCP&lt;/code&gt; to &lt;code&gt;MCPServer&lt;/code&gt;. Many tutorials still use &lt;code&gt;from mcp.server.fastmcp import FastMCP&lt;/code&gt;, which fails on 2.x. Pin the version so a future release doesn't change things under you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Add a Dockerfile
&lt;/h2&gt;

&lt;p&gt;Put this &lt;code&gt;Dockerfile&lt;/code&gt; next to &lt;code&gt;server.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="s2"&gt;"mcp==2.3.0"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; server.py .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "server.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing the package during the build (not at startup) keeps restarts fast: the container starts the server straight away instead of downloading packages each time.&lt;/p&gt;

&lt;p&gt;Push both files to a GitHub repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Deploy it on Hubfly space
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Hubfly space dashboard&lt;/a&gt; and click &lt;strong&gt;Create Project&lt;/strong&gt;. Give it a name like &lt;code&gt;notes-mcp&lt;/code&gt;, pick a region close to you, and (optional) open &lt;strong&gt;Advanced Options&lt;/strong&gt; to set a monthly budget cap, for example $10.&lt;/li&gt;
&lt;li&gt;In the project, click &lt;strong&gt;Create → Create Container&lt;/strong&gt;. Name it &lt;code&gt;mcp-server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Deployment Source&lt;/strong&gt;, choose &lt;strong&gt;Git repository&lt;/strong&gt;, connect GitHub through the GitHub App, and pick your repository. A &lt;code&gt;Dockerfile&lt;/code&gt; in the repository root is used for the build.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Resources&lt;/strong&gt;, keep &lt;strong&gt;Shared Tier (Free)&lt;/strong&gt;: 0.3 vCPU and 256 MB RAM, enough for a small server.&lt;/li&gt;
&lt;li&gt;Click through to &lt;strong&gt;Review&lt;/strong&gt; and press &lt;strong&gt;Deploy Container&lt;/strong&gt;. The container page shows each deploy stage as it runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Open the &lt;strong&gt;Logs&lt;/strong&gt; tab. When you see this line, the server is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Give it a public HTTPS address
&lt;/h2&gt;

&lt;p&gt;Right now the server only exists on the private network. To reach it from your laptop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the container's &lt;strong&gt;Network&lt;/strong&gt; tab and click &lt;strong&gt;Add Domain&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;Auto Endpoint&lt;/strong&gt;. Under &lt;strong&gt;Suggested ports&lt;/strong&gt;, the live scan shows &lt;strong&gt;8000 · Open now&lt;/strong&gt;. Click it.&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;TLS / SSL&lt;/strong&gt; and &lt;strong&gt;Force HTTPS&lt;/strong&gt; on, and click &lt;strong&gt;Create Subdomain&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The address is named after your container, for example &lt;code&gt;https://mcp-server.eu1.hubfly.app&lt;/code&gt;. Your MCP endpoint is that address plus &lt;code&gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check that it answers. This is the MCP "hello" (the &lt;code&gt;initialize&lt;/code&gt; call):&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;-i&lt;/span&gt; https://&amp;lt;your-address&amp;gt;/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get &lt;code&gt;HTTP 200&lt;/code&gt;, an &lt;code&gt;mcp-session-id&lt;/code&gt; header and a reply that starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The round trip takes well under a second once the server is awake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't stop here.&lt;/strong&gt; At this point anyone who finds the address can call your tools. Public addresses get visits from scanners within minutes of being created, so lock it before you connect anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Lock it with an access key
&lt;/h2&gt;

&lt;p&gt;Hubfly space can check an access key at the edge, before the request ever reaches your container. Your Python code doesn't change.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to the project overview and click the &lt;strong&gt;Firewall&lt;/strong&gt; box above your container. This opens &lt;strong&gt;Security &amp;amp; Protection Studio&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check that your domain is selected in the domain picker, then open the &lt;strong&gt;Identity Access Control&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Access Authentication Mode&lt;/strong&gt;, choose &lt;strong&gt;API Key Only&lt;/strong&gt;, switch &lt;strong&gt;Policy Enforcement State&lt;/strong&gt; on, and click &lt;strong&gt;Save Policy Settings&lt;/strong&gt;. The status changes from &lt;em&gt;Permissive&lt;/em&gt; to &lt;em&gt;Enforced&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;API Keys&lt;/strong&gt; tab. Enter a name (for example &lt;code&gt;laptop-clients&lt;/code&gt;), leave &lt;strong&gt;Allowed Paths&lt;/strong&gt; empty, optionally set an expiry date, and click &lt;strong&gt;Generate Secret Key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the key (it starts with &lt;code&gt;hfa_live_&lt;/code&gt;). &lt;strong&gt;It is shown only once&lt;/strong&gt;, so save it in your password manager right away.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now run the &lt;code&gt;curl&lt;/code&gt; from Step 4 again without a key. You get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt;
&lt;span class="na"&gt;{"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"identity_access_required","request_id":"..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A made-up key gets the same 401. These requests are stopped at the edge: they never show up in your container's logs, so a flood of bad requests doesn't cost your server any work.&lt;/p&gt;

&lt;p&gt;To call it with the key, add one header:&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="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-HubFly-Access-Key: &lt;/span&gt;&lt;span class="nv"&gt;$MCP_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the key in an environment variable or your password manager, never in a Git repository. Issue one key per person or machine, with an expiry date, so you can retire one without breaking the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Connect Claude Code, Cursor and Codex
&lt;/h2&gt;

&lt;p&gt;First, put the key in your shell so it stays out of config files:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MCP_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hfa_live_..."&lt;/span&gt;   &lt;span class="c"&gt;# paste your key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (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;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http notes https://&amp;lt;your-address&amp;gt;/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"X-HubFly-Access-Key: &lt;/span&gt;&lt;span class="nv"&gt;$MCP_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;/mcp&lt;/code&gt; inside Claude Code to see the server and its two tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor:&lt;/strong&gt; add this to &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; (all projects) or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; (one project):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-address&amp;gt;/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"X-HubFly-Access-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${env:MCP_KEY}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Codex:&lt;/strong&gt; add this to &lt;code&gt;~/.codex/config.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.notes]&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://&amp;lt;your-address&amp;gt;/mcp"&lt;/span&gt;
&lt;span class="py"&gt;env_http_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;"X-HubFly-Access-Key"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MCP_KEY"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;env_http_headers&lt;/code&gt; reads the value from the &lt;code&gt;MCP_KEY&lt;/code&gt; environment variable (use &lt;code&gt;http_headers&lt;/code&gt; instead for a fixed value). The key name matters: Codex's setting is &lt;code&gt;http_headers&lt;/code&gt;/&lt;code&gt;env_http_headers&lt;/code&gt;, not the &lt;code&gt;headers&lt;/code&gt; you use in Cursor, and a request without the key gets a 401.&lt;/p&gt;

&lt;p&gt;Ask your client to list its MCP tools; you should see &lt;code&gt;add_note&lt;/code&gt; and &lt;code&gt;list_notes&lt;/code&gt;. Then try "add a note: buy oat milk" followed by "list my notes". Every client you connect uses the same server, so they share the same notes. If a client reports 401, the header name or key is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about claude.ai and Claude Desktop connectors?&lt;/strong&gt; Custom connectors there support OAuth or no auth. Fixed headers are a limited beta for some organizations, and a custom header name like &lt;code&gt;X-HubFly-Access-Key&lt;/code&gt; needs approval from Anthropic first. For those apps, plan on OAuth.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;421 Misdirected Request&lt;/code&gt; with "Invalid Host header".&lt;/strong&gt; This happens if you run the server with Uvicorn directly (&lt;code&gt;uvicorn.run(mcp.streamable_http_app(), ...)&lt;/code&gt;) instead of &lt;code&gt;mcp.run(...)&lt;/code&gt;. In that mode the SDK turns on DNS-rebinding protection and only accepts &lt;code&gt;Host: 127.0.0.1&lt;/code&gt; or &lt;code&gt;localhost&lt;/code&gt;, so every request through your public address is refused. Fix it by allowing your address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.transport_security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TransportSecuritySettings&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamable_http_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;transport_security&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;TransportSecuritySettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;allowed_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-server.eu1.hubfly.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# your address
&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;Note that once you set &lt;code&gt;allowed_hosts&lt;/code&gt;, local requests to &lt;code&gt;127.0.0.1&lt;/code&gt; are refused with the same 421, so add &lt;code&gt;"127.0.0.1:8000"&lt;/code&gt; to the list if you also run it locally. The &lt;code&gt;mcp.run(transport="streamable-http", host="0.0.0.0")&lt;/code&gt; setup from Step 1 doesn't hit this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;502 Bad Gateway&lt;/code&gt; from the address.&lt;/strong&gt; The server is listening on &lt;code&gt;127.0.0.1&lt;/code&gt;, usually because &lt;code&gt;host="0.0.0.0"&lt;/code&gt; is missing. Add it and redeploy. If &lt;strong&gt;Suggested ports&lt;/strong&gt; in the Add Domain dialog shows nothing, the server isn't listening yet: check the Logs tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The client times out on the first call of the day.&lt;/strong&gt; Shared-tier containers suspend after 15 minutes of inactivity, and the first call after a pause can be slow. Some clients give up quickly (Codex waits 10 seconds by default; raise it with &lt;code&gt;startup_timeout_sec = 30&lt;/code&gt; in the same TOML block). If you use the server all day, move it to the dedicated tier with &lt;strong&gt;Auto Sleep&lt;/strong&gt; off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus: notes disappear after a redeploy.&lt;/strong&gt; This example keeps notes in memory, and everything outside a volume is wiped when the container restarts or redeploys. For real data, use a database (see below). Also keep an eye on memory: this small server already uses most of the shared tier's 256 MB, so heavier tools need a dedicated container.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared tier:&lt;/strong&gt; free within the 360 compute hours a month included in the free tier. The project's budget panel shows a projected &lt;strong&gt;$0.00/month&lt;/strong&gt; for this setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always on (dedicated, 0.5 vCPU, 512 MB RAM, 5 GB disk, Auto Sleep off):&lt;/strong&gt; about &lt;strong&gt;$2.25 a month&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;CPU: 0.5 × $0.004 × 730 h = $1.46&lt;/li&gt;
&lt;li&gt;RAM: 0.5 GB × $0.002 × 730 h = $0.73&lt;/li&gt;
&lt;li&gt;Disk: 5 GB × $0.00001536 × 730 h = $0.06&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The access key check, the HTTPS address and the certificate are free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Getting an MCP server off &lt;code&gt;localhost&lt;/code&gt; takes four things: speak HTTP (&lt;code&gt;streamable-http&lt;/code&gt;), listen on &lt;code&gt;0.0.0.0&lt;/code&gt;, give it an HTTPS address, and check a key before requests reach your code. Do those once and every AI tool you use shares the same tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give your tools real data:&lt;/strong&gt; add a PostgreSQL database from the templates with a volume so data survives redeploys, and connect to it over the private network. See the &lt;a href="https://www.hubfly.space/docs/compose/" rel="noopener noreferrer"&gt;Compose docs&lt;/a&gt; and &lt;a href="https://blog.hubfly.space/docker-health-checks-postgres-redis?utm_source=devto&amp;amp;utm_campaign=deploy-mcp-server-https-access-key" rel="noopener noreferrer"&gt;Docker health checks that catch real problems&lt;/a&gt; for a Compose setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let your team in with their login instead of keys:&lt;/strong&gt; switch the route to &lt;strong&gt;Login or API Key&lt;/strong&gt; so teammates sign in in the browser while tools keep using keys. See &lt;a href="https://www.hubfly.space/docs/identity-access/" rel="noopener noreferrer"&gt;Identity Access&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let your coding agent deploy for you:&lt;/strong&gt; the &lt;a href="https://www.hubfly.space/ai" rel="noopener noreferrer"&gt;Hubfly space AI skill&lt;/a&gt; teaches Claude Code, Codex and Cursor to deploy and update projects with the &lt;code&gt;hubfly&lt;/code&gt; CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space →&lt;/strong&gt;&lt;/a&gt; Free tier, 360 compute hours a month, no card.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to deploy on every push with GitHub Actions and a webhook</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:18:48 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/how-to-deploy-on-every-push-with-github-actions-and-a-webhook-2dn0</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/how-to-deploy-on-every-push-with-github-actions-and-a-webhook-2dn0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/deploy-on-push-github-actions-webhook?utm_source=devto&amp;amp;utm_campaign=deploy-on-push-github-actions-webhook" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; every push to &lt;code&gt;main&lt;/code&gt; redeploys your container on Hubfly space, and you can send a rollback action when a release goes bad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; developers who build images or test code in GitHub Actions (or GitLab CI) and still redeploy by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 15 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; deploy webhooks are free. You pay only for the container itself; the shared tier is free for 360 hours a month.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your app is live, but every change still ends with you opening the dashboard and clicking Redeploy. That's slow, and it's how the wrong build reaches your users on a Friday evening. In this guide you'll connect GitHub Actions to Hubfly space with a signed deploy webhook, so every push to &lt;code&gt;main&lt;/code&gt; goes live on its own once your tests pass, and you can send a rollback when a release goes bad.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Try it on Hubfly space. Start free, 360 hours included →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a deploy webhook does
&lt;/h2&gt;

&lt;p&gt;A deploy webhook is an address your CI calls when a build is ready. Hubfly space then does one action on your container: pull the new image or commit, update its settings, restart it, or roll it back. Pushing an image to a registry never redeploys anything by itself, so the webhook is the missing "now ship it" step.&lt;/p&gt;

&lt;p&gt;Each webhook has three settings that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How Hubfly space checks the caller&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 for anything real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Allowed actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What the caller may do&lt;/td&gt;
&lt;td&gt;Only what your pipeline needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP allowlist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Where calls may come from&lt;/td&gt;
&lt;td&gt;Your fixed runner IPs, if you have them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three security modes differ a lot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;How the call is checked&lt;/th&gt;
&lt;th&gt;Actions the dashboard offers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Token inside the URL&lt;/td&gt;
&lt;td&gt;Image Pull only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;Token inside the URL&lt;/td&gt;
&lt;td&gt;Image Pull, Provisioning, Hard Restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC-SHA256 (secure)&lt;/td&gt;
&lt;td&gt;Signed timestamp + body, no token in the URL&lt;/td&gt;
&lt;td&gt;Every action, including Config Update and Rollback Recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why it matters: with Basic or Standard, anyone who sees the URL (a leaked build log is enough) can trigger it. With HMAC-SHA256 the URL alone is useless. Each request must be signed with a secret, and the signature expires after five minutes, so a captured request can't be replayed later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the webhook
&lt;/h2&gt;

&lt;p&gt;Open your project, then the &lt;strong&gt;Pipeline&lt;/strong&gt; tab, and click &lt;strong&gt;Create webhook&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give it a clear &lt;strong&gt;Webhook name&lt;/strong&gt;, such as &lt;code&gt;GitHub Actions deploy&lt;/code&gt;, and an optional description.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Target Container Context&lt;/strong&gt;, pick the container this webhook may deploy. Leave it on &lt;strong&gt;All Containers (Project Scope)&lt;/strong&gt; only if one pipeline deploys several services; then every request must say which container it means.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Next Step&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose &lt;strong&gt;HMAC-SHA256&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under &lt;strong&gt;Allowed Operation Permissions&lt;/strong&gt;, turn on &lt;strong&gt;Image Pull&lt;/strong&gt; and &lt;strong&gt;Rollback Recovery&lt;/strong&gt;. Add &lt;strong&gt;Config Update&lt;/strong&gt; only if your pipeline also changes environment variables. Leave &lt;strong&gt;Deprovisioning&lt;/strong&gt; off: a deploy pipeline never needs to delete anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your CI runs on a machine with a fixed public IP (a self-hosted runner, a build server), enter it under &lt;strong&gt;IP Allowlist Restriction&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Register Webhook&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A dialog then shows the &lt;strong&gt;HMAC Signing Secret Key&lt;/strong&gt; and the &lt;strong&gt;Deployment Pipeline URL Target&lt;/strong&gt;. The secret is shown once. Copy both now; if you lose the secret, delete the webhook and create a new one. Webhooks can't be edited after you create them, so changing the allowed actions also means creating a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Store the values in GitHub
&lt;/h2&gt;

&lt;p&gt;In your repository, open &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; and add two repository secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HUBFLY_WEBHOOK_URL&lt;/code&gt;: the URL you copied (it looks like &lt;code&gt;https://api.hubfly.space/api/webhooks/deploy/whk_…&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HUBFLY_WEBHOOK_SECRET&lt;/code&gt;: the signing secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secrets are masked in logs, which keeps the signing key out of your build output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Sign and send the request
&lt;/h2&gt;

&lt;p&gt;Add this workflow as &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;. The &lt;code&gt;deploy&lt;/code&gt; job waits for &lt;code&gt;test&lt;/code&gt;, so a failed test never reaches production. Replace the test step with your own.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Hubfly space&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "run your tests here"&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Trigger Hubfly space deployment&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;HUBFLY_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HUBFLY_WEBHOOK_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;HUBFLY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HUBFLY_WEBHOOK_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;COMMIT_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;body=$(printf '{"action":"pull","ref":"main","commitSha":"%s"}' "$COMMIT_SHA")&lt;/span&gt;
          &lt;span class="s"&gt;timestamp=$(date +%s)&lt;/span&gt;
          &lt;span class="s"&gt;signature=$(printf '%s.%s' "$timestamp" "$body" \&lt;/span&gt;
            &lt;span class="s"&gt;| openssl dgst -sha256 -hmac "$HUBFLY_WEBHOOK_SECRET" -hex | sed 's/^.* //')&lt;/span&gt;
          &lt;span class="s"&gt;curl --fail-with-body -X POST "$HUBFLY_WEBHOOK_URL" \&lt;/span&gt;
            &lt;span class="s"&gt;-H 'Content-Type: application/json' \&lt;/span&gt;
            &lt;span class="s"&gt;-H "X-Hub-Timestamp: $timestamp" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "X-Hub-Signature-256: sha256=$signature" \&lt;/span&gt;
            &lt;span class="s"&gt;--data-binary "$body"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each line does, in plain words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;body&lt;/code&gt; is the exact JSON you send. For a Git-backed container it names the branch and commit. For an image-based container, send &lt;code&gt;{"action":"pull","image":"ghcr.io/your-org/your-app:sha-7f3b1a9"}&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt; is the current Unix time. It's part of the signature, which is what stops old requests from being replayed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signature&lt;/code&gt; is an HMAC-SHA256 of &lt;code&gt;timestamp.body&lt;/code&gt;, with the dot. Hubfly space computes the same value with your secret and compares.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--data-binary&lt;/code&gt; sends the body byte for byte. Change a single space after signing and the signature no longer matches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--fail-with-body&lt;/code&gt; makes the job fail, and print the reason, if Hubfly space refuses the call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a self-hosted runner, keep in mind that &lt;code&gt;openssl -hmac&lt;/code&gt; puts the secret on the command line, where other users on that machine could see it in the process list. Use a dedicated runner user.&lt;/p&gt;

&lt;p&gt;For a project-scoped webhook, add &lt;code&gt;"containerId":"&amp;lt;your container ID&amp;gt;"&lt;/code&gt; to the body.&lt;/p&gt;

&lt;p&gt;Deploy an immutable tag (&lt;code&gt;sha-7f3b1a9&lt;/code&gt;) or a commit SHA rather than &lt;code&gt;latest&lt;/code&gt;. Then you always know which build is running, and a rollback has something specific to go back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Watch it, and roll back when needed
&lt;/h2&gt;

&lt;p&gt;Each call shows up under &lt;strong&gt;Pipeline → Recent deployments&lt;/strong&gt; with its action, trigger, status, duration and any error. The webhook card counts &lt;strong&gt;Triggers&lt;/strong&gt; and shows the &lt;strong&gt;Last Run&lt;/strong&gt;. You can also list the history with &lt;code&gt;GET /api/v1/projects/&amp;lt;project-id&amp;gt;/deployments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a release is bad, send the rollback action with the same signing step:&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="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"action":"rollback"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback is an explicit action: send it yourself, from a manual workflow or an incident script. When you send an &lt;code&gt;update&lt;/code&gt;, the docs also list rollback options for it: &lt;code&gt;rollback.enabled&lt;/code&gt;, &lt;code&gt;rollback.onHealthCheckFail&lt;/code&gt; and a positive &lt;code&gt;rollback.timeoutSeconds&lt;/code&gt;, meant to revert the update if the new version fails its health check. A rollback needs a previous version to go back to, and a health-check trigger needs a health check, so always open the deployment event to confirm what actually happened.&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;You see&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrong or missing URL token (Basic/Standard)&lt;/td&gt;
&lt;td&gt;Copy the full URL again. Never add a token to a secure URL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;400&lt;/code&gt; on a secure webhook&lt;/td&gt;
&lt;td&gt;Bad signature, a timestamp older than five minutes, or the body changed after signing&lt;/td&gt;
&lt;td&gt;Sign &lt;code&gt;timestamp.body&lt;/code&gt; and send the same bytes with &lt;code&gt;--data-binary&lt;/code&gt;. Check the runner's clock.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The action isn't allowed, or the caller's IP isn't on the allowlist&lt;/td&gt;
&lt;td&gt;Create a new webhook with the right actions or IPs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Container is missing"&lt;/td&gt;
&lt;td&gt;Project-scoped webhook called without &lt;code&gt;containerId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;containerId&lt;/code&gt; to the body.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image pushed, nothing changed&lt;/td&gt;
&lt;td&gt;A registry push never redeploys by itself&lt;/td&gt;
&lt;td&gt;Call the webhook with &lt;code&gt;{"action":"pull"}&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limits and pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks are free.&lt;/strong&gt; There's no charge per webhook or per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The container is what you pay for.&lt;/strong&gt; The shared tier (0.3 vCPU, 256 MB) is free for 360 runtime hours a month and sleeps when idle. A dedicated container with 1 vCPU and 512 MB running all month costs 1 × $0.004 × 730 + 0.5 × $0.002 × 730 = $2.92 + $0.73 = &lt;strong&gt;$3.65 a month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five-minute window&lt;/strong&gt; for secure signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One action per request&lt;/strong&gt;, and only the actions you allowed when you created the webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No edits:&lt;/strong&gt; to change a webhook's mode, actions or IPs, create a new one and delete the old one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is not for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Teams on GitHub-hosted runners who want an IP allowlist.&lt;/strong&gt; Those runners don't have fixed addresses, so the allowlist only fits self-hosted runners or a fixed-IP build server. Use the signature alone, or move the deploy job to a self-hosted runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone who wants Hubfly space to build and deploy on every push with no CI at all.&lt;/strong&gt; Connect the repo directly instead: Git deploys can deploy on push, or after CI succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams that want automatic rollback without health checks.&lt;/strong&gt; Automatic rollback on an update depends on a health check (&lt;code&gt;rollback.onHealthCheckFail&lt;/code&gt;). Add one first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelines that need preview environments per pull request.&lt;/strong&gt; A webhook deploys to existing containers; it doesn't create a new environment for each branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/docker-health-checks-postgres-redis?utm_source=devto&amp;amp;utm_campaign=deploy-on-push-github-actions-webhook" rel="noopener noreferrer"&gt;Docker health checks that catch real problems, explained simply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/canary-release-weighted-load-balancer?utm_source=devto&amp;amp;utm_campaign=deploy-on-push-github-actions-webhook" rel="noopener noreferrer"&gt;Canary releases with a weighted load balancer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=deploy-on-push-github-actions-webhook" rel="noopener noreferrer"&gt;Gate your app on a database migration job&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a health check&lt;/strong&gt; so a failing release can be caught and rolled back: see the &lt;a href="https://www.hubfly.space/docs/observability/" rel="noopener noreferrer"&gt;observability docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send 10% of traffic to the new version first&lt;/strong&gt; with a weighted load balancer: &lt;a href="https://www.hubfly.space/docs/load-balancers/" rel="noopener noreferrer"&gt;load balancer docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get notified when an image is pushed or scanned&lt;/strong&gt; with registry webhooks: &lt;a href="https://www.hubfly.space/docs/webhooks/" rel="noopener noreferrer"&gt;webhooks docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ready to stop redeploying by hand?&lt;/strong&gt; No card needed: &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Start free, 360 hours included →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>cicd</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>How to sandbox AI agents safely: one container per task</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:17:03 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/how-to-sandbox-ai-agents-safely-one-container-per-task-4loo</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/how-to-sandbox-ai-agents-safely-one-container-per-task-4loo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/sandbox-ai-agents-container-per-task?utm_source=devto&amp;amp;utm_campaign=sandbox-ai-agents-container-per-task" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated 21 October 2026.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a fresh, locked-down container for every agent task, a budget with alerts for all agent work, an optional wallet and key per agent, and an off switch you can reach in two clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; developers letting an AI agent run code it wrote (coding agents, data agents, tool-using bots).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 15 minutes in the dashboard. The free shared tier gives you 360 compute hours a month.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your agent writes a Python script and runs it. Most days that's fine. Then one day it deletes the wrong folder, prints your API keys into a log, or loops for six hours on a paid machine. If that code ran on your laptop or your main server, the damage lands on everything you own.&lt;/p&gt;

&lt;p&gt;In this guide you'll give every agent task its own small, locked-down container on Hubfly space, put a budget around all of it, and set up a way to stop everything at once. The goal: when an agent does something dumb, only a throwaway box is hurt, the bill stays small, and you can switch it off in seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=agent-sandbox" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space&lt;/strong&gt;&lt;/a&gt;. Steps 1 to 4 and 7 work on the free shared tier. Sub-account wallets (Step 5) need paid balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety model: four walls around every agent
&lt;/h2&gt;

&lt;p&gt;Many sandbox guides cover containers, resource limits and network rules. Fewer cover spend limits, revocable keys per agent, or cleaning up what the agent leaves behind. A safe setup has four walls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wall&lt;/th&gt;
&lt;th&gt;What it protects against&lt;/th&gt;
&lt;th&gt;Where you set it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. One container per task&lt;/td&gt;
&lt;td&gt;A bad task breaking other tasks or your own machine&lt;/td&gt;
&lt;td&gt;Create Container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Fewer Linux permissions&lt;/td&gt;
&lt;td&gt;Code escaping its box or sniffing the network&lt;/td&gt;
&lt;td&gt;Security Capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Money limits&lt;/td&gt;
&lt;td&gt;Surprise bills (alerts at 80% and 100%, per-agent wallets)&lt;/td&gt;
&lt;td&gt;Project budget, sub-account wallet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. An off switch and cleanup&lt;/td&gt;
&lt;td&gt;Leftover containers and leaked keys&lt;/td&gt;
&lt;td&gt;Danger Zone, key revoke&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Build them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a project just for agent work, with a $10 budget
&lt;/h2&gt;

&lt;p&gt;A Hubfly space Cell project holds containers, volumes, environment variables and a budget. Keeping agent work in its own project keeps its private network, its secrets and its bill apart from your real apps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;agent-sandbox&lt;/code&gt;, pick &lt;strong&gt;Cell&lt;/strong&gt;, and choose the region closest to you (for example &lt;strong&gt;eu-central&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced Options&lt;/strong&gt; and click the &lt;strong&gt;$10&lt;/strong&gt; quick cap under &lt;strong&gt;Monthly Budget Limit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Project&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why the budget? Agents don't get tired. A loop that keeps starting containers keeps going until something stops it. The project's &lt;strong&gt;Settings → General &amp;amp; Budget&lt;/strong&gt; page shows your projected cost against the cap, and you get a notification at 80% and at 100%. Treat it as an alarm, not a guaranteed stop: Step 7 is how you actually stop things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Give each task its own container
&lt;/h2&gt;

&lt;p&gt;Every task the agent runs gets a fresh container. When the task ends, you delete it. Nothing carries over, so a broken or poisoned task can't affect the next one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the project, click &lt;strong&gt;Create → Create Container&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container name:&lt;/strong&gt; &lt;code&gt;agent-task-001&lt;/code&gt;. Use one name per task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Source:&lt;/strong&gt; &lt;strong&gt;Docker image&lt;/strong&gt;. Use an image that contains your task runner, for example &lt;code&gt;ghcr.io/your-org/agent-runner:1.0&lt;/code&gt;, or a plain runtime like &lt;code&gt;python:3.12-alpine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources:&lt;/strong&gt; choose &lt;strong&gt;Shared Tier&lt;/strong&gt;. It gives the task 0.3 vCPU, 256 MB of RAM and a small disk. That's a natural size limit: a runaway script can't eat a whole server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration → Process Settings → Command:&lt;/strong&gt; the command the task runs, &lt;strong&gt;one argument per line&lt;/strong&gt;. For a one-shot script:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python
/task/run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid commands that serve files or open ports (like &lt;code&gt;python -m http.server&lt;/code&gt;). Anything listening is reachable by every other container in the project.&lt;/p&gt;

&lt;p&gt;Containers in the same project share a private network. If you run untrusted tasks side by side, run them one at a time per project, or give each agent its own sub-account (Step 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Take away permissions the task doesn't need
&lt;/h2&gt;

&lt;p&gt;Linux "capabilities" are small pieces of root power: changing file owners, opening raw network sockets, creating device files. A script that reads a CSV needs almost none of them. About half of the top sandbox guides skip this wall.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Configuration → Security Capabilities&lt;/strong&gt;. Two protections are already on by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seccomp profile: runtime-default&lt;/strong&gt; blocks dangerous system calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No new privileges: Enabled&lt;/strong&gt; stops a process from gaining more power than it started with, even through a &lt;code&gt;setuid&lt;/code&gt; program.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below that, the container starts with 13 default capabilities. Under &lt;strong&gt;Drop capabilities&lt;/strong&gt;, click these five:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;What it allows&lt;/th&gt;
&lt;th&gt;Why an agent task doesn't need it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NET_RAW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw sockets (ping, packet sniffing, spoofing)&lt;/td&gt;
&lt;td&gt;Normal HTTP calls work without it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MKNOD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creating device files&lt;/td&gt;
&lt;td&gt;Ordinary scripts never create devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SYS_CHROOT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changing the root folder&lt;/td&gt;
&lt;td&gt;A common step in escape tricks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SETFCAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Giving capabilities to files&lt;/td&gt;
&lt;td&gt;Lets a program grant itself power later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SETPCAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changing a process's capabilities&lt;/td&gt;
&lt;td&gt;Same risk, at the process level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Selection Summary&lt;/strong&gt; now reads &lt;strong&gt;Added 8, Dropped 5&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;CHOWN&lt;/code&gt;, &lt;code&gt;SETUID&lt;/code&gt;, &lt;code&gt;SETGID&lt;/code&gt; and &lt;code&gt;DAC_OVERRIDE&lt;/code&gt; unless you know your image never switches users or fixes file ownership at start-up. Many official images do, and they fail to start without them. Tighten one capability at a time and watch the logs.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Continue&lt;/strong&gt; to &lt;strong&gt;Storage&lt;/strong&gt; and add nothing. With no volume, the dashboard says "Ephemeral container storage will be used", so nothing the task writes survives it. Then &lt;strong&gt;Review&lt;/strong&gt; and &lt;strong&gt;Deploy Container&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Keep the sandbox off the internet
&lt;/h2&gt;

&lt;p&gt;Don't add a domain or a public port to a task container. Without one, nobody outside can reach it. The Review step shows its private address, like &lt;code&gt;agent-task-001-&amp;lt;id&amp;gt;.eucentral.hubfly.internal&lt;/code&gt;, and the private network is isolated to containers inside the same project.&lt;/p&gt;

&lt;p&gt;Two limits to plan around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assume outbound traffic is open.&lt;/strong&gt; Hubfly space doesn't document outbound (egress) controls, so assume the task can call any website. Never put secrets in a task container's environment. Give it only the one short-lived value it needs, or none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything public gets found fast.&lt;/strong&gt; A new public subdomain can see bot requests within minutes. If an agent must show a preview, put a login in front of it first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Give each agent its own wallet and key
&lt;/h2&gt;

&lt;p&gt;So far, every agent works inside your account. If an agent holds a key to your account and that key leaks, all your projects are exposed. Hubfly space sub-accounts fix that: each agent becomes a separate tenant with its own projects, its own wallet and its own API keys.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your profile, then &lt;strong&gt;Sub-accounts → Create&lt;/strong&gt;. Give it a name like &lt;code&gt;research-agent&lt;/code&gt; and, optionally, an external reference.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Manage&lt;/strong&gt;, use &lt;strong&gt;Fund wallet&lt;/strong&gt; to move a small amount (say $5) from your paid balance. Only paid balance can move, not promotional credit.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New key&lt;/strong&gt;. The secret is shown once, so store it in your orchestrator's secret manager right away. Tenant keys start with &lt;code&gt;hfsk_&lt;/code&gt; and only work for that one tenant.&lt;/li&gt;
&lt;li&gt;Give the agent its own &lt;code&gt;agent-sandbox&lt;/code&gt; project inside the sub-account, and repeat Steps 2 to 4 there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate money.&lt;/strong&gt; The agent's work is paid from its own wallet, which holds only what you moved into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A key you can kill.&lt;/strong&gt; &lt;strong&gt;Revoke&lt;/strong&gt; one agent's key without touching anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits:&lt;/strong&gt; 25 active sub-accounts and 10 active keys each by default, one level deep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Careful:&lt;/strong&gt; &lt;strong&gt;Revoke&lt;/strong&gt; and &lt;strong&gt;Suspend&lt;/strong&gt; act immediately, with no confirmation dialog. And &lt;strong&gt;suspending a sub-account doesn't stop its running containers&lt;/strong&gt;, so stop or delete them first (Step 7).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Automate it: create and remove a container per task
&lt;/h2&gt;

&lt;p&gt;Clicking through the dashboard for every task doesn't scale. Your orchestrator (the code that hands tasks to the agent) can do Steps 2 and 3 with two API calls: create before the task, remove after. Field names follow the &lt;a href="https://www.hubfly.space/docs/api/cell/containers/" rel="noopener noreferrer"&gt;containers API reference&lt;/a&gt;. Check the &lt;code&gt;resources&lt;/code&gt; units there before you rely on the values below, which mirror the shared tier.&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;# Run on your orchestrator, never inside the sandbox.&lt;/span&gt;
&lt;span class="c"&gt;# HUBFLY_TOKEN: a key for the account that owns PROJECT&lt;/span&gt;
&lt;span class="c"&gt;# (for a sub-account agent: its hfsk_ key and a project inside that sub-account).&lt;/span&gt;
&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.hubfly.space/api/v1
&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-project-id&amp;gt;
&lt;span class="nv"&gt;TASK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;agent-task-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt;/containers/create"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HUBFLY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TASK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'",
    "kind": "job",
    "tier": "shared",
    "source": { "type": "docker", "dockerImage": "ghcr.io/your-org/agent-runner:1.0" },
    "resources": { "cpu": 0.3, "ram": 256, "storage": 2 },
    "security": {
      "noNewPrivileges": true,
      "seccompProfile": "runtime-default",
      "dropCapabilities": ["NET_RAW", "MKNOD", "SYS_CHROOT", "SETFCAP", "SETPCAP"]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bake the task command into the image (&lt;code&gt;CMD ["python", "/task/run.py"]&lt;/code&gt;), so the API call doesn't need it. When the task is done, remove the container. This is permanent:&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;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt;/containers/&amp;lt;container-id&amp;gt;/remove"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HUBFLY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the remove call in a &lt;code&gt;finally&lt;/code&gt; block or an exit trap, so it also runs when the task crashes. Forgotten containers are a common way sandbox bills creep up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Your off switch
&lt;/h2&gt;

&lt;p&gt;Know where this is before you need it. Open the project's &lt;strong&gt;Settings → Danger Zone&lt;/strong&gt; (for a sub-account agent, the project inside its sub-account):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hibernate Project&lt;/strong&gt; removes running containers and networking routes but keeps the configuration, so you can restore later. Use it when an agent misbehaves and you want to look before you delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete All Resources&lt;/strong&gt; permanently removes every container, volume and public route. The project itself stays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the agent has its own sub-account, open it next and &lt;strong&gt;Revoke&lt;/strong&gt; its key. Hibernate already stopped what was running; the revoked key means the agent can't start anything new.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. "Monthly free tier compute hours exhausted. Upgrade this container to a dedicated tier to resume running."&lt;/strong&gt;&lt;br&gt;
The account has used its 360 free shared-tier hours for the month. Stop containers you don't need, or pick &lt;strong&gt;Dedicated Tier&lt;/strong&gt; for task containers. A dedicated 1 vCPU / 512 MB container costs $0.005 an hour, so a 10-minute task costs less than a tenth of a cent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The container exits right away after you dropped capabilities.&lt;/strong&gt;&lt;br&gt;
The image needed one of them, usually to switch users or fix file ownership at start-up. Open &lt;strong&gt;Logs&lt;/strong&gt;, look for "operation not permitted", and add back only that capability (often &lt;code&gt;SETUID&lt;/code&gt;/&lt;code&gt;SETGID&lt;/code&gt; or &lt;code&gt;CHOWN&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You suspended an agent and it's still costing money.&lt;/strong&gt;&lt;br&gt;
Suspend blocks the agent's keys and changes, but doesn't stop running containers. Stop or delete them first, then suspend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Rates: $0.004 per vCPU-hour and $0.002 per GB-hour of RAM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared tier tasks:&lt;/strong&gt; free within 360 compute hours a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 10-minute task on a dedicated 1 vCPU / 512 MB container:&lt;/strong&gt; (1 × $0.004 + 0.5 × $0.002) × 10/60 = &lt;strong&gt;$0.00083&lt;/strong&gt;. A thousand of them is about &lt;strong&gt;$0.83&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One always-on dedicated worker (1 vCPU / 512 MB, 730 hours):&lt;/strong&gt; 1 × 0.004 × 730 + 0.5 × 0.002 × 730 = &lt;strong&gt;$3.65 a month&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard says shared containers suspend after 15 minutes of inactivity. Don't count on it to clean up: delete task containers yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;An agent sandbox isn't one setting. It's four walls: a fresh container per task, fewer Linux permissions, money limits, and an off switch you've already found. Set them up once, and a bad agent run costs you a throwaway container and a few cents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let your coding agent deploy without deleting your data:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/coding-agent-deploy-safely-hubfly?utm_source=devto&amp;amp;utm_campaign=sandbox-ai-agents-container-per-task" rel="noopener noreferrer"&gt;1 skill, 3 permission lists&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One wallet and one key per customer, through the API:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/multi-tenant-saas-isolated-tenants-hubfly-subaccounts?utm_source=devto&amp;amp;utm_campaign=sandbox-ai-agents-container-per-task" rel="noopener noreferrer"&gt;isolated SaaS tenants with sub-accounts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep task results:&lt;/strong&gt; write outputs to Hubfly space Storage (S3-compatible) instead of the container, which is deleted after the task. &lt;a href="https://www.hubfly.space/docs/storage/assets-and-images/" rel="noopener noreferrer"&gt;Storage docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read next:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/docker-health-checks-postgres-redis?utm_source=devto&amp;amp;utm_campaign=sandbox-ai-agents-container-per-task" rel="noopener noreferrer"&gt;Docker health checks that catch real problems, explained simply&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=agent-sandbox" rel="noopener noreferrer"&gt;&lt;strong&gt;Create your agent sandbox project on Hubfly space&lt;/strong&gt;&lt;/a&gt;: free shared tier with 360 compute hours a month.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to move your app from Railway to Hubfly space, step by step</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:35:05 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/how-to-move-your-app-from-railway-to-hubfly-space-step-by-step-3ih4</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/how-to-move-your-app-from-railway-to-hubfly-space-step-by-step-3ih4</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/migrate-from-railway?utm_source=devto&amp;amp;utm_campaign=migrate-from-railway" rel="noopener noreferrer"&gt;Hubfly space blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; your Railway web service and Postgres database running on Hubfly space, your data copied with one throwaway job, and your domain switched over with minutes of read-only time, not hours of downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone with a Railway project made of a web service, a Postgres database and maybe a volume or a cron job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time needed:&lt;/strong&gt; about 45 minutes for a small app, most of it waiting for DNS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost on Hubfly space:&lt;/strong&gt; about &lt;strong&gt;$7.31 a month&lt;/strong&gt; for a web service and Postgres, each on 1 vCPU and 512 MB, running 24/7, plus 1 GB of disk. The math is below.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your app runs on Railway and it works. But the bill moves with your traffic, and Railway's &lt;code&gt;railway.json&lt;/code&gt; and &lt;code&gt;railway.toml&lt;/code&gt; files stop working on &lt;strong&gt;1 December 2026&lt;/strong&gt; (&lt;a href="https://docs.railway.com/config-as-code" rel="noopener noreferrer"&gt;Railway docs&lt;/a&gt;). If you use config-as-code, you'll have to rewrite it either way. That makes now a good moment to decide where the app should live.&lt;/p&gt;

&lt;p&gt;This guide moves a typical Railway project (a web service plus Postgres) to Hubfly space. You'll export your variables, describe the app in one Compose file, copy the database with a one-shot job that runs inside your new project, and switch your domain with a short write freeze. Your users see a few minutes of read-only mode at most, and you can roll back by pointing DNS back at Railway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space&lt;/strong&gt;&lt;/a&gt;: the free tier includes 360 compute hours a month and needs no card, so you can rehearse the whole move before you pay anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start: what moves, and where it goes
&lt;/h2&gt;

&lt;p&gt;Write down every piece of your Railway project first. Anything you forget here becomes an outage later.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On Railway&lt;/th&gt;
&lt;th&gt;On Hubfly space&lt;/th&gt;
&lt;th&gt;How it moves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service built from GitHub&lt;/td&gt;
&lt;td&gt;Container built from the same repo, or a Compose service&lt;/td&gt;
&lt;td&gt;Connect the repo or point Compose at an image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres database service&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;postgres:16-alpine&lt;/code&gt; container with a volume&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pg_dump&lt;/code&gt; piped into &lt;code&gt;pg_restore&lt;/code&gt; (step 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variables, including &lt;code&gt;${{Postgres.DATABASE_URL}}&lt;/code&gt; references&lt;/td&gt;
&lt;td&gt;Compose &lt;code&gt;environment:&lt;/code&gt; or project variables, with &lt;code&gt;{{service:db.host}}&lt;/code&gt; references&lt;/td&gt;
&lt;td&gt;Export, then rewrite references (step 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume (one per service)&lt;/td&gt;
&lt;td&gt;Volume mounted at the same path&lt;/td&gt;
&lt;td&gt;Archive and upload (step 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private hostname &lt;code&gt;web.railway.internal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{service:web.host}}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace in variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domain (CNAME + TXT)&lt;/td&gt;
&lt;td&gt;Add the domain to the container&lt;/td&gt;
&lt;td&gt;DNS switch (step 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cronSchedule&lt;/code&gt; service&lt;/td&gt;
&lt;td&gt;A job container or an always-on worker&lt;/td&gt;
&lt;td&gt;Re-create&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Export your variables
&lt;/h2&gt;

&lt;p&gt;Variables are the easiest thing to lose. Export them per service with the Railway CLI (&lt;a href="https://docs.railway.com/reference/cli-api" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;railway &lt;span class="nb"&gt;link&lt;/span&gt;                      &lt;span class="c"&gt;# pick the project and environment&lt;/span&gt;
railway variables &lt;span class="nt"&gt;--service&lt;/span&gt; web &lt;span class="nt"&gt;--kv&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; web.env
railway variables &lt;span class="nt"&gt;--service&lt;/span&gt; Postgres &lt;span class="nt"&gt;--kv&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; postgres.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;web.env&lt;/code&gt; and check three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sealed variables are missing.&lt;/strong&gt; Railway never shows sealed values in the UI, the API or the CLI (&lt;a href="https://docs.railway.com/variables" rel="noopener noreferrer"&gt;Railway docs&lt;/a&gt;). Find them in your password manager or your provider's console (Stripe, Resend, OAuth apps) and add them by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway-only variables can go.&lt;/strong&gt; Drop &lt;code&gt;RAILWAY_*&lt;/code&gt; entries. Keep &lt;code&gt;PORT&lt;/code&gt; only if your app reads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database URLs point at Railway.&lt;/strong&gt; The exported &lt;code&gt;DATABASE_URL&lt;/code&gt; holds Railway's private hostname, which won't resolve anywhere else. You'll replace it in step 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why this matters: a missing secret doesn't fail the deploy. It fails the first checkout or login after you switch DNS, in front of real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Hubfly space dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it after your app (&lt;code&gt;orders-api&lt;/code&gt;), choose &lt;strong&gt;Cell&lt;/strong&gt;, and pick the region closest to your users (eu-central, eu-north or usa-central).&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced Options&lt;/strong&gt; and set a &lt;strong&gt;Monthly Budget Limit&lt;/strong&gt;. $10 is plenty for the rehearsal. It keeps a mistake from turning into a surprise bill.&lt;/li&gt;
&lt;li&gt;Optional: add shared values (an API key every service needs) under &lt;strong&gt;Project Environment Variables&lt;/strong&gt;. They're encrypted with AES-256-GCM and are shared across the project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Describe the app in one Compose file
&lt;/h2&gt;

&lt;p&gt;Railway builds each service separately. On Hubfly space you describe the whole app in one Compose file, and the Stack Composer deploys it in order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me-long-random&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

  &lt;span class="na"&gt;migrate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SOURCE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your Railway DATABASE_PUBLIC_URL&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;TARGET_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://postgres:change-me-long-random@{{service:db.host}}:5432/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;until&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$SOURCE_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$TARGET_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;waiting;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;done;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pg_dump&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$SOURCE_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--format=custom&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-owner&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-acl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pg_restore&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-owner&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-acl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--exit-on-error&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$TARGET_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;psql&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$TARGET_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'SELECT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;count(*)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;FROM&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orders'"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db&lt;/code&gt;&lt;/strong&gt; is your new Postgres. The named volume keeps its files across redeploys; without it, every update would wipe your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The health check uses &lt;code&gt;-h 127.0.0.1&lt;/code&gt;.&lt;/strong&gt; That forces a TCP check. Postgres briefly runs a temporary server on its Unix socket during first start, and a socket-only check can pass before the real server listens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;migrate&lt;/code&gt;&lt;/strong&gt; is a one-shot job (&lt;code&gt;restart: "no"&lt;/code&gt; becomes a Job). It waits until both databases answer, streams a dump from Railway straight into the new database, and prints a row count you can compare. No laptop in the middle, no dump file to store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;{{service:db.host}}&lt;/code&gt;&lt;/strong&gt; is Hubfly space's version of Railway's &lt;code&gt;${{Postgres.DATABASE_URL}}&lt;/code&gt;. It resolves to the database's private hostname, inside a longer URL too.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;orders&lt;/code&gt; in the last command with one of your own tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add your web service under &lt;code&gt;services:&lt;/code&gt; the same way, with &lt;code&gt;depends_on: db: condition: service_healthy&lt;/code&gt; and &lt;code&gt;DATABASE_URL: postgresql://postgres:change-me-long-random@{{service:db.host}}:5432/app&lt;/code&gt;. If you'd rather build from GitHub than from an image, create the web container from your repo after the stack is up; a root &lt;code&gt;Dockerfile&lt;/code&gt; wins over auto-detection.&lt;/p&gt;

&lt;p&gt;Don't commit the real password. After the first deploy, open each container's &lt;strong&gt;Configuration → Environment Variables → Edit&lt;/strong&gt; and switch the database variables, including &lt;code&gt;SOURCE_URL&lt;/code&gt;, to &lt;strong&gt;Secret&lt;/strong&gt;. Variables are edited one at a time there, so paste longer lists into the Compose file before you import it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Copy the database
&lt;/h2&gt;

&lt;p&gt;First, give the copy job a way in. In Railway, open your Postgres service, enable public access through the &lt;strong&gt;TCP Proxy&lt;/strong&gt;, and copy the &lt;code&gt;DATABASE_PUBLIC_URL&lt;/code&gt; it creates (&lt;a href="https://docs.railway.com/guides/postgresql" rel="noopener noreferrer"&gt;Railway Postgres docs&lt;/a&gt;). Paste it as &lt;code&gt;SOURCE_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then rehearse:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your project, click &lt;strong&gt;Create → Compose Stack&lt;/strong&gt;, open the &lt;strong&gt;YAML&lt;/strong&gt; tab, paste the file and click &lt;strong&gt;Import YAML&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy Stack&lt;/strong&gt;. The activity log shows the volume, then &lt;code&gt;db&lt;/code&gt;, then the health gate, then &lt;code&gt;migrate&lt;/code&gt;. A two-service stack like this one finishes in about 40 seconds.&lt;/li&gt;
&lt;li&gt;Open the &lt;code&gt;migrate&lt;/code&gt; container → &lt;strong&gt;Logs&lt;/strong&gt;. You should see &lt;code&gt;accepting connections&lt;/code&gt; for both databases, then your row count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How long the real copy takes depends on bandwidth, not Postgres. Inside one region, a 24 MB database with 200,000 rows copies in about 2 seconds. Across the internet from Railway, the job's speed is set by the container's network link: a shared-tier container shows a 15 Mbps limit under its &lt;strong&gt;Network&lt;/strong&gt; tab, roughly 2 MB per second. Custom-format dumps are compressed, so at 15 Mbps a 1 GB dump takes at most about 9 minutes. Run the rehearsal to get your real number. The job only runs for minutes, so the shared tier fits it. Railway bills proxy traffic as egress at $0.05 per GB (&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;Railway pricing&lt;/a&gt;), so a 1 GB copy costs about 5 cents there.&lt;/p&gt;

&lt;p&gt;Why rehearse: the dry run tells you exactly how long your write freeze will be, and it catches extensions or roles your new database is missing while nothing is at stake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Move volume files (skip if you only use Postgres)
&lt;/h2&gt;

&lt;p&gt;Railway allows one volume per service. If yours holds uploads or a SQLite file, download the files with &lt;code&gt;railway volume files&lt;/code&gt; or over &lt;code&gt;scp&lt;/code&gt; through &lt;code&gt;railway ssh&lt;/code&gt; (&lt;a href="https://docs.railway.com/volumes/reference" rel="noopener noreferrer"&gt;Railway volume docs&lt;/a&gt;). Then mount a volume at the same path on Hubfly space and copy the files into it, for example from a one-shot job that downloads the archive.&lt;/p&gt;

&lt;p&gt;If those files are user uploads, consider moving them to Hubfly space Storage instead. It's S3-compatible, so your app's S3 client works with a new endpoint and keys, and it costs about $0.011 per GB-month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Switch the domain with a short write freeze
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A day before:&lt;/strong&gt; lower your domain's DNS TTL to 60–300 seconds, so the switch spreads in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the domain on Hubfly space:&lt;/strong&gt; add &lt;code&gt;app.your-domain.com&lt;/code&gt; to your web container, create the DNS records Hubfly space shows you, and wait for the certificate to be issued. Let's Encrypt issues and renews it for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze writes on Railway:&lt;/strong&gt; put the app in maintenance mode, or stop the web service. Anything written after the final dump would be lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the final copy into a clean database:&lt;/strong&gt; open &lt;strong&gt;Project Settings → Danger Zone → Delete All Resources&lt;/strong&gt; (the &lt;strong&gt;Delete Everything&lt;/strong&gt; button) to remove the rehearsal containers and volume, then import and deploy the same Compose file again. Compare the job's row count with Railway's, then turn off public access (the TCP Proxy) on Railway's Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch DNS&lt;/strong&gt; to the Hubfly space target and watch the web container's logs for real traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up later:&lt;/strong&gt; keep Railway running read-only for a few days as your rollback, then delete its CNAME and TXT records (&lt;a href="https://docs.railway.com/networking/domains/working-with-domains" rel="noopener noreferrer"&gt;both are required on Railway&lt;/a&gt;) and the Railway services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rollback is simple while Railway still exists: point DNS back. Plan to roll back only before new writes land on Hubfly space; after that, copying new data back is a second migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs: Railway vs Hubfly space
&lt;/h2&gt;

&lt;p&gt;Prices as of 20 October 2026. Railway bills measured usage per second (&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;railway.com/pricing&lt;/a&gt;); Hubfly space bills the size you reserve per hour (&lt;a href="https://www.hubfly.space/pricing/" rel="noopener noreferrer"&gt;hubfly.space/pricing&lt;/a&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup (web + Postgres, 730 h, 1 GB volume)&lt;/th&gt;
&lt;th&gt;Railway Hobby&lt;/th&gt;
&lt;th&gt;Hubfly space&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Light use: 0.15 vCPU and 0.45 GB RAM in use on average&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$7.77&lt;/strong&gt; (the $5 plan fee is absorbed into usage)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$7.31&lt;/strong&gt; (two 1 vCPU / 512 MB containers, 24/7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Busy: both services using 1 vCPU and 0.5 GB nonstop&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$50.88&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$7.31&lt;/strong&gt; (same reserved size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging copy that sleeps most of the day&lt;/td&gt;
&lt;td&gt;Pays for active time; a slept service accrues no compute charges (&lt;a href="https://docs.railway.com/guides/cut-idle-costs-serverless" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Shared tier, free within 360 compute hours a month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Railway math&lt;/strong&gt; (CPU $0.00000772 per vCPU-second, RAM $0.00000386 per GB-second, volume $0.00000006 per GB-second; 730 h = 2,628,000 s):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Light: 0.15 × 0.00000772 × 2,628,000 = $3.04; 0.45 × 0.00000386 × 2,628,000 = $4.56; 1 × 0.00000006 × 2,628,000 = $0.16. Total &lt;strong&gt;$7.77&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Busy: 2 × 0.00000772 × 2,628,000 = $40.58; 1 × 0.00000386 × 2,628,000 = $10.14; volume $0.16. Total &lt;strong&gt;$50.88&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hubfly space math&lt;/strong&gt; (CPU $0.004 per vCPU-hour, RAM $0.002 per GB-hour, storage $0.00001536 per GB-hour):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One container: 1 × 0.004 × 730 = $2.92; 0.5 × 0.002 × 730 = $0.73. That's $3.65.&lt;/li&gt;
&lt;li&gt;Two containers: $7.30. Volume: 1 × 0.00001536 × 730 = $0.011. Total &lt;strong&gt;$7.31&lt;/strong&gt;. Faster volume modes (Balanced 1.5×, High Performance 2×) add up to about a cent per GB a month ($0.017 and $0.022 instead of $0.011).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest read: for a small, quiet app the two bills land within a few cents of each other. Hubfly space wins when your app does steady work, because the price doesn't climb with load. Railway wins when your services idle near zero most of the month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Railway is still the better pick
&lt;/h2&gt;

&lt;p&gt;Fair is fair. Stay on Railway, or move only part of your app, if you depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR environments:&lt;/strong&gt; Railway spins up an isolated copy of your environment for every pull request and deletes it when the PR is merged or closed (&lt;a href="https://docs.railway.com/guides/preview-deployments-with-pr-environments" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in database backups and point-in-time restore,&lt;/strong&gt; plus in-place Postgres major-version upgrades with automatic backups and one-click revert (&lt;a href="https://docs.railway.com/guides/postgresql" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). Hubfly space doesn't document database backups yet, so schedule your own &lt;code&gt;pg_dump&lt;/code&gt; to Storage if you move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paying almost nothing for idle services:&lt;/strong&gt; with per-second usage billing, a mostly idle app can cost less than a reserved container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More regions and multi-region replicas:&lt;/strong&gt; Railway lists four regions across the US, Europe and Asia (&lt;a href="https://docs.railway.com/deployments/regions" rel="noopener noreferrer"&gt;regions&lt;/a&gt;) and can place replicas in several of them (&lt;a href="https://docs.railway.com/deployments/scaling" rel="noopener noreferrer"&gt;scaling&lt;/a&gt;). Hubfly space offers three regions today.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;no response&lt;/code&gt; or &lt;code&gt;Name does not resolve&lt;/code&gt; on the first connection.&lt;/strong&gt; A fresh container can take a couple of seconds before private DNS answers. The &lt;code&gt;until pg_isready …; do sleep 2; done&lt;/code&gt; loop in the job handles it. Without the loop, the job fails, the stack rolls back, and the job's logs go with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The health check passes too early.&lt;/strong&gt; &lt;code&gt;pg_isready&lt;/code&gt; without &lt;code&gt;-h 127.0.0.1&lt;/code&gt; can succeed against Postgres's temporary startup server. Keep the &lt;code&gt;-h 127.0.0.1&lt;/code&gt; flag so dependents wait for the real one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;${{...}}&lt;/code&gt; or &lt;code&gt;$$&lt;/code&gt; copied as-is.&lt;/strong&gt; Railway's &lt;code&gt;${{Postgres.DATABASE_URL}}&lt;/code&gt; means nothing elsewhere; replace it with a full URL using &lt;code&gt;{{service:db.host}}&lt;/code&gt;. And write &lt;code&gt;$VAR&lt;/code&gt; in Compose commands, not &lt;code&gt;$$VAR&lt;/code&gt;: the Stack Composer keeps &lt;code&gt;$$&lt;/code&gt; literally, and the shell turns it into a process ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A Railway move is mostly bookkeeping: export the variables, rewrite the references, copy the data with a job that runs next to the new database, and switch DNS during a short freeze. Rehearse once on the free tier and the real cutover is a few minutes of read-only mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read next:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/heroku-alternatives-2026-prices?utm_source=devto&amp;amp;utm_campaign=migrate-from-railway" rel="noopener noreferrer"&gt;Best Heroku alternatives in 2026: 7 options and what they cost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/how-to-deploy-odoo-on-hubfly-space-with-postgresql-and-persistent-storage?utm_source=devto&amp;amp;utm_campaign=migrate-from-railway" rel="noopener noreferrer"&gt;How to deploy Odoo on Hubfly space with PostgreSQL and persistent storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/coding-agent-deploy-safely-hubfly?utm_source=devto&amp;amp;utm_campaign=migrate-from-railway" rel="noopener noreferrer"&gt;Let your coding agent deploy to Hubfly space safely&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put uploads in Hubfly space Storage:&lt;/strong&gt; S3-compatible buckets with image resizing built in (&lt;a href="https://www.hubfly.space/docs/storage/assets-and-images/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach the database from your laptop&lt;/strong&gt; without opening it to the internet, with a HubTunnel from the Hubfly space CLI (&lt;a href="https://www.hubfly.space/docs/tunnels/" rel="noopener noreferrer"&gt;tunnels docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy automatically on every push&lt;/strong&gt; with deploy webhooks and auto-rollback (&lt;a href="https://www.hubfly.space/docs/deployment-pipelines/" rel="noopener noreferrer"&gt;deployment pipelines&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ready to move?&lt;/strong&gt; &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Create a free Hubfly space project&lt;/a&gt;, paste the Compose file above, and run your first rehearsal copy today. Moving a bigger Railway project? &lt;a href="https://www.hubfly.space" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; about help with the cutover.&lt;/p&gt;

</description>
      <category>railway</category>
      <category>postgres</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to resize and watermark product photos with just a URL</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:32:55 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/how-to-resize-and-watermark-product-photos-with-just-a-url-563</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/how-to-resize-and-watermark-product-photos-with-just-a-url-563</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; one upload per photo, and every thumbnail, WebP version and watermarked copy from a URL like &lt;code&gt;…/image?w=400&amp;amp;h=400&amp;amp;fit=cover&amp;amp;format=webp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; shops, marketplaces and apps that show user or product photos, and don't want to run an image server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 10 minutes in the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; transforms are free. You pay only for storing originals and cached variants, at the Standard storage rate of about $0.011 per GB-month.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every product photo on your site needs four or five sizes: a thumbnail, a card, a full view, a social preview. Most teams either resize on upload with a script that breaks the day design changes a size, or run an image server they then have to patch and scale. Both cost you time, and slow pages cost you sales.&lt;/p&gt;

&lt;p&gt;In this guide you'll store each photo once in &lt;strong&gt;Hubfly space Storage&lt;/strong&gt; and ask for any size, format or watermark straight from the URL. Your designer can change the card size tomorrow without anyone re-processing a single file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space →&lt;/strong&gt;&lt;/a&gt; (free tier, no card needed)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the feature does
&lt;/h2&gt;

&lt;p&gt;Hubfly space Storage is S3-compatible object storage. When you register an uploaded file as a &lt;strong&gt;managed asset&lt;/strong&gt;, it gets delivery links and an image endpoint. Add query parameters to that endpoint and the service renders the variant on the first request, then caches it in your bucket.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Limits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;w&lt;/code&gt;, &lt;code&gt;h&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Output width and height&lt;/td&gt;
&lt;td&gt;1–4,096 px each, 16 megapixels max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inside&lt;/code&gt; (default), &lt;code&gt;contain&lt;/code&gt;, &lt;code&gt;cover&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Anything except &lt;code&gt;inside&lt;/code&gt; needs both &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;format&lt;/code&gt;, &lt;code&gt;quality&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;webp&lt;/code&gt; (default), &lt;code&gt;jpeg&lt;/code&gt;, &lt;code&gt;png&lt;/code&gt;; quality 1–100 (default 80)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fx&lt;/code&gt;, &lt;code&gt;fy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Focal point for &lt;code&gt;cover&lt;/code&gt; crops, 0 to 1&lt;/td&gt;
&lt;td&gt;Defaults to the centre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rotate&lt;/code&gt;, &lt;code&gt;flip&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0/90/180/270; &lt;code&gt;h&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt; or &lt;code&gt;hv&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;sharpen&lt;/code&gt;, &lt;code&gt;brightness&lt;/code&gt;, &lt;code&gt;contrast&lt;/code&gt;, &lt;code&gt;saturation&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Effects&lt;/td&gt;
&lt;td&gt;Service-side bounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wm_text&lt;/code&gt; or &lt;code&gt;wm_key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;One text or image watermark, placed with &lt;code&gt;wm_x&lt;/code&gt;, &lt;code&gt;wm_y&lt;/code&gt;, &lt;code&gt;wm_opacity&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Image watermarks must be saved as allowed first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full reference: &lt;a href="https://www.hubfly.space/docs/storage/assets-and-images/" rel="noopener noreferrer"&gt;Storage assets and images&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough: a product image pipeline in 10 minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create a Storage project
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;, name it (for example &lt;code&gt;shop-media&lt;/code&gt;), pick the &lt;strong&gt;Storage&lt;/strong&gt; platform and choose a region. A Storage project keeps your buckets, keys and delivery settings apart from your app's containers, so you can give the app a key that only touches images.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a bucket
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Create bucket&lt;/strong&gt; and call it &lt;code&gt;product-images&lt;/code&gt;. A bucket is the folder your app will write to; keeping product photos in their own bucket lets you scope a key to just that bucket later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Upload a photo as a managed file
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Upload files&lt;/strong&gt;, drop in a photo and leave &lt;strong&gt;Managed file&lt;/strong&gt; ticked. That tick is what registers the file as an asset with delivery links and an image endpoint. A plain S3 object doesn't get them automatically.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvz7u9onde2f9yuinn1ae.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvz7u9onde2f9yuinn1ae.png" alt="The Hubfly space Storage upload dialog with the Managed file box ticked for mug-blue.jpg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Choose who can see it
&lt;/h3&gt;

&lt;p&gt;Open the file and go to &lt;strong&gt;Delivery&lt;/strong&gt;. New assets start as &lt;strong&gt;Protected&lt;/strong&gt;: every link needs the asset's access key or a temporary share link, and a request without one gets &lt;code&gt;401 access key required&lt;/code&gt;. For catalogue photos that anyone may see, switch &lt;strong&gt;Link access&lt;/strong&gt; to &lt;strong&gt;Public&lt;/strong&gt;. Keep customer uploads (IDs, invoices, private galleries) on Protected, and give them random object keys: a Protected viewer page still shows the file name and size to anyone with the link.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Build your image URLs
&lt;/h3&gt;

&lt;p&gt;Your asset URL looks like &lt;code&gt;https://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;&lt;/code&gt;. That address opens a viewer page. For image bytes, add &lt;code&gt;/image&lt;/code&gt; and your parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;/image?w=400&amp;amp;h=400&amp;amp;fit=cover&amp;amp;format=webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Transform &amp;amp; Cache&lt;/strong&gt; tab has a form for every parameter and a &lt;strong&gt;Preview transformation&lt;/strong&gt; button that builds the URL for you.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvtd6t3797lafug5h18cv.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvtd6t3797lafug5h18cv.png" alt="The Transform &amp;amp; Cache tab with fields for width, quality, focal point, effects, watermark position, fit and format" width="676" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use the URLs in a responsive image tag so each screen downloads only what it needs:&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://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;/image?w=800&amp;amp;format=webp"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"https://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;/image?w=400&amp;amp;format=webp 400w,
          https://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;/image?w=800&amp;amp;format=webp 800w,
          https://&amp;lt;storage-node&amp;gt;/product-images/&amp;lt;asset-id&amp;gt;/image?w=1600&amp;amp;format=webp 1600w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 100vw, 50vw"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Blue ceramic mug on a wooden table"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a social preview card, ask for the exact shape: &lt;code&gt;?w=1200&amp;amp;h=630&amp;amp;fit=cover&amp;amp;format=jpeg&amp;amp;quality=85&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Add a watermark
&lt;/h3&gt;

&lt;p&gt;Add text and placement to the same URL. &lt;code&gt;wm_x&lt;/code&gt; and &lt;code&gt;wm_y&lt;/code&gt; go from 0 to 1, so 0.9/0.9 puts the mark near the bottom-right corner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;…/image?w=800&amp;amp;h=500&amp;amp;fit=cover&amp;amp;format=webp&amp;amp;wm_text=ACME%20Shop&amp;amp;wm_opacity=0.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft84f3mz2gr4fqfi3890n.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft84f3mz2gr4fqfi3890n.png" alt="An 800×500 WebP product photo of a blue mug with an ACME Shop text watermark in the bottom-right corner" width="505" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a logo instead of text, upload it in &lt;strong&gt;Transform &amp;amp; Cache&lt;/strong&gt;, click &lt;strong&gt;Save allowed watermark&lt;/strong&gt;, and use &lt;code&gt;wm_key&lt;/code&gt;. Only a saved key works in public URLs, so a public link can't pull in any other file as a watermark.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Check the cache
&lt;/h3&gt;

&lt;p&gt;The same tab lists every cached variant with its size and expiry, and lets you delete one. Identical URLs reuse the cached file instead of creating a new variant. On each image you can keep variants for the bucket default, a custom number of hours, or forever.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4embuhlurqduoxggsw7.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4embuhlurqduoxggsw7.png" alt="The cache retention setting and the list of 11 cached variants totalling 78 KB" width="676" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing it from your app
&lt;/h3&gt;

&lt;p&gt;The same flow works through the &lt;a href="https://www.hubfly.space/docs/api/storage/assets-and-images/" rel="noopener noreferrer"&gt;Storage API&lt;/a&gt;. Here are the routes from the API reference, each under &lt;code&gt;https://api.hubfly.space/api/v1/projects/:projectId/storage/&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Key fields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Get a short-lived upload URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST objects/upload-session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bucket&lt;/code&gt;, &lt;code&gt;objectKey&lt;/code&gt;, &lt;code&gt;contentType&lt;/code&gt; → &lt;code&gt;upload_url&lt;/code&gt;, &lt;code&gt;expires_at&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload the bytes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PUT&lt;/code&gt; to &lt;code&gt;upload_url&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;before &lt;code&gt;expires_at&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register the asset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST assets/create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bucket&lt;/code&gt;, &lt;code&gt;objectKey&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt; → asset ID and URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make it public&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT assets/update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;assetId&lt;/code&gt;, &lt;code&gt;accessMode: "public"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share a private file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST assets/share-token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;assetId&lt;/code&gt;, &lt;code&gt;ttlSeconds&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The upload session matters: your browser code never sees your S3 secret, only a URL that expires.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The image comes back at full size.&lt;/strong&gt; You added parameters to the viewer page or the &lt;code&gt;/media&lt;/code&gt; link. Transforms only work on the &lt;code&gt;/image&lt;/code&gt; path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"unsupported or repeated image parameter".&lt;/strong&gt; The service rejects unknown and duplicate parameters. Don't pass your own tracking or cache-busting query strings to image URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"fit requires both dimensions" or a size error.&lt;/strong&gt; &lt;code&gt;cover&lt;/code&gt;, &lt;code&gt;contain&lt;/code&gt; and &lt;code&gt;fill&lt;/code&gt; need &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt;; each side must be 1–4,096 px and the output 16 MP or less.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A fourth one shows up later: each image allows &lt;strong&gt;100 distinct transforms per UTC day&lt;/strong&gt;. Pick a fixed set of sizes (say 400, 800, 1600) instead of the exact pixel width of every screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and pricing, honestly (October 2026)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transforms:&lt;/strong&gt; free. Variants are stored under &lt;code&gt;.hubfly-images/&lt;/code&gt; and count toward your storage like any other file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage rate:&lt;/strong&gt; the &lt;a href="https://www.hubfly.space/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; lists standard storage at $0.00001536 per GB-hour. Over a 730-hour month that's 0.00001536 × 730 = &lt;strong&gt;$0.0112 per GB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example:&lt;/strong&gt; 2,000 product photos at 1.5 MB is 3.0 GB. Four WebP sizes at an assumed 80 KB each (WebP product shots usually land between 20 and 100 KB) add 2,000 × 4 × 80 KB = 0.64 GB. Total 3.64 GB × $0.0112128 ≈ &lt;strong&gt;$0.04 a month&lt;/strong&gt;. A full 100 GB project quota would be about $1.12 a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth:&lt;/strong&gt; delivery is metered, and you can follow it under &lt;strong&gt;Activity &amp;amp; usage&lt;/strong&gt; in the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier:&lt;/strong&gt; you can sign up and build this without a card; the free tier also includes 360 compute hours a month for your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; traffic uses HTTPS, but object bodies are not encrypted at rest. Encrypt sensitive files in your app first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who it's not for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;AVIF&lt;/strong&gt; or animated output. Formats are WebP, JPEG and PNG, and animated sources use the first frame.&lt;/li&gt;
&lt;li&gt;You generate &lt;strong&gt;a different size per user or viewport&lt;/strong&gt;. The 100-per-day limit will bite; resize on upload instead.&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;private photos in long-lived embeds&lt;/strong&gt;. Protected links rely on an access key or short share links (15 minutes to 24 hours in the dashboard). A dedicated media CDN with signed URLs may suit you better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Upload once, describe the size in the URL, and let the cache do the rest. Keep a short list of sizes, keep private files Protected, and watch storage rather than per-image fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read next:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/multi-tenant-saas-isolated-tenants-hubfly-subaccounts?utm_source=devto&amp;amp;utm_campaign=resize-watermark-product-photos-url" rel="noopener noreferrer"&gt;Isolated SaaS tenants with sub-accounts&lt;/a&gt; · &lt;a href="https://blog.hubfly.space/deploy-docker-compose-as-is-8-platforms-prices?utm_source=devto&amp;amp;utm_campaign=resize-watermark-product-photos-url" rel="noopener noreferrer"&gt;Deploy a docker-compose.yml as-is&lt;/a&gt; · &lt;a href="https://blog.hubfly.space/heroku-alternatives-2026-prices?utm_source=devto&amp;amp;utm_campaign=resize-watermark-product-photos-url" rel="noopener noreferrer"&gt;Best Heroku alternatives in 2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the shop itself on Hubfly space Cell:&lt;/strong&gt; deploy your app and database from a &lt;a href="https://www.hubfly.space/docs/compose/" rel="noopener noreferrer"&gt;Compose file&lt;/a&gt; next to your images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bucket per customer:&lt;/strong&gt; if you sell a SaaS, give each tenant its own space with &lt;a href="https://www.hubfly.space/docs/subaccounts/" rel="noopener noreferrer"&gt;sub-accounts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video too:&lt;/strong&gt; the same Storage project turns uploads into HLS streams at 360p to 1080p (&lt;a href="https://www.hubfly.space/docs/storage/video/" rel="noopener noreferrer"&gt;video docs&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Start free on Hubfly space, 360 hours included →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>images</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Best Heroku alternatives in 2026: 7 options and what they cost</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:36:24 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/best-heroku-alternatives-in-2026-7-options-and-what-they-cost-15on</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/best-heroku-alternatives-in-2026-7-options-and-what-they-cost-15on</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/heroku-alternatives-2026-prices?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;Hubfly blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your Heroku bill is a dyno plus a database, and you want to know where to move and what it will cost. Heroku still works, and your app on it is fine today. But in February 2026 Heroku said it is moving to a "sustaining engineering" model, centered on upkeep and dependable operation, with new features no longer the priority, and it stopped offering Enterprise contracts to new customers (&lt;a href="https://www.heroku.com/blog/an-update-on-heroku/" rel="noopener noreferrer"&gt;Heroku's announcement&lt;/a&gt;). Card-paying customers keep the same pricing. Still, if you're starting something new, or your bill keeps growing, it's a good moment to look around.&lt;/p&gt;

&lt;p&gt;The problem: most "Heroku alternatives" lists give you a "starting at" price per platform and stop there. That's not what you pay. You pay for a web service &lt;strong&gt;plus&lt;/strong&gt; a database, and sometimes a workspace fee on top. This guide prices the same small app everywhere, so the numbers compare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space&lt;/strong&gt;&lt;/a&gt;: free tier, 360 compute hours a month, no card needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; the monthly cost of the same small app (one always-on web service with 512 MB of RAM plus a Postgres database) on Heroku and 7 alternatives, with every price linked to the vendor's own pricing page (prices as of 19 October 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone running a side project, an internal tool or an early SaaS on Heroku who wants to know where to go and what it will cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time needed:&lt;/strong&gt; 10 minutes to read, about 15 minutes to move a small app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short answer:&lt;/strong&gt; for a managed database, Render and DigitalOcean cost about the same as Heroku. For the lowest bill on a managed platform, Fly.io and Hubfly space land around $7.50 a month, but you run Postgres in a container yourself. If you're happy to be your own ops team, Coolify on one Hetzner server is the cheapest per gigabyte.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The reference app
&lt;/h2&gt;

&lt;p&gt;To compare Heroku alternatives fairly, every price below is for the same setup, the one most Heroku apps actually are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 web service&lt;/strong&gt;, always on, about 512 MB of RAM (the size of a Heroku Basic dyno).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 Postgres database&lt;/strong&gt;, small, with at least 1 GB of storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;730 hours&lt;/strong&gt; a month (a full month, always on).&lt;/li&gt;
&lt;li&gt;Lowest-priced region where a platform charges differently by region.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this matters: a price per container alone hides the database, which is often half the bill. And a "free" plan that sleeps after 15 minutes isn't the same product as an always-on one. So the main table is always-on, and free tiers get their own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison table (always on, per month)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Web service (512 MB)&lt;/th&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;Monthly total&lt;/th&gt;
&lt;th&gt;Database is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heroku&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic dyno &lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;$7&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Essential-0, 1 GB &lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;$5&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;512 MB instance &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;$7&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;256 MB RAM, 1 GB SSD &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;$6&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 vCPU, 512 MiB &lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;$5&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Dev database, 512 MiB &lt;a href="https://www.digitalocean.com/pricing/app-platform" rel="noopener noreferrer"&gt;$7&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed (dev tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.5 GB RAM, usage-based (&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;rates&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Same, plus 1 GB volume&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;about $12.33&lt;/strong&gt; (see math, quiet app)&lt;/td&gt;
&lt;td&gt;You run the template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fly.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;shared-cpu-1x, 512 MB &lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;$3.69&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Same machine size + 1 GB volume ($3.69 + $0.15)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$7.53&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You run it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fly.io + Managed Postgres&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;$3.69&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Basic &lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;$38&lt;/a&gt; + 1 GB at $0.28&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$41.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hubfly space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 vCPU, 512 MB, always on: $3.71&lt;/td&gt;
&lt;td&gt;Same size + 1 GB volume: $3.72&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$7.43&lt;/strong&gt; (see math)&lt;/td&gt;
&lt;td&gt;You run it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Koyeb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;eco-micro, 0.25 vCPU, 512 MB &lt;a href="https://www.koyeb.com/docs/reference/instances" rel="noopener noreferrer"&gt;$2.68&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;small, 1 GB RAM &lt;a href="https://www.koyeb.com/docs/databases" rel="noopener noreferrer"&gt;$29.76&lt;/a&gt; + 1 GB at $0.50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$32.94&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coolify on Hetzner CX23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One server, 2 vCPU, 4 GB: &lt;a href="https://docs.hetzner.com/general/infrastructure-and-availability/price-adjustment/" rel="noopener noreferrer"&gt;$6.49&lt;/a&gt; excl. IPv4&lt;/td&gt;
&lt;td&gt;Runs on the same server&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6.49 + IPv4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You run it, and the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All prices were read on the vendors' official pages on 19 October 2026, in USD, before tax. Prices change. Click through before you decide.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the usage-based numbers are worked out
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hubfly space&lt;/strong&gt; bills per resource: $0.004 per vCPU-hour, $0.002 per GB of RAM per hour, and $0.00001536 per GB of storage per hour (&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). For one always-on dedicated container with 1 vCPU, 512 MB of RAM and 5 GB of disk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU: 1 × $0.004 × 730 h = $2.92&lt;/li&gt;
&lt;li&gt;RAM: 0.5 GB × $0.002 × 730 h = $0.73&lt;/li&gt;
&lt;li&gt;Disk: 5 GB × $0.00001536 × 730 h = $0.056&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $3.71 a month per container&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of those (web and Postgres) come to $7.41. The Postgres volume adds 1 GB on Balanced storage (1.5× the base rate): 1 × $0.00001536 × 1.5 × 730 = $0.017. &lt;strong&gt;Grand total: $7.43.&lt;/strong&gt; The Stack Composer's &lt;strong&gt;Monthly Estimate&lt;/strong&gt; prices each service as you set it up, before you deploy (screenshots below). Drop each container to 0.5 vCPU and the total falls to about $4.51.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Railway&lt;/strong&gt; bills what your services actually use, per second: $0.00000386 per GB of RAM and $0.00000772 per vCPU (&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). A 730-hour month is 2,628,000 seconds. Assume each service holds 0.5 GB of RAM and averages 0.05 vCPU (a quiet app):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAM: 0.5 × $0.00000386 × 2,628,000 = $5.07 per service&lt;/li&gt;
&lt;li&gt;CPU: 0.05 × $0.00000772 × 2,628,000 = $1.01 per service&lt;/li&gt;
&lt;li&gt;Two services: $12.17, plus a 1 GB volume at $0.16 = &lt;strong&gt;about $12.33&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hobby plan's $5 minimum is included in that, not added. A busier app costs more; one that idles at 200 MB costs less. That's the point of usage pricing, and also why you can't know the bill in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fly.io&lt;/strong&gt; prices above are for Ashburn (&lt;code&gt;iad&lt;/code&gt;), one of its two lowest-priced regions. Fly lists higher prices elsewhere; a 1 GB machine is $6.70 in Ashburn and $7.73 in Frankfurt (&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Free tiers: what you get, and the catch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Free web service&lt;/th&gt;
&lt;th&gt;Free database&lt;/th&gt;
&lt;th&gt;The catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heroku&lt;/td&gt;
&lt;td&gt;None. Eco is &lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;$5&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Eco dynos sleep after 30 minutes of inactivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Yes, 750 hours a month (&lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Web spins down after 15 minutes without traffic and takes about a minute to wake. Free Postgres &lt;strong&gt;expires after 30 days&lt;/strong&gt;, with a 14-day grace period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;30-day trial with $5 credit, then $1 a month (&lt;a href="https://railway.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Within credit&lt;/td&gt;
&lt;td&gt;0.5 GB RAM per service and 0.5 GB of volume on Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fly.io&lt;/td&gt;
&lt;td&gt;Trial only: 2 hours of machine time or 7 days (&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Pay as you go after that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;Static sites only, up to 3 apps (&lt;a href="https://docs.digitalocean.com/products/app-platform/details/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No free container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Koyeb&lt;/td&gt;
&lt;td&gt;1 web service, Frankfurt or Washington (&lt;a href="https://www.koyeb.com/docs/reference/instances" rel="noopener noreferrer"&gt;instances&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;1 database, 5 hours of active time, 1 GB (&lt;a href="https://www.koyeb.com/docs/faqs/pricing" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Free instance scales to zero after 1 hour without traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hubfly space&lt;/td&gt;
&lt;td&gt;Shared tier, 360 compute hours a month, no card (&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Postgres runs as a container on the same free hours&lt;/td&gt;
&lt;td&gt;Shared containers sleep when idle. Volumes are still billed (1 GB is about $0.02 a month)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your app must answer instantly at 3 a.m., none of the free tiers is for you. Pay for always-on, on any platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each platform wins
&lt;/h2&gt;

&lt;p&gt;A fair comparison says where the others are better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heroku&lt;/strong&gt; is still the original "push and forget" experience, with a large &lt;a href="https://elements.heroku.com/addons" rel="noopener noreferrer"&gt;add-on marketplace&lt;/a&gt;. Card customers keep the same prices. Stay if your app is stable and the bill doesn't hurt; the main risk is fewer new features from here on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; works the most like Heroku: managed Postgres, the same web-service model, and a free tier for experiments. You pay a little more than Heroku for the same setup, and $0.15 per GB after 5 GB of bandwidth on Hobby (&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt; matches Heroku's price with a cheaper web container ($5) and a $7 dev database. Bandwidth overage is only $0.02 per GiB. Good if you already use DigitalOcean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Railway&lt;/strong&gt; has a visual project canvas and bills by the second for what you really use. Great for spiky or idle apps. Harder to predict a monthly bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fly.io&lt;/strong&gt; wins on regions: you can run the same app close to users on several continents, at $3.69 for a 512 MB machine in Ashburn (&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). Its managed Postgres is the most expensive line in this table, so most small apps run their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Koyeb&lt;/strong&gt; has the cheapest small container here ($2.68 for 512 MB). Its managed Postgres starts at $29.76 a month, so it's cheapest when the database lives elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coolify on Hetzner&lt;/strong&gt; gives you the most server for the money: 2 vCPU and 4 GB of RAM for $6.49. Coolify itself is &lt;a href="https://coolify.io/pricing" rel="noopener noreferrer"&gt;free to self-host&lt;/a&gt;. But you're the ops team: OS updates, backups, monitoring and the 3 a.m. disk-full alert are yours. One more catch on 19 October 2026: Hetzner's &lt;a href="https://www.hetzner.com/cloud/cost-optimized/" rel="noopener noreferrer"&gt;cost-optimized page&lt;/a&gt; listed the CX23 as "currently unavailable".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hubfly space&lt;/strong&gt; lands next to Fly.io on price ($7.43 always on, $4.51 at half a vCPU), with no per-seat, per-project or custom-domain fee. You can also set a monthly budget cap per project, so a runaway app stops instead of surprising you. Where it's weaker today: Postgres runs as a container you manage, and Hubfly doesn't yet document database backups or snapshots. If you need to run in many regions at once, Fly.io is the stronger pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving a Heroku app to Hubfly space in about 15 minutes
&lt;/h2&gt;

&lt;p&gt;Here's the path for the reference app: a web service and Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a project.&lt;/strong&gt; In the &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;, pick &lt;strong&gt;Cell&lt;/strong&gt;, choose a region and, under &lt;strong&gt;Advanced Options&lt;/strong&gt;, set a &lt;strong&gt;Monthly Budget Limit&lt;/strong&gt; (for example $10). A budget cap means a bug can't run up a big bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Describe your app in a Compose file.&lt;/strong&gt; Heroku's Procfile and add-ons become two services. Replace &lt;code&gt;your-image&lt;/code&gt; with your app's image, or deploy from Git (Hubfly builds from a root &lt;code&gt;Dockerfile&lt;/code&gt; or detects your stack from package files).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notes&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-d"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notes"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-image&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://app:change-me@{{service:db.host}}:5432/notes&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;change-me&lt;/code&gt; with a long random password (for example from &lt;code&gt;openssl rand -hex 24&lt;/code&gt;) and keep the real value out of Git: set it in the service's environment variables in the dashboard.&lt;/p&gt;

&lt;p&gt;Why the volume: it keeps your database files when the container restarts or redeploys. Without it, every update would wipe your data. Why &lt;code&gt;{{service:db.host}}&lt;/code&gt;: Hubfly fills in the database's private address, so the app never goes over the public internet to reach Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Import and check the price.&lt;/strong&gt; Click &lt;strong&gt;Create → Compose Stack&lt;/strong&gt;, open the &lt;strong&gt;YAML&lt;/strong&gt; tab, paste the file and click &lt;strong&gt;Import YAML&lt;/strong&gt;. The &lt;strong&gt;Monthly Estimate&lt;/strong&gt; shows the cost before anything runs. Both services start on the free shared tier.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfolv9safkt77tl2szf0.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfolv9safkt77tl2szf0.jpg" alt="Hubfly Stack Composer with the db and web services imported on the shared tier, Monthly Estimate $0.01" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Choose always-on, if you need it.&lt;/strong&gt; Click the &lt;code&gt;web&lt;/code&gt; service, open &lt;strong&gt;Resources&lt;/strong&gt; and switch &lt;strong&gt;Service tier&lt;/strong&gt; to &lt;strong&gt;Dedicated (Production)&lt;/strong&gt;. The default is 1 vCPU, 512 MB and 5 GB, with &lt;strong&gt;24x7 On&lt;/strong&gt;. The estimate updates right away.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wuztv8p6wo6kxrs4uc8.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wuztv8p6wo6kxrs4uc8.jpg" alt="The same stack with the web service on the dedicated tier, Monthly Estimate $3.72" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Deploy.&lt;/strong&gt; Click &lt;strong&gt;Deploy Stack&lt;/strong&gt;. The volume is created, Postgres deploys first, then the web service. The whole stack is up in under a minute.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb0998vgb197h093zvxoq.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb0998vgb197h093zvxoq.jpg" alt="Deployment log: volume ready, db deployed, service condition satisfied, web deployed, stack deployment finished in 39 seconds" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Give it a public URL.&lt;/strong&gt; Open the &lt;code&gt;web&lt;/code&gt; service, then &lt;strong&gt;Network → Add Domain → Auto Endpoint&lt;/strong&gt; and click &lt;strong&gt;Create Subdomain&lt;/strong&gt;. You get an HTTPS address like &lt;code&gt;your-word.eu1.hubfly.app&lt;/code&gt; within seconds. The &lt;code&gt;ports: "80:80"&lt;/code&gt; line only gives a raw TCP address; the Auto Endpoint is what gives you HTTPS. Add your own domain the same way under &lt;strong&gt;Custom Domain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Move your data.&lt;/strong&gt; Dump from Heroku with &lt;code&gt;heroku pg:backups:capture&lt;/code&gt; and &lt;code&gt;heroku pg:backups:download&lt;/code&gt;, then open a &lt;a href="https://www.hubfly.space/docs/tunnels/" rel="noopener noreferrer"&gt;HubTunnel&lt;/a&gt; to the new database (&lt;code&gt;hubfly tunnel create --project &amp;lt;project-id&amp;gt; --container &amp;lt;db-container-id&amp;gt; --port 5432 --local 15432&lt;/code&gt;) and restore: &lt;code&gt;pg_restore --no-owner --no-acl -h 127.0.0.1 -p 15432 -U app -d notes latest.dump&lt;/code&gt;. Switch your DNS when the data is in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. The app returns 502 Bad Gateway.&lt;/strong&gt; Heroku sets &lt;code&gt;PORT&lt;/code&gt; for you and your app listens on it. On Hubfly, the app must listen on &lt;code&gt;0.0.0.0&lt;/code&gt; and on the port you expose. An app bound to &lt;code&gt;127.0.0.1&lt;/code&gt; gives a 502. Fix: bind to &lt;code&gt;0.0.0.0&lt;/code&gt; (&lt;a href="https://blog.hubfly.space/docker-502-bad-gateway-bind-0-0-0-0?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;full fix&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ports: "80:80"&lt;/code&gt; gives you a TCP address, not a website.&lt;/strong&gt; Importing a Compose file with &lt;code&gt;ports&lt;/code&gt; creates a raw TCP endpoint on &lt;code&gt;tcp.eu1.hubfly.app&lt;/code&gt;. For HTTPS, add an &lt;strong&gt;Auto Endpoint&lt;/strong&gt; or a custom domain as in step 6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The web service starts before the database is ready.&lt;/strong&gt; Don't count on &lt;code&gt;depends_on: condition: service_healthy&lt;/code&gt; to hold the web service back. Add a connection retry with backoff in your app, so it keeps trying until Postgres accepts connections, and again after any restart (&lt;a href="https://blog.hubfly.space/docker-health-checks-postgres-redis?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;health checks explained&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Bonus: Hubfly doesn't read a Procfile. Put your start command in the Dockerfile or in &lt;strong&gt;Configuration → Process Settings&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want a managed Postgres and no ops?&lt;/strong&gt; Render (about $13) or DigitalOcean (about $12). Same price range as Heroku.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want the lowest bill on a managed platform, and can run Postgres in a container?&lt;/strong&gt; Fly.io ($7.53) or Hubfly space ($7.43, or $4.51 at half a vCPU), with a budget cap on Hubfly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want the most server per dollar and don't mind ops?&lt;/strong&gt; Coolify on Hetzner ($6.49 plus IPv4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bill depends on traffic and you like per-second pricing?&lt;/strong&gt; Railway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just need the cheapest container, with the database elsewhere?&lt;/strong&gt; Koyeb ($2.68).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read next: &lt;a href="https://blog.hubfly.space/heroku-vs-render-vs-railway-vs-hubfly-pricing?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;Heroku vs Render vs Railway vs Hubfly: 4 prices for one app + Postgres&lt;/a&gt;, &lt;a href="https://blog.hubfly.space/deploy-docker-compose-as-is-8-platforms-prices?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;Deploy a docker-compose.yml as-is: 8 platforms compared&lt;/a&gt; and &lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;Gate your app on a Postgres migration job&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ship a new version to 10% of users first&lt;/strong&gt; with a weighted load balancer: &lt;a href="https://blog.hubfly.space/canary-release-weighted-load-balancer?utm_source=devto&amp;amp;utm_campaign=heroku-alternatives-2026-prices" rel="noopener noreferrer"&gt;canary releases guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep uploads and images out of your app container&lt;/strong&gt; with S3-compatible &lt;a href="https://www.hubfly.space/docs/" rel="noopener noreferrer"&gt;Storage&lt;/a&gt; buckets and on-the-fly image resizing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock an admin panel behind a login&lt;/strong&gt; at the edge with &lt;a href="https://www.hubfly.space/docs/identity-access/" rel="noopener noreferrer"&gt;Identity Access&lt;/a&gt;, no code changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Moving from Heroku?&lt;/strong&gt; Paste your stack into the Stack Composer and see the monthly estimate before you deploy anything. &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Start free on Hubfly space&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>heroku</category>
      <category>devops</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Docker health checks that catch real problems, explained simply</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:20:03 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/docker-health-checks-that-catch-real-problems-explained-simply-50m</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/docker-health-checks-that-catch-real-problems-explained-simply-50m</guid>
      <description>&lt;p&gt;&lt;em&gt;A plain guide to Docker health checks for your app, Postgres (pg_isready) and Redis, and three mistakes that make a check pass when it shouldn't.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your app starts, tries to reach the database, and crashes because Postgres is still waking up. Or worse: everything looks "healthy", but Redis is rejecting every request because of a password typo. A health check is a small command that answers one question, over and over: &lt;em&gt;is this service actually ready to do its job?&lt;/em&gt; Get it right and you can see when a service is truly ready, broken ones are easy to spot, and your users stop seeing errors during deploys.&lt;/p&gt;

&lt;p&gt;In this guide you'll add working health checks to a web app, Postgres and Redis, deploy them as one stack, and learn the three mistakes that make a health check say "all good" when it isn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.hubfly.space/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=health-checks" rel="noopener noreferrer"&gt;&lt;strong&gt;Try it on Hubfly space&lt;/strong&gt;&lt;/a&gt;: the whole stack runs on the free shared tier (360 hours a month, no card needed).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a Compose file with checks for Postgres (&lt;code&gt;pg_isready&lt;/code&gt;), Redis (&lt;code&gt;redis-cli ping | grep -q PONG&lt;/code&gt;) and a web app (&lt;code&gt;wget&lt;/code&gt;, shown with nginx), deployed in dependency order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone running more than one container, from side projects to small production apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 15 minutes. The stack deploys in about a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three traps:&lt;/strong&gt; &lt;code&gt;redis-cli&lt;/code&gt; exits 0 even when the password is wrong; &lt;code&gt;pg_isready&lt;/code&gt; can pass while Postgres is still running its first-boot scripts; and the image may not contain the tool your check calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a health check is (and what it isn't)
&lt;/h2&gt;

&lt;p&gt;A health check is a command that runs &lt;strong&gt;inside&lt;/strong&gt; the container on a timer. Exit code &lt;code&gt;0&lt;/code&gt; means healthy. Anything else counts as a failure. After a set number of failures in a row, the container is marked unhealthy.&lt;/p&gt;

&lt;p&gt;Every check has four timing settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Docker default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How often the check runs&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long one run may take before it counts as failed&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Failures in a row before "unhealthy"&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start_period&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grace time after start when failures don't count&lt;/td&gt;
&lt;td&gt;0s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things a health check does &lt;strong&gt;not&lt;/strong&gt; do on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't restart anything.&lt;/strong&gt; A container that fails its check keeps running. Restart policies react to a process &lt;em&gt;exiting&lt;/em&gt;, not to a failed check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't fix your app's startup.&lt;/strong&gt; It tells other services, and you, when something is ready. Your app should still retry its database connection for a few seconds on startup, because networks blink.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why it matters: without checks, "the container is running" is the only signal you have, and a running container can be completely useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write a check for Postgres
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shop&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shop"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why each piece is there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pg_isready&lt;/code&gt;&lt;/strong&gt; ships with the Postgres image, so there's nothing to install. It asks the server "are you accepting connections?" and exits 0 when the answer is yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-h 127.0.0.1&lt;/code&gt;&lt;/strong&gt; is the important bit. On first boot, the official image starts a temporary Postgres that listens &lt;strong&gt;only on a local socket&lt;/strong&gt; while it runs your init scripts. A plain &lt;code&gt;pg_isready&lt;/code&gt; checks that socket and can pass too early. Forcing TCP with &lt;code&gt;-h 127.0.0.1&lt;/code&gt; waits for the real server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;start_period: 20s&lt;/code&gt;&lt;/strong&gt; gives a fresh database time to initialize before failures count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The volume&lt;/strong&gt; keeps your data when the container restarts or redeploys. Without it, every update would start from an empty database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Write a check for Redis (and avoid the exit-0 trap)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;redis-server&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--requirepass&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$REDIS_PASSWORD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDISCLI_AUTH=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$REDIS_PASSWORD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;redis-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ping&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;grep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PONG"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the syntax for the Hubfly space Stack Composer. In a local &lt;code&gt;docker compose&lt;/code&gt; file, write &lt;code&gt;$$REDIS_PASSWORD&lt;/code&gt; instead (see "What can go wrong" #2).&lt;/p&gt;

&lt;p&gt;Most examples online use &lt;code&gt;redis-cli ping&lt;/code&gt; on its own. That check passes even when Redis is refusing you. Run it against a password-protected Redis and look at the exit code:&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2hppunxkczcwhfl3m1w.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2hppunxkczcwhfl3m1w.png" alt="Console output: redis-cli ping without a password prints NOAUTH and still exits 0; with the wrong password it prints WRONGPASS and still exits 0" width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redis-cli&lt;/code&gt; prints the error and &lt;strong&gt;still exits 0&lt;/strong&gt;. Your check would say "healthy" while every real request fails. The fix is to check for the actual answer: &lt;code&gt;| grep -q PONG&lt;/code&gt; exits 1 unless Redis really replied &lt;code&gt;PONG&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;REDISCLI_AUTH&lt;/code&gt; instead of &lt;code&gt;-a password&lt;/code&gt;: &lt;code&gt;redis-cli&lt;/code&gt; reads the password from this variable, so it doesn't show up in the process list or the shell history.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;exec&lt;/code&gt; in the command: it makes &lt;code&gt;redis-server&lt;/code&gt; the main process, so it receives stop signals directly and shuts down cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write a check for your web app
&lt;/h2&gt;

&lt;p&gt;Add a small &lt;code&gt;/health&lt;/code&gt; route to your app that returns 200 when it can reach what it needs. Then check it from inside the container:&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;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app:latest&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-O&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/dev/null&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:3000/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool the image actually has.&lt;/strong&gt; Alpine-based images include &lt;code&gt;wget&lt;/code&gt; (from BusyBox); many, like &lt;code&gt;redis:7-alpine&lt;/code&gt;, have no &lt;code&gt;curl&lt;/code&gt;, while &lt;code&gt;nginx:alpine&lt;/code&gt; has both. Run &lt;code&gt;which curl wget&lt;/code&gt; in the container to be sure. Slim Debian images (&lt;code&gt;python:3.13-slim&lt;/code&gt;, &lt;code&gt;node:22-slim&lt;/code&gt;) have neither. For those, call your runtime instead, for example &lt;code&gt;python -c "import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:3000/health', timeout=2)"&lt;/code&gt;. Distroless and &lt;code&gt;scratch&lt;/code&gt; images have no shell at all, so build a tiny check into your app binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;127.0.0.1&lt;/code&gt;, not your public URL.&lt;/strong&gt; The check runs inside the container. Testing the public URL measures your proxy and DNS, not your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; sets the order: &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt; first, &lt;code&gt;web&lt;/code&gt; last. In local Docker Compose it also holds &lt;code&gt;web&lt;/code&gt; back until both checks pass. On Hubfly space, the order is the same, but don't count on the gate to wait for a passing check: a dependency can be marked ready within seconds of deploying. Your app's connection retry is what actually protects you, so keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Deploy the stack on Hubfly space
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://dashboard.hubfly.space/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=health-checks" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, click &lt;strong&gt;Create Project&lt;/strong&gt;. Name it (for example &lt;code&gt;shop-api&lt;/code&gt;), pick a region, and optionally open &lt;strong&gt;Advanced Options&lt;/strong&gt; to set a monthly budget cap such as $10.&lt;/li&gt;
&lt;li&gt;In the project, click &lt;strong&gt;Create → Compose Stack&lt;/strong&gt;, open the &lt;strong&gt;YAML&lt;/strong&gt; tab, paste the full file below, and click &lt;strong&gt;Import YAML&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy Stack&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The file uses &lt;code&gt;nginx:alpine&lt;/code&gt; as a stand-in web server so you can deploy it exactly as written. To use your own app, change &lt;code&gt;image&lt;/code&gt; and point the check at your &lt;code&gt;/health&lt;/code&gt; route and port, for example &lt;code&gt;http://127.0.0.1:3000/health&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shop&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shop"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;redis-server&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--requirepass&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$REDIS_PASSWORD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDISCLI_AUTH=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;$REDIS_PASSWORD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;redis-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ping&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;grep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PONG"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-O&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/dev/null&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;change-me&lt;/code&gt; with real passwords before you deploy anything that matters. Note that &lt;code&gt;--requirepass&lt;/code&gt; on the command line is visible in the container's process list; for production, put the password in a Redis config file on a volume instead.&lt;/p&gt;

&lt;p&gt;The deploy log shows the order: the volume first, then &lt;code&gt;db&lt;/code&gt;, then &lt;code&gt;cache&lt;/code&gt;, and &lt;code&gt;web&lt;/code&gt; last, once both dependencies have deployed. The whole stack takes about a minute.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47uzek2by55f5jr6fzi0.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47uzek2by55f5jr6fzi0.png" alt="The Compose deployment log: volume ready, db deployed in stage 1, cache in stage 2, and web waiting for db before it deploys" width="800" height="464"&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0f3ylzp275siwscahbcx.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0f3ylzp275siwscahbcx.png" alt="The project canvas with db, cache and web running on the internal network" width="765" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Make sure each check can fail
&lt;/h2&gt;

&lt;p&gt;Don't trust a check you've never seen fail. Open each container, go to the &lt;strong&gt;Console&lt;/strong&gt; tab, and run its test command by hand:&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;# In the db container&lt;/span&gt;
pg_isready &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-U&lt;/span&gt; shop &lt;span class="nt"&gt;-d&lt;/span&gt; shop&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"check exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
pg_isready &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 5433&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"wrong port exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# In the cache container&lt;/span&gt;
sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping | grep -q PONG'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"check exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
redis-cli ping | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; PONG&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"without password exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# In the web container&lt;/span&gt;
wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /dev/null http://127.0.0.1/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"check exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /dev/null http://127.0.0.1/missing&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"missing page exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want &lt;code&gt;0&lt;/code&gt; for each real check and a non-zero code for each deliberately broken one. Now you know the check can tell the difference.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7h5wxktnnjh4b4mxwrx.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7h5wxktnnjh4b4mxwrx.png" alt="Console output in the db container: pg_isready accepts connections with exit 0, and gets no response with exit 2 on a wrong port" width="800" height="117"&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0d7cpmy2rny8bvjvcofs.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0d7cpmy2rny8bvjvcofs.png" alt="Console output in the cache container: the real check exits 0, the check without a password exits 1, and the image has wget but no curl" width="800" height="117"&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz43id9j5alaqqnvqp3rn.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz43id9j5alaqqnvqp3rn.png" alt="Console output in the web container: the check exits 0, and a missing page returns 404 and exits 1" width="800" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The container's status badge doesn't always show the check result, so the Console is the reliable place to look.&lt;/p&gt;

&lt;p&gt;You can change a check later on a single container: open it, go to &lt;strong&gt;Configuration → Health Check → Edit&lt;/strong&gt;, update the test command, interval, timeout, start period or retries, and click &lt;strong&gt;Save Changes&lt;/strong&gt;. The container restarts with the new settings.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj063g758ylxwluw3ewuz.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj063g758ylxwluw3ewuz.png" alt="The Health Check editor on a container's Configuration tab" width="799" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. The check passes, but the service is broken.&lt;/strong&gt; Classic cause: &lt;code&gt;redis-cli ping&lt;/code&gt; without &lt;code&gt;| grep -q PONG&lt;/code&gt;, or an HTTP check that treats any response as success. Fix: check for the exact good answer, then prove it fails by running a broken version by hand (Step 5).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your password variable turns into nonsense.&lt;/strong&gt; In a local &lt;code&gt;docker compose&lt;/code&gt; file you must write &lt;code&gt;$$REDIS_PASSWORD&lt;/code&gt;, because Compose replaces single &lt;code&gt;$&lt;/code&gt; variables with values from your laptop's environment. The Hubfly space Stack Composer passes your command to the shell exactly as written, so there you write a single &lt;code&gt;$REDIS_PASSWORD&lt;/code&gt;. If you paste &lt;code&gt;$$REDIS_PASSWORD&lt;/code&gt; into Hubfly, the shell reads &lt;code&gt;$$&lt;/code&gt; as its process ID and Redis ends up with a password like &lt;code&gt;1REDIS_PASSWORD&lt;/code&gt;. The symptom: your app gets &lt;code&gt;WRONGPASS&lt;/code&gt; with the password you set. Worse, a failing check doesn't stop the stack from deploying, and the container keeps running, so nothing looks wrong at first. Fix: use &lt;code&gt;$&lt;/code&gt; on Hubfly, &lt;code&gt;$$&lt;/code&gt; locally, and check with &lt;code&gt;REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping&lt;/code&gt; in the Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "executable file not found" or the check always fails.&lt;/strong&gt; The image doesn't contain &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt; or even a shell. Fix: run &lt;code&gt;which curl wget&lt;/code&gt; in the Console, then pick a tool the image has (Step 3). For &lt;code&gt;scratch&lt;/code&gt; or distroless images, add a &lt;code&gt;/health&lt;/code&gt; subcommand to your binary and use the exec form: &lt;code&gt;["CMD", "/app", "health"]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A health check is only useful if it can fail. Check for the real answer (&lt;code&gt;PONG&lt;/code&gt;, a 200 from &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;pg_isready&lt;/code&gt; over TCP), give slow starters a &lt;code&gt;start_period&lt;/code&gt;, use a tool the image actually contains, and run every check by hand once. Pair that with a connection retry in your app, and deploys stop being the moment your users see errors.&lt;/p&gt;

&lt;p&gt;Related guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=docker-health-checks-postgres-redis" rel="noopener noreferrer"&gt;Run a database migration before your app starts&lt;/a&gt;, the next step after health checks.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/docker-502-bad-gateway-bind-0-0-0-0?utm_source=devto&amp;amp;utm_campaign=docker-health-checks-postgres-redis" rel="noopener noreferrer"&gt;Why your app shows a 502 error in Docker (and how to fix it)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hubfly.space/deploy-docker-compose-as-is-8-platforms-prices?utm_source=devto&amp;amp;utm_campaign=docker-health-checks-postgres-redis" rel="noopener noreferrer"&gt;Deploy a docker-compose.yml as-is&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Paste the Compose file from Step 4 into a new project on &lt;a href="https://dashboard.hubfly.space/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=health-checks" rel="noopener noreferrer"&gt;Hubfly space&lt;/a&gt;. On the free shared tier, the dashboard projects about $0.02 a month for this stack. That's the 1 GB database volume; shared runtime is free within 360 hours a month, and sleeping containers use no compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go further on Hubfly space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send traffic only to healthy copies of your app:&lt;/strong&gt; put a &lt;a href="https://www.hubfly.space/docs/load-balancers/" rel="noopener noreferrer"&gt;load balancer&lt;/a&gt; in front of two or more &lt;code&gt;web&lt;/code&gt; containers. It uses health probes to route around a target that stops answering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship a release slowly:&lt;/strong&gt; send 10% of traffic to a new version first with a &lt;a href="https://blog.hubfly.space/canary-release-weighted-load-balancer?utm_source=devto&amp;amp;utm_campaign=docker-health-checks-postgres-redis" rel="noopener noreferrer"&gt;weighted load balancer&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach your database from your laptop without a public port:&lt;/strong&gt; open a short-lived &lt;a href="https://www.hubfly.space/docs/tunnels/" rel="noopener noreferrer"&gt;tunnel&lt;/a&gt; with &lt;code&gt;hubfly tunnel create&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>postgres</category>
      <category>redis</category>
    </item>
    <item>
      <title>Deploy a docker-compose.yml as-is: 8 platforms, which keys survive, and what web + Postgres + Redis costs</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:14:13 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/deploy-a-docker-composeyml-as-is-8-platforms-which-keys-survive-and-what-web-postgres-redis-5f0i</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/deploy-a-docker-composeyml-as-is-8-platforms-which-keys-survive-and-what-web-postgres-redis-5f0i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/deploy-docker-compose-as-is-8-platforms-prices?utm_source=devto&amp;amp;utm_campaign=deploy-docker-compose-as-is-8-platforms-prices" rel="noopener noreferrer"&gt;Hubfly blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; which platforms take your &lt;code&gt;docker-compose.yml&lt;/code&gt; without a rewrite, which Compose keys survive, and a monthly price for the same 3-service stack (web + Postgres + Redis) on 8 platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; you have a Compose file that works on your laptop and you want it online without learning a new config format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; 10 minutes to read. 2 minutes to try it on Hubfly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short answer:&lt;/strong&gt; Hubfly runs the file as-is; Coolify's docs say it runs Compose natively; Railway imports it (some keys may not carry over). Render, DigitalOcean App Platform, Heroku and Koyeb want a rewrite. Cheapest always-on stack: about $6–12/month if you run Postgres yourself, $15–27 if you want them managed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: your Compose file works, until you try to host it
&lt;/h2&gt;

&lt;p&gt;You have &lt;code&gt;docker compose up&lt;/code&gt; working. Web, Postgres, Redis, a healthcheck, two volumes. Then you pick a host, and it wants a &lt;code&gt;render.yaml&lt;/code&gt;, an app spec, a &lt;code&gt;fly.toml&lt;/code&gt; or three separate services clicked together by hand.&lt;/p&gt;

&lt;p&gt;Most "deploy Docker Compose" guides skip the two questions that matter. Will my file run unchanged? And what will the whole stack cost per month, not just the web box?&lt;/p&gt;

&lt;p&gt;This post answers both, from each platform's docs and pricing page, plus a real Compose deploy on Hubfly with a stopwatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reference stack
&lt;/h2&gt;

&lt;p&gt;One small web service that waits for a healthy Postgres and a healthy Redis. Both data services have named volumes. A bind mount is included on purpose, to show what a platform does with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik/whoami:v1.10&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./static:/srv:ro&lt;/span&gt;        &lt;span class="c1"&gt;# bind mount, on purpose&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;   &lt;span class="c1"&gt;# use a secret in real life&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-server"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--appendonly"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redisdata:/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redisdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which platforms take a Compose file?
&lt;/h2&gt;

&lt;p&gt;Docs and pricing pages are linked in each row and in the price table below. "As-is" means you hand over the file, not a translation of it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Compose support&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;What the docs say about keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Coolify&lt;/strong&gt; (self-hosted)&lt;/td&gt;
&lt;td&gt;Yes, native&lt;/td&gt;
&lt;td&gt;Git repo or "Raw" mode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;build:&lt;/code&gt;, named volumes, healthchecks supported; bind mounts only inside the resource folder; avoid &lt;code&gt;ports:&lt;/code&gt; (&lt;a href="https://coolify.io/docs/applications/build-packs/docker-compose" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hubfly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, native&lt;/td&gt;
&lt;td&gt;Paste or upload YAML in the Stack Composer&lt;/td&gt;
&lt;td&gt;Walkthrough below: &lt;code&gt;depends_on&lt;/code&gt; conditions, healthchecks, named volumes, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;ports&lt;/code&gt; kept; bind mount dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, import&lt;/td&gt;
&lt;td&gt;Drag the file onto the project canvas&lt;/td&gt;
&lt;td&gt;Railway says it doesn't support every Compose config yet; per-key list not published (&lt;a href="https://docs.railway.com/guides/dockerfiles" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fly.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Early&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[build.compose]&lt;/code&gt; in &lt;code&gt;fly.toml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Announced in a community post (July 2025), not in the main docs; many features listed as incompatible (&lt;a href="https://community.fly.io/t/docker-compose-compatibility-the-journey-begins/25285" rel="noopener noreferrer"&gt;post&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Rewrite as a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint&lt;/td&gt;
&lt;td&gt;Compose isn't in the &lt;a href="https://render.com/docs/blueprint-spec" rel="noopener noreferrer"&gt;Blueprint spec&lt;/a&gt;; the &lt;a href="https://feedback.render.com/features/p/support-docker-compose" rel="noopener noreferrer"&gt;feature request&lt;/a&gt; dates from 2019&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;App spec, from Git or images&lt;/td&gt;
&lt;td&gt;Compose not listed (&lt;a href="https://docs.digitalocean.com/products/app-platform/support/" rel="noopener noreferrer"&gt;support page&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Koyeb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workaround&lt;/td&gt;
&lt;td&gt;Docker-in-Docker in one privileged service&lt;/td&gt;
&lt;td&gt;Official &lt;a href="https://koyeb.com/tutorials/deploy-apps-using-docker-compose-on-koyeb" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; uses the Privileged toggle; no native import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heroku&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Git or Docker deploys&lt;/td&gt;
&lt;td&gt;Compose isn't offered on the &lt;a href="https://www.heroku.com/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fair warning for every row: "supports Compose" never means every key. Swarm-only &lt;code&gt;deploy:&lt;/code&gt; blocks, host networking and bind mounts are where files usually break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hubfly, step by step
&lt;/h2&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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2Fdc5b9e8d-2eb0-4d3f-ac0a-b3f945fa6b0a.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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2Fdc5b9e8d-2eb0-4d3f-ac0a-b3f945fa6b0a.jpg" alt="Hubfly Stack Composer after importing the Compose YAML: 3 services, 2 volumes, $0.02 monthly estimate" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Stack Composer after pasting the YAML: 3 services, 2 volumes, monthly estimate $0.02.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import.&lt;/strong&gt; Create → Compose Stack → YAML tab → paste → Import YAML. It loaded in under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What survives.&lt;/strong&gt; Read the generated spec (JSON tab):

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; → kept on &lt;code&gt;web&lt;/code&gt; for both &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both &lt;code&gt;healthcheck&lt;/code&gt; blocks → kept, with interval, timeout and retries.&lt;/li&gt;
&lt;li&gt;Named volumes → 2 volumes of 1 GB each, mounted at the right paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;command:&lt;/code&gt; on Redis → kept.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports: "80:80"&lt;/code&gt; → container port 80 exposed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bind mount &lt;code&gt;./static:/srv:ro&lt;/code&gt; → silently dropped.&lt;/strong&gt; No warning in the composer. If your app needs files from the host, bake them into the image or use a volume.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy.&lt;/strong&gt; Click Deploy Stack. "Stack deployment finished" arrives in about &lt;strong&gt;79 seconds&lt;/strong&gt; for 3 services and 2 volumes (the deploy page timer, which starts after validation, shows about 1 min 17 s).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; The log is the useful part:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;db&lt;/code&gt; deployed (10:54:10), then &lt;code&gt;cache&lt;/code&gt; (10:54:26).&lt;/li&gt;
&lt;li&gt;"Waiting for 'db' before deploying 'web'" → "Service condition satisfied" 2 s later.&lt;/li&gt;
&lt;li&gt;Same for &lt;code&gt;cache&lt;/code&gt; 3 s later. Then &lt;code&gt;web&lt;/code&gt; deployed (10:54:48).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2F257b5cf5-f5fc-428b-809f-a67b8b19cd7c.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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2F257b5cf5-f5fc-428b-809f-a67b8b19cd7c.jpg" alt="Compose deployment activity log: web waits for db and cache health conditions before deploying" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The deploy activity log: web waits for db and cache to become healthy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One oddity: the composer labels stages in file order. &lt;code&gt;web&lt;/code&gt; shows as "Stage 1" even though it deploys last. The real order followed &lt;code&gt;depends_on&lt;/code&gt;, so trust the log, not the stage number.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost on the dashboard.&lt;/strong&gt; The project summary projected &lt;strong&gt;$0.03/month&lt;/strong&gt;, with the shared tier's auto-sleep on.&lt;/li&gt;
&lt;/ol&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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2F364c3cca-3e47-4b69-a5c5-19518902552a.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%2Feager-gecko.eu1.hubfly.app%2Fblog-images%2Fblog%2F2026%2F10%2F364c3cca-3e47-4b69-a5c5-19518902552a.jpg" alt="Project billing panel: projected cost $0.03 per month against a $10 monthly cap" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Project Settings → Billing &amp;amp; Monthly Budget: projected $0.03/mo against a $10 cap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The composer shows $0.02, the project page $0.03. The gap is most likely the storage class: the volumes were created as Balanced (1.5× the standard rate). 2 GB × $0.00001536 × 1.5 × 730 h = &lt;strong&gt;$0.034&lt;/strong&gt;, which rounds to $0.03; at the standard rate it's $0.022. Shared-tier compute shows as free here. Treat that as a dev and staging number, not a production price.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; Project Settings → Danger Zone → Delete All Resources, then Delete Project. The whole run takes about 7 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the same stack costs, platform by platform
&lt;/h2&gt;

&lt;p&gt;Assumptions: always on, 730 hours a month, the smallest paid size each platform sells for web, Postgres and Redis, 1 GB of data each, no egress. Usage-based platforms are priced at 0.5 vCPU + 512 MB per service (or the nearest size), which is a ceiling, not an average.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Web&lt;/th&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;th&gt;Monthly total&lt;/th&gt;
&lt;th&gt;Managed DB?&lt;/th&gt;
&lt;th&gt;Pricing page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coolify + Hetzner CX23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;one box&lt;/td&gt;
&lt;td&gt;on the box&lt;/td&gt;
&lt;td&gt;on the box&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$6.49 + IPv4&lt;/strong&gt; ($11.49 + IPv4 with Coolify Cloud)&lt;/td&gt;
&lt;td&gt;No, you run it&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.hetzner.com/general/infrastructure-and-availability/price-adjustment/" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt;, &lt;a href="https://coolify.io/pricing" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Hubfly&lt;/strong&gt; (dedicated, always on)&lt;/td&gt;
&lt;td&gt;$2.19&lt;/td&gt;
&lt;td&gt;$2.19 + volume&lt;/td&gt;
&lt;td&gt;$2.19 + volume&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ $6.67&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No, containers + volumes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly pricing&lt;/a&gt;, math below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Fly.io&lt;/strong&gt;, self-run DBs&lt;/td&gt;
&lt;td&gt;$3.69&lt;/td&gt;
&lt;td&gt;$3.69 + $0.15 vol&lt;/td&gt;
&lt;td&gt;$3.69 + $0.15 vol&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$11.37&lt;/strong&gt; (+$2 for a dedicated IPv4)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heroku&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$7 Basic&lt;/td&gt;
&lt;td&gt;$5 Mini&lt;/td&gt;
&lt;td&gt;$3 Mini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.heroku.com/pricing" rel="noopener noreferrer"&gt;Dynos&lt;/a&gt;, &lt;a href="https://help.heroku.com/1CDF2VHY/what-are-your-cheapest-heroku-postgres-and-heroku-data-for-redis-plans" rel="noopener noreferrer"&gt;Postgres + Key-Value&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$7&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;$10 Key Value&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;Render pricing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$7 Dev DB&lt;/td&gt;
&lt;td&gt;$15 Valkey&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$27&lt;/strong&gt; ($35.15 with a full managed Postgres)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.digitalocean.com/products/app-platform/details/pricing/" rel="noopener noreferrer"&gt;App Platform&lt;/a&gt;, &lt;a href="https://docs.digitalocean.com/products/databases/valkey/details/pricing/" rel="noopener noreferrer"&gt;Valkey&lt;/a&gt;, &lt;a href="https://www.digitalocean.com/pricing/managed-databases" rel="noopener noreferrer"&gt;Databases&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Koyeb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$5.36 micro&lt;/td&gt;
&lt;td&gt;$30.26 (Small $29.76 + 1 GB at $0.50)&lt;/td&gt;
&lt;td&gt;$1.61 eco-nano, self-run&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$37.23&lt;/strong&gt; (a plan fee may apply; check the pricing page)&lt;/td&gt;
&lt;td&gt;Postgres yes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.koyeb.com/docs/reference/instances" rel="noopener noreferrer"&gt;Instances&lt;/a&gt;, &lt;a href="https://www.koyeb.com/pricing" rel="noopener noreferrer"&gt;Postgres pricing&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;$15 + $0.15 vol&lt;/td&gt;
&lt;td&gt;$15 + $0.15 vol&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;up to ≈ $45.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No, services you run&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.railway.com/reference/pricing/plans" rel="noopener noreferrer"&gt;Plans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Fly.io&lt;/strong&gt;, managed&lt;/td&gt;
&lt;td&gt;$3.69&lt;/td&gt;
&lt;td&gt;$38.28 MPG Basic&lt;/td&gt;
&lt;td&gt;$10 Upstash fixed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$51.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;Fly pricing&lt;/a&gt;, &lt;a href="https://fly.io/docs/upstash/redis/" rel="noopener noreferrer"&gt;Upstash on Fly&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How to read Railway's row: it bills real usage, per second. $45.30 assumes all three services use their full 0.5 vCPU and 512 MB around the clock ($20/vCPU-month × 0.5 + $10/GB-month × 0.5 = $15 each). An idle side project uses far less, and the $5 Hobby fee covers the first $5. An idle Railway bill depends on your app, so it isn't quoted here.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hubfly math, so you can check it
&lt;/h3&gt;

&lt;p&gt;Published rates (&lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;): CPU $0.004 per vCPU-hour, RAM $0.002 per GB-hour, storage $0.00001536 per GB-hour, 730 hours a month.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One service at 0.5 vCPU / 512 MB: 0.5 × 0.004 × 730 = &lt;strong&gt;$1.46&lt;/strong&gt; CPU, plus 0.5 × 0.002 × 730 = &lt;strong&gt;$0.73&lt;/strong&gt; RAM → &lt;strong&gt;$2.19&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Three services: 3 × $2.19 = &lt;strong&gt;$6.57&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Two 1 GB Balanced volumes: 2 × 0.00001536 × 1.5 × 730 = &lt;strong&gt;$0.034&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Container disk, 2 GB each (the dedicated tier's minimum in the dashboard): 6 × 0.00001536 × 730 = &lt;strong&gt;$0.067&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total ≈ $6.67/month.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This row is computed from published dedicated-tier rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the competitors win (and they do)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed databases.&lt;/strong&gt; Heroku, Render and DigitalOcean run Postgres and Redis for you. If you'd rather never think about Postgres upgrades or restores, the $15–27 tier is worth it. Render's $6 Postgres includes point-in-time recovery on the Hobby workspace, per its &lt;a href="https://render.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;. On Hubfly, Coolify and self-run Fly, Postgres is a container you own, and backups are your job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coolify runs Compose closest to raw&lt;/strong&gt;, including &lt;code&gt;build:&lt;/code&gt; from your repo, and one flat-priced server. You also patch that server yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway's import is drag-and-drop:&lt;/strong&gt; drop the file on the canvas and review the staged services (&lt;a href="https://docs.railway.com/guides/dockerfiles" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fly's 256 MB Machine is $2.19/month&lt;/strong&gt;, and Fly lists 17 regions in its &lt;a href="https://docs.fly.io/about/pricing" rel="noopener noreferrer"&gt;pricing selector&lt;/a&gt;, against Hubfly's 3. Koyeb's eco-nano ($1.61) is the cheapest always-on container in this table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heroku's cheapest managed Postgres + Redis pair costs $8&lt;/strong&gt; together, the lowest managed pair in this table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where Hubfly fits: your Compose file ran unchanged, &lt;code&gt;depends_on&lt;/code&gt; health conditions were respected, and a 3-service always-on stack is about $6.67 by the published rates. The trade-off is that you manage the database containers. Hubfly doesn't document database backups, so schedule your own &lt;code&gt;pg_dump&lt;/code&gt; to off-site storage.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your bind mounts disappear.&lt;/strong&gt; Hubfly dropped &lt;code&gt;./static:/srv:ro&lt;/code&gt; without a warning, and Coolify restricts them to the resource folder. Fix: &lt;code&gt;COPY&lt;/code&gt; the files into the image, or use a named volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app starts before the database is ready.&lt;/strong&gt; &lt;code&gt;depends_on&lt;/code&gt; without a condition only waits for the container to start. Fix: add a &lt;code&gt;healthcheck&lt;/code&gt; and &lt;code&gt;condition: service_healthy&lt;/code&gt;, as above. For migrations, gate the app on a one-shot job (see the &lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=deploy-docker-compose-as-is-8-platforms-prices" rel="noopener noreferrer"&gt;migration job guide&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 after a "successful" deploy.&lt;/strong&gt; Your app is listening on 127.0.0.1 inside the container. Fix: bind to &lt;code&gt;0.0.0.0&lt;/code&gt; (&lt;a href="https://blog.hubfly.space/docker-502-bad-gateway-bind-0-0-0-0?utm_source=devto&amp;amp;utm_campaign=deploy-docker-compose-as-is-8-platforms-prices" rel="noopener noreferrer"&gt;1-line fix&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus trap: a hard-coded &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; in Compose is fine on a laptop and bad in a repo. Use the platform's secret or encrypted env vars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a Cell project with a $10 budget cap (Advanced Options at project creation).&lt;/li&gt;
&lt;li&gt;Create → Compose Stack → YAML → paste the file above (change the password) → Import YAML.&lt;/li&gt;
&lt;li&gt;Check the JSON tab to see what survived. Deploy Stack and watch the activity log.&lt;/li&gt;
&lt;li&gt;Delete All Resources, then Delete Project when you're done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For competitor prices, open the linked pricing pages: prices change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you want your Compose file to run with no rewrite, your real shortlist is Coolify (your own server) or a platform with native import like Hubfly or Railway. If you want managed Postgres and Redis and don't mind translating your file, Heroku ($15) and Render ($23) are the honest picks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://blog.hubfly.space/canary-release-weighted-load-balancer?utm_source=devto&amp;amp;utm_campaign=deploy-docker-compose-as-is-8-platforms-prices" rel="noopener noreferrer"&gt;Canary releases with a weighted load balancer&lt;/a&gt;, once your stack is live and you want to ship v2 safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price your own stack:&lt;/strong&gt; open the &lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;Hubfly pricing calculator&lt;/a&gt;, set 3 dedicated services at 0.5 vCPU / 512 MB, and compare with the table above.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Isolated SaaS tenants on Hubfly: 1 wallet and 1 key per customer in 4 API calls</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:57:26 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/isolated-saas-tenants-on-hubfly-1-wallet-and-1-key-per-customer-in-4-api-calls-h2n</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/isolated-saas-tenants-on-hubfly-1-wallet-and-1-key-per-customer-in-4-api-calls-h2n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://blog.hubfly.space/multi-tenant-saas-isolated-tenants-hubfly-subaccounts?utm_source=devto&amp;amp;utm_campaign=multi-tenant-saas-isolated-tenants-hubfly-subaccounts" rel="noopener noreferrer"&gt;Hubfly blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a pattern for giving each customer their own wallet, projects and machine key on Hubfly, using sub-accounts. Three control-plane calls per customer, then the normal API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; SaaS teams, agencies and AI-agent builders who run infrastructure &lt;em&gt;for&lt;/em&gt; their customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 10 minutes to read. Creating, suspending and revoking a tenant in the dashboard takes about 2 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: one customer's bill is everyone's bill
&lt;/h2&gt;

&lt;p&gt;You run a SaaS. Each customer gets a database, a worker or a preview app. On most clouds, all of that lands in one account and one bill.&lt;/p&gt;

&lt;p&gt;Then customer 1042's import job loops all weekend. You find out from your card statement. You also can't tell which customer owes what, and one leaked key reaches every tenant.&lt;/p&gt;

&lt;p&gt;The textbook answer is the "silo" model: a separate account per customer. On AWS this usually means AWS Organizations and an account-vending process. Most guides stop at the diagram. This post shows the actual calls on Hubfly, and the limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hubfly sub-accounts are
&lt;/h2&gt;

&lt;p&gt;A sub-account is a tenant, not a team member. It has no password, no login and no invitations. Your software talks to it with a machine key (&lt;a href="https://www.hubfly.space/docs/subaccounts" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Each tenant gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Its own wallet&lt;/strong&gt;, with paid and promotional balances, in integer micro-USD (&lt;code&gt;1000000&lt;/code&gt; = $1.00).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its own projects&lt;/strong&gt;, which are ordinary Hubfly projects: containers, Compose, volumes, domains, firewall, logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its own keys.&lt;/strong&gt; An &lt;code&gt;hfsk_&lt;/code&gt; key is locked to one tenant. A parent &lt;code&gt;hfpk_&lt;/code&gt; key can manage all your tenants and target one with the &lt;code&gt;X-HubFly-Subaccount&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;externalRef&lt;/code&gt;&lt;/strong&gt;, unique inside your parent, so you can map &lt;code&gt;customer_1042&lt;/code&gt; in your database to &lt;code&gt;sub_…&lt;/code&gt; in Hubfly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs list the use cases plainly: SaaS customer infrastructure, AI agents, managed hosting, preview environments and internal developer portals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your app (parent hfpk_ key)
 ├── sub_A  wallet $25 ── project ── containers, volumes, domains
 ├── sub_B  wallet $10 ── project ── ...
 └── sub_C  suspended  (keys disabled)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Walkthrough: the dashboard flow
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Profile → Sub-accounts&lt;/strong&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbt217j2wushn2lwk8bx.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbt217j2wushn2lwk8bx.jpg" alt="The Create sub-account dialog with Account name and External reference fields" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create.&lt;/strong&gt; Click &lt;strong&gt;New sub-account&lt;/strong&gt;, type a name such as &lt;code&gt;acme-corp&lt;/code&gt; and an external reference such as &lt;code&gt;tenant_001&lt;/code&gt;, and click &lt;strong&gt;Create sub-account&lt;/strong&gt;. It shows as &lt;strong&gt;Active&lt;/strong&gt; with a &lt;code&gt;sub_…&lt;/code&gt; ID within seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect.&lt;/strong&gt; &lt;strong&gt;Manage&lt;/strong&gt; opens a panel with the wallet balance ($0.00), active keys, status, a &lt;strong&gt;Fund wallet&lt;/strong&gt; box ("Move paid balance from the parent account"), &lt;strong&gt;Machine credentials&lt;/strong&gt; and &lt;strong&gt;Suspend account&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4135zlpkrqj9quj8nkt5.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4135zlpkrqj9quj8nkt5.jpg" alt="Tenant panel: wallet $0.00, 0 active keys, status Active" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key (the surprise).&lt;/strong&gt; &lt;strong&gt;New key&lt;/strong&gt; has no form. The dashboard immediately creates a key called &lt;strong&gt;Master key&lt;/strong&gt; and shows the &lt;code&gt;hfsk_&lt;/code&gt; secret once, with "HubFly stores only a hash of this key and cannot recover it later." If you don't need it, close the dialog and click &lt;strong&gt;Revoke&lt;/strong&gt;. The banner says "API key revoked." and active keys go back to 0. Note that &lt;strong&gt;Revoke&lt;/strong&gt; also has no confirmation step.&lt;/li&gt;
&lt;/ol&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v7nvlbyk7pgrm73ua56.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v7nvlbyk7pgrm73ua56.jpg" alt="Banner " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Suspend.&lt;/strong&gt; &lt;strong&gt;Suspend account&lt;/strong&gt; works in one click, again with no confirmation. Status changes to &lt;strong&gt;Suspended&lt;/strong&gt;, &lt;strong&gt;New key&lt;/strong&gt; greys out, and the button becomes &lt;strong&gt;Resume account&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9es6iwd9zrwsp8673250.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9es6iwd9zrwsp8673250.jpg" alt="Tenant status Suspended with a Resume account button" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything above costs $0.00. Funding the wallet and closing a tenant happen through the API below; the dashboard has no Close button, so a tenant you stop using stays suspended until you close it by API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production flow: 4 calls to onboard, 1 to close
&lt;/h2&gt;

&lt;p&gt;The dashboard is fine for a look around. For real customers, provision from your backend with a parent platform key. These come from the &lt;a href="https://www.hubfly.space/docs/api/subaccounts" rel="noopener noreferrer"&gt;Sub-account API guide&lt;/a&gt;; all paths are under &lt;code&gt;https://api.hubfly.space/api/v1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the tenant.&lt;/strong&gt; Use an idempotency key, so a retry returns the same tenant instead of a duplicate.&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;-X&lt;/span&gt; POST https://api.hubfly.space/api/v1/subaccounts/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HUBFLY_PLATFORM_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: tenant-customer-1042-v1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"Customer 1042","externalRef":"customer_1042"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Fund its wallet.&lt;/strong&gt; This moves paid balance from your parent wallet. Promotional credit can't be transferred.&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;-X&lt;/span&gt; POST https://api.hubfly.space/api/v1/subaccounts/&lt;span class="nv"&gt;$SUBACCOUNT_ID&lt;/span&gt;/wallet/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HUBFLY_PLATFORM_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: funding-customer-1042-2026-10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amountMicroUsd":"25000000","idempotencyKey":"funding-customer-1042-2026-10"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Mint a tenant key&lt;/strong&gt; and write it straight to your secret manager. It's shown once.&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;-X&lt;/span&gt; POST https://api.hubfly.space/api/v1/subaccounts/&lt;span class="nv"&gt;$SUBACCOUNT_ID&lt;/span&gt;/api-keys/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HUBFLY_PLATFORM_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"production workload"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Use the normal endpoints.&lt;/strong&gt; There are no special tenant URLs. With the tenant key, &lt;code&gt;GET /projects&lt;/code&gt; lists only that tenant's projects. With your platform key, add &lt;code&gt;-H "X-HubFly-Subaccount: $SUBACCOUNT_ID"&lt;/code&gt;. The TypeScript and Go SDKs and the CLI (&lt;code&gt;--subaccount sub_…&lt;/code&gt; or &lt;code&gt;HUBFLY_SUBACCOUNT&lt;/code&gt;) support the same pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Close when the customer leaves.&lt;/strong&gt; Stop and remove the tenant's projects, settle any negative balance, then call &lt;code&gt;POST /subaccounts/$SUBACCOUNT_ID/close&lt;/code&gt;. Eligible paid balance returns to your parent wallet; promotional balance expires; keys are revoked.&lt;/p&gt;

&lt;p&gt;Isolation rules: an &lt;code&gt;hfsk_&lt;/code&gt; key sent with a different tenant header gets &lt;code&gt;403 SUBACCOUNT_SCOPE&lt;/code&gt;, and a platform key can't target a tenant outside its parent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and pricing (honest version)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Limits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;25 active sub-accounts per parent and 10 active keys per parent or tenant, by default.&lt;/li&gt;
&lt;li&gt;One level deep: a tenant can't create tenants. You can't move a tenant to another parent.&lt;/li&gt;
&lt;li&gt;No GPU, no GitHub OAuth repo discovery, no team members or invitations, no payment methods inside a tenant.&lt;/li&gt;
&lt;li&gt;No automatic top-up for tenant wallets in the first release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suspending doesn't stop running resources.&lt;/strong&gt; The docs say to stop billable resources before you suspend.&lt;/li&gt;
&lt;li&gt;Closing needs no active projects and no negative balance. Force close isn't available yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; sub-accounts have no listed fee. A tenant pays normal Hubfly rates from its own wallet: $0.004 per vCPU-hour, $0.002 per GB-hour of RAM, $0.00001536 per GB-hour of Standard storage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per tenant, always on (730 h)&lt;/th&gt;
&lt;th&gt;Math&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 vCPU, 0.5 GB RAM, 5 GB disk&lt;/td&gt;
&lt;td&gt;2.92 + 0.73 + 0.06&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.71&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5 vCPU, 0.5 GB RAM, 5 GB disk&lt;/td&gt;
&lt;td&gt;1.46 + 0.73 + 0.06&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2.25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25 tenants on the first size&lt;/td&gt;
&lt;td&gt;25 × $3.706&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$92.65&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Whether each tenant gets its own 360 free hours isn't documented, so don't price your plans on it. Per the &lt;a href="https://www.hubfly.space/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, sleeping containers pause compute billing; volumes are still billed.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You mint keys by accident.&lt;/strong&gt; One click on &lt;strong&gt;New key&lt;/strong&gt; creates a key named Master key. Create keys from your backend, name them per workload, and revoke old ones after rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You suspend a tenant and it keeps billing.&lt;/strong&gt; Suspension disables credentials and changes, not running containers. Stop the tenant's workloads first, then suspend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A customer passes someone else's tenant ID.&lt;/strong&gt; Never accept a &lt;code&gt;subaccountId&lt;/code&gt; from the client. Resolve it from your own customer record, and alert on &lt;code&gt;403 SUBACCOUNT_SCOPE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;409 API_KEY_LIMIT_REACHED&lt;/code&gt;.&lt;/strong&gt; A tenant holds 10 active keys by default. Revoke unused keys before creating another.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus: an ambiguous funding failure. Don't retry with a new idempotency key until you've read the wallet; you could fund twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is NOT for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your own team.&lt;/strong&gt; For colleagues, use an organization with roles. Sub-accounts have no logins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU workloads per customer.&lt;/strong&gt; Not supported in tenants today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More than 25 customers, today, with no plan.&lt;/strong&gt; That's the default cap. Talk to us first if you need more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers who must hold their own bill.&lt;/strong&gt; Tenants have no payment methods; you fund them from your paid balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you sell software that runs on infrastructure, sub-accounts give you the silo model without account vending: one wallet, one project namespace and one revocable key per customer. Provision from your backend with idempotency keys, keep keys out of the dashboard, and stop workloads before you suspend.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://blog.hubfly.space/coding-agent-deploy-safely-hubfly?utm_source=devto&amp;amp;utm_campaign=multi-tenant-saas-isolated-tenants-hubfly-subaccounts" rel="noopener noreferrer"&gt;Let your coding agent deploy to Hubfly safely&lt;/a&gt; (give each agent its own tenant), &lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=multi-tenant-saas-isolated-tenants-hubfly-subaccounts" rel="noopener noreferrer"&gt;Gate your app on a Postgres migration job&lt;/a&gt; and &lt;a href="https://blog.hubfly.space/docker-502-bad-gateway-bind-0-0-0-0?utm_source=devto&amp;amp;utm_campaign=multi-tenant-saas-isolated-tenants-hubfly-subaccounts" rel="noopener noreferrer"&gt;502 Bad Gateway in Docker? Bind to 0.0.0.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building a platform on Hubfly?&lt;/strong&gt; &lt;a href="https://www.hubfly.space/" rel="noopener noreferrer"&gt;Talk to sales&lt;/a&gt; about tenant limits, or &lt;a href="https://dashboard.hubfly.space/" rel="noopener noreferrer"&gt;start free with 360 hours included&lt;/a&gt; and create your first sub-account in Profile → Sub-accounts.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>api</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Canary releases with a weighted load balancer: 10% to v2, proven with 200 requests</title>
      <dc:creator>octave Nkurunziza</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:56:13 +0000</pubDate>
      <link>https://dev.to/octave_nkurunziza_afb0512/canary-releases-with-a-weighted-load-balancer-10-to-v2-proven-with-200-requests-46ed</link>
      <guid>https://dev.to/octave_nkurunziza_afb0512/canary-releases-with-a-weighted-load-balancer-10-to-v2-proven-with-200-requests-46ed</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you get:&lt;/strong&gt; a canary release that sends exactly 1 in 10 requests to a new version, a 7-line script that proves the split, and a rollback that takes effect in under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone running plain containers (no Kubernetes, no service mesh) who wants to ship v2 to a slice of users first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; about 15 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've got v2 ready. The tests pass. Your stomach still says "not for everyone at once."&lt;/p&gt;

&lt;p&gt;A canary release fixes that: v2 gets a small slice of real traffic, you watch it, then you turn the dial up or pull it out. Setting a 90/10 split takes a minute. Checking that you actually got one is the part people skip, and a 90/10 setting can quietly give you something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a weighted canary works
&lt;/h2&gt;

&lt;p&gt;A load balancer sits in front of two backends. Each backend gets a weight. With weights 9 and 1, a weighted round robin sends 9 requests to v1, then 1 to v2, and repeats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           ┌──────────────────────┐
Internet → │  load balancer (WRR) │ ──9──→ app-v1 (stable)
           │  canary-router       │ ──1──→ app-v2 (canary)
           └──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things decide whether this works in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The strategy must actually read the weights.&lt;/strong&gt; Some strategies ignore them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky strategies change who sees v2.&lt;/strong&gt; IP hashing pins each client to one backend, so a 10% canary means "10% of clients", and you may never be one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback must be one action.&lt;/strong&gt; If pulling v2 out takes a redeploy, it isn't a canary, it's a hope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Build it on Hubfly, then check all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: deploy v1 and v2 side by side
&lt;/h2&gt;

&lt;p&gt;Here, both versions are &lt;a href="https://hub.docker.com/r/hashicorp/http-echo" rel="noopener noreferrer"&gt;&lt;code&gt;hashicorp/http-echo&lt;/code&gt;&lt;/a&gt;, a tiny server that replies with a fixed string. That makes counting trivial: every response says &lt;code&gt;v1&lt;/code&gt; or &lt;code&gt;v2&lt;/code&gt;. In real life, these are your two image tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-v1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/http-echo:1.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-listen=:5678"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-text=v1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;app-v2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/http-echo:1.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-listen=:5678"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-text=v2"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Hubfly dashboard: open your project, click &lt;strong&gt;Create → Compose Stack&lt;/strong&gt;, open the &lt;strong&gt;YAML&lt;/strong&gt; tab, paste the file, click &lt;strong&gt;Import YAML&lt;/strong&gt;, then &lt;strong&gt;Deploy Stack&lt;/strong&gt;. Neither service publishes a port. The load balancer will be the only way in.&lt;/p&gt;

&lt;p&gt;The stack is up in about 33 seconds.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmanxsjm60gec3mb7h6ms.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmanxsjm60gec3mb7h6ms.png" alt="Compose deployment complete: app-v1 and app-v2 deployed in 33 s" width="799" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: create the load balancer
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Create → Create Load Balancer&lt;/strong&gt;. The editor has three columns: ingress domains on the left, the balancer in the middle, backend targets on the right.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Balancer name:&lt;/strong&gt; &lt;code&gt;canary-router&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend targets → Add Target Container:&lt;/strong&gt; pick &lt;code&gt;app-v1&lt;/code&gt;, set &lt;strong&gt;Port&lt;/strong&gt; to &lt;code&gt;5678&lt;/code&gt;, &lt;strong&gt;Weight&lt;/strong&gt; to &lt;code&gt;9&lt;/code&gt;, click &lt;strong&gt;Add Target&lt;/strong&gt;. Repeat for &lt;code&gt;app-v2&lt;/code&gt; with weight &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingress domains → Add Domain → Hubfly Free → Attach Free Domain.&lt;/strong&gt; You get an HTTPS subdomain like &lt;code&gt;canary-router-xx-xxx.eu1.hubfly.app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategy:&lt;/strong&gt; choose &lt;strong&gt;Weighted Round Robin&lt;/strong&gt;. Not "Round Robin". This is the step the rest of this post is about.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy Load Balancer&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch the port: the target form defaults to &lt;code&gt;3000&lt;/code&gt;. This app listens on &lt;code&gt;5678&lt;/code&gt;, so leaving the default points the balancer at a closed port.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58sjof1bpgsipoqlch8v.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58sjof1bpgsipoqlch8v.png" alt="Strategy dropdown: Round Robin, Least Connections, IP Hash, Weighted Round Robin, Weighted Least Connections, Weighted IP Hash, Random, Weighted Random" width="260" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hubfly's &lt;a href="https://www.hubfly.space/docs/load-balancers/" rel="noopener noreferrer"&gt;load balancer docs&lt;/a&gt; describe three strategies (&lt;code&gt;round_robin&lt;/code&gt;, &lt;code&gt;least_conn&lt;/code&gt;, &lt;code&gt;ip_hash&lt;/code&gt;) and a canary recipe with &lt;code&gt;weight: 9&lt;/code&gt; and &lt;code&gt;weight: 1&lt;/code&gt;. The dashboard offers eight. Only the weighted strategies apply your weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: prove the split (don't trust the percentages)
&lt;/h2&gt;

&lt;p&gt;Open your load balancer's domain in a browser tab. It shows &lt;code&gt;v1&lt;/code&gt; or &lt;code&gt;v2&lt;/code&gt;. Then open the browser console (F12 → Console) on that tab and paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/?n=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;t=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends 200 sequential requests from the same origin, skips the cache, and counts each status-plus-body pair. A run takes about 80 seconds (median 402 ms per request).&lt;/p&gt;

&lt;p&gt;Here is what you get with the same two containers and the same 9/1 weights, changing only the strategy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Targets / weights&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;v1&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;th&gt;v2 share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Round Robin&lt;/td&gt;
&lt;td&gt;app-v1 w9, app-v2 w1 (dashboard showed 90% / 10%)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;61%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round Robin, 2nd run&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round Robin&lt;/td&gt;
&lt;td&gt;3 × v1 + 1 × v2, all weight 1&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weighted Round Robin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;app-v1 w9, app-v2 w1&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weighted IP Hash&lt;/td&gt;
&lt;td&gt;app-v1 w9, app-v2 w1, one client&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every response was a clean &lt;code&gt;v1&lt;/code&gt; or &lt;code&gt;v2&lt;/code&gt; reply, with no errors.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh707nbhnlvtu5xlzhhch.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh707nbhnlvtu5xlzhhch.png" alt="Split results for each load-balancer strategy" width="799" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What the table says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round Robin ignores your weights.&lt;/strong&gt; The target cards still show "90%" and "10%", which is what makes it dangerous. A repeat run settles into strict alternation: v1, v2, v1, v2. Your "10% canary" is a coin flip. (A first run can start with 15 v1 responses, then a long v2 streak, then the same alternation; that run gave v2 61%.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weighted Round Robin is exact.&lt;/strong&gt; v2 answered request 1, 11, 21 and so on. Precisely 20 of 200. No randomness, so you don't need thousands of requests to trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target count also works.&lt;/strong&gt; Three v1 containers and one v2 under plain Round Robin gave exactly 25%. That's the fallback if you're on a balancer without weights, but it costs you extra containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP hash pins you.&lt;/strong&gt; With Weighted IP Hash, all 40 requests from a single browser go to v1. That's what you want for sessions. It's terrible for checking your canary from your own laptop.&lt;/li&gt;
&lt;/ul&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffhzbdugpwdxkcrf3j6x.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffhzbdugpwdxkcrf3j6x.png" alt="Load balancer set to Weighted Round Robin, app-v1 weight 9 (90%), app-v2 weight 1 (10%)" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: watch v2, then promote or roll back
&lt;/h2&gt;

&lt;p&gt;While v2 takes its 10%, watch two numbers per version: error rate and latency. On Hubfly, each container has its own &lt;strong&gt;Logs&lt;/strong&gt; and &lt;strong&gt;Metrics&lt;/strong&gt; tabs, so you can check &lt;code&gt;app-v1&lt;/code&gt; and &lt;code&gt;app-v2&lt;/code&gt; separately. One &lt;a href="https://oneuptime.com/blog/post/2026-01-24-traefik-canary-deployments/view" rel="noopener noreferrer"&gt;Traefik canary guide&lt;/a&gt; rolls back when the canary's 5xx rate passes 5%, and alerts when p95 latency stays above 2 seconds for 5 minutes. Pick numbers that match your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Promote:&lt;/strong&gt; raise &lt;code&gt;app-v2&lt;/code&gt;'s weight in steps (for example 1 → 3 → 9 against v1's 9), checking each step with the script. When v2 carries everything, remove &lt;code&gt;app-v1&lt;/code&gt; from the targets. The balancer details panel edits weights live, and every step uses the same control. The balancer also health-checks targets with lightweight requests to paths like &lt;code&gt;/health&lt;/code&gt; and drops a failing one from rotation, as described in the &lt;a href="https://www.hubfly.space/docs/load-balancers/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;. Give v2 a real health endpoint so that check means something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roll back:&lt;/strong&gt; open the balancer, click the trash icon on the &lt;code&gt;app-v2&lt;/code&gt; target. With a request loop running, the last v2 response comes back about 0.66 s after the click, and none of the 108 requests in that loop fail.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. "I set 90/10 and v2 gets half the traffic."&lt;/strong&gt;&lt;br&gt;
Your strategy is plain Round Robin. It ignores weights, even though the UI shows percentages. Pick the Weighted version of whichever strategy you use, then rerun the script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "I can't reproduce the bug users report on v2."&lt;/strong&gt;&lt;br&gt;
You're on an IP-hash strategy, and your IP hashes to v1. Weights then decide &lt;em&gt;which clients&lt;/em&gt; land on v2, not which requests. Test v2 directly before adding it to the balancer, or temporarily use Weighted Round Robin while you test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "v2 never answers after I add it."&lt;/strong&gt;&lt;br&gt;
Check the target port. The form defaults to &lt;code&gt;3000&lt;/code&gt;. If your container listens elsewhere (here: &lt;code&gt;5678&lt;/code&gt;), the balancer is knocking on a closed door. Also check that v2 is running before you add it.&lt;/p&gt;

&lt;p&gt;Two smaller gotchas from the dashboard: the &lt;strong&gt;+&lt;/strong&gt; button next to a weight steps by 5 (1 → 6 → 11), and scrolling the mouse wheel over a weight slider changes it. Re-read the weights before you close the panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost
&lt;/h2&gt;

&lt;p&gt;Four shared-tier containers plus the balancer project to &lt;strong&gt;$0.00/month&lt;/strong&gt; in the dashboard. The shared tier counts against the free 360 compute hours a month.&lt;/p&gt;

&lt;p&gt;For a production-sized canary, say both versions as dedicated containers with 1 vCPU, 0.5 GB RAM and 5 GB disk, each one costs about $3.71/month always on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU: 1 × $0.004 × 730 h = $2.92&lt;/li&gt;
&lt;li&gt;RAM: 0.5 × $0.002 × 730 h = $0.73&lt;/li&gt;
&lt;li&gt;Disk: 5 × $0.00001536 × 730 h ≈ $0.06&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The canary only exists during the rollout. Running v2 next to v1 for a 24-hour canary adds about $0.12.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Setting a weight is not the same as getting a split. Pick a weighted strategy, then count. Two hundred requests and one table tell you more than the dashboard's percentages do.&lt;/p&gt;

&lt;p&gt;Read next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.hubfly.space/docker-502-bad-gateway-bind-0-0-0-0?utm_source=devto&amp;amp;utm_campaign=canary-release-weighted-load-balancer" rel="noopener noreferrer"&gt;502 Bad Gateway in Docker? Bind to 0.0.0.0, not 127.0.0.1&lt;/a&gt;: if your new target returns 502s, start here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.hubfly.space/docker-compose-migration-job-service-completed-successfully?utm_source=devto&amp;amp;utm_campaign=canary-release-weighted-load-balancer" rel="noopener noreferrer"&gt;Gate your app on a Postgres migration job&lt;/a&gt;: schema changes are the main reason a rollback isn't safe. Make them backward-compatible before the canary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.hubfly.space/coding-agent-deploy-safely-hubfly?utm_source=devto&amp;amp;utm_campaign=canary-release-weighted-load-balancer" rel="noopener noreferrer"&gt;Let your coding agent deploy to Hubfly, not delete your data&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Finished with the canary?&lt;/strong&gt; Delete the load balancer separately. Danger Zone → Delete All Resources removes the containers but leaves the balancer behind.&lt;/p&gt;

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

&lt;p&gt;Paste the Compose file above into a new Hubfly project, add a load balancer with &lt;strong&gt;Weighted Round Robin&lt;/strong&gt; and weights 9 and 1, and run the script. The free tier needs no card, and this setup projects to $0.00/month. &lt;a href="https://dashboard.hubfly.space" rel="noopener noreferrer"&gt;Start on Hubfly&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>loadbalancing</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
