<?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: Viktor Rozenko</title>
    <description>The latest articles on DEV Community by Viktor Rozenko (@sharpvik).</description>
    <link>https://dev.to/sharpvik</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%2F4131712%2Fa93ba93a-a02e-488d-90c2-a55dea42cb68.png</url>
      <title>DEV Community: Viktor Rozenko</title>
      <link>https://dev.to/sharpvik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sharpvik"/>
    <language>en</language>
    <item>
      <title>Pure cinema</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:59:21 +0000</pubDate>
      <link>https://dev.to/sharpvik/pure-cinema-66k</link>
      <guid>https://dev.to/sharpvik/pure-cinema-66k</guid>
      <description>&lt;p&gt;Most of what we ship is there to make something work, or stop something going&lt;br&gt;
wrong. A refusal that waits for a human. A graph that is also the firewall. A&lt;br&gt;
rollback that never puts back an old password. Important, and none of it is&lt;br&gt;
much fun to watch.&lt;/p&gt;

&lt;p&gt;This one is fun to watch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gagarin.cloud/blog/pure-cinema" rel="noopener noreferrer"&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%2Fnde91ggsm2o865inzq3c.jpg" alt="Screen recording: Claude Code deploys the chit-chat recipe on the left; on the right the Gagarin console, with LIVE on, draws a Postgres, an API and a web service as they arrive, then turns them green." width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Twenty-six seconds, nothing sped up. On the left, Claude Code is deploying a project. On the right, the console has LIVE on and is left alone. &lt;a href="https://gagarin.cloud/blog/pure-cinema" rel="noopener noreferrer"&gt;Watch the recording on gagarin.cloud.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are looking at
&lt;/h2&gt;

&lt;p&gt;On the left, Claude Code working through a deploy: a Postgres, an API, a web&lt;br&gt;
page. On the right, the project page in the console, empty when the clip&lt;br&gt;
starts. The agent runs &lt;code&gt;gg resource add&lt;/code&gt;, and a Postgres plate fades in on the&lt;br&gt;
right. It ships the API, and a second plate arrives above the first with an&lt;br&gt;
arrow down into it. It ships the web service, the picture slides&lt;br&gt;
to make room, and a third plate lands on top with its &lt;code&gt;public&lt;/code&gt; tag. The three&lt;br&gt;
read &lt;code&gt;RESTARTING&lt;/code&gt; for a moment, then one by one turn green, and the line under&lt;br&gt;
the project name goes from "nothing running" to "3 of 3 drifting" to&lt;br&gt;
"3 services, in agreement".&lt;/p&gt;

&lt;p&gt;Nobody pressed refresh. Nobody touched the console at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is done, briefly
&lt;/h2&gt;

&lt;p&gt;There is no feed. The engine answers a status read and pushes nothing, so LIVE&lt;br&gt;
is a re-read on a clock: every five seconds the page asks again, through the&lt;br&gt;
same session and the same path as opening it would. The next read waits for&lt;br&gt;
the last one to finish, so a slow cluster slows the clock rather than piling&lt;br&gt;
reads on itself. A tab you are not looking at reads nothing, and catches up the&lt;br&gt;
moment you look back. The switch is remembered per browser, and it is off&lt;br&gt;
until you turn it on, because a status read asks the cluster about every&lt;br&gt;
workload and we would rather not do that to everybody.&lt;/p&gt;

&lt;p&gt;The motion is the part we spent the time on. A re-read that redrew the graph&lt;br&gt;
from scratch would snap: a new service would appear fully formed and&lt;br&gt;
everything under it would jump a rank. Instead a plate that stays slides from&lt;br&gt;
where it was to where it now belongs, with its arrows following. A new one&lt;br&gt;
fades in at its place. One that was destroyed fades out where it stood. When a&lt;br&gt;
plate's reading changes, it rings once in its own colour. The ledger on the&lt;br&gt;
left does the same: rows arrive and leave, and a cost or a replica count that&lt;br&gt;
moves flashes as it does. If your system asks for reduced motion, all of it&lt;br&gt;
settles without the choreography.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Because the whole pitch of this platform is that you hand a repository to an&lt;br&gt;
agent and it comes back with an address, and that pitch has a weak moment in&lt;br&gt;
the middle. For a minute or two the agent is running commands and you are&lt;br&gt;
reading a transcript, trusting that each line did what it says.&lt;/p&gt;

&lt;p&gt;LIVE turns that minute into something you can see. The agent says it added a&lt;br&gt;
database, and a database appears. It says web now reaches api, and an arrow is&lt;br&gt;
drawn. It says everything is up, and everything is green. The transcript and&lt;br&gt;
the picture agree, or they do not, and you know which before the agent tells&lt;br&gt;
you.&lt;/p&gt;

&lt;p&gt;That is a real thing, and it is still not why we built it. We built it because&lt;br&gt;
the first time we watched a project assemble itself on screen while an agent&lt;br&gt;
typed, everyone in the room went quiet, and that seemed worth keeping.&lt;/p&gt;

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

&lt;p&gt;Open any project at &lt;a href="https://my.gagarin.cloud" rel="noopener noreferrer"&gt;my.gagarin.cloud&lt;/a&gt;, press LIVE&lt;br&gt;
in the bar above the graph, and tell your coding agent to deploy something&lt;br&gt;
into it. If you have nothing to hand, the &lt;a href="https://gagarin.cloud/recipes/chit-chat" rel="noopener noreferrer"&gt;chit-chat recipe&lt;/a&gt;&lt;br&gt;
in the recording is a Postgres, an API and a web page, and takes an agent&lt;br&gt;
about a minute.&lt;/p&gt;

&lt;p&gt;Then sit back. It is the most relaxing minute in infrastructure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagarin.cloud/blog/pure-cinema" rel="noopener noreferrer"&gt;gagarin.cloud&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>cloud</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Leaving Railway in an afternoon</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:59:11 +0000</pubDate>
      <link>https://dev.to/sharpvik/leaving-railway-in-an-afternoon-2d4n</link>
      <guid>https://dev.to/sharpvik/leaving-railway-in-an-afternoon-2d4n</guid>
      <description>&lt;p&gt;&lt;a href="https://gagarin.cloud/blog/gagarin-vs-railway" rel="noopener noreferrer"&gt;Gagarin vs Railway&lt;/a&gt; says where Railway is the better&lt;br&gt;
pick. Read that first if you have not decided. This post is for the afternoon&lt;br&gt;
after you have: what you type, in what order, and where you lose time.&lt;/p&gt;

&lt;p&gt;The project is a common shape. A web service, a worker that reads the same&lt;br&gt;
database, a Postgres with a few gigabytes in it, and a cron job that runs a&lt;br&gt;
report at three in the morning. Each service has a Dockerfile, because Railway&lt;br&gt;
builds from one when it finds one. If yours build with Nixpacks or Railpack,&lt;br&gt;
budget an hour at the start to write two Dockerfiles. Nothing else here depends&lt;br&gt;
on the shape of your code.&lt;/p&gt;

&lt;p&gt;The clock below is a budget for a project this size. Yours will differ by the&lt;br&gt;
size of the dump and the state of your DNS.&lt;/p&gt;
&lt;h2&gt;
  
  
  13:00: Write down what only Railway knows
&lt;/h2&gt;

&lt;p&gt;Three things live on Railway and not in your repository: the environment&lt;br&gt;
variables, the database, and the schedule. Get all three onto your machine&lt;br&gt;
before you touch anything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&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; worker &lt;span class="nt"&gt;--kv&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; worker.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open both files and sort the lines into three piles. Railway's own:&lt;br&gt;
&lt;code&gt;RAILWAY_*&lt;/code&gt;, &lt;code&gt;PORT&lt;/code&gt;, and any line that references another service with&lt;br&gt;
&lt;code&gt;${{...}}&lt;/code&gt;. Delete those. The database: &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;PGHOST&lt;/code&gt; and friends.&lt;br&gt;
Gagarin provides those, so delete them too. Yours: Stripe keys, SMTP passwords,&lt;br&gt;
feature flags. Keep those, and split them: one file per vendor for the&lt;br&gt;
secrets, &lt;code&gt;.env.stripe&lt;/code&gt; and &lt;code&gt;.env.smtp&lt;/code&gt;, and the plain settings left where they&lt;br&gt;
were, in &lt;code&gt;web.env&lt;/code&gt; and &lt;code&gt;worker.env&lt;/code&gt;. The reason is in the 14:10 step. For a&lt;br&gt;
project this size you end up with about ten lines across the four files.&lt;/p&gt;

&lt;p&gt;The database comes out with the tool it went in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;railway connect postgres   &lt;span class="c"&gt;# in one terminal&lt;/span&gt;
pg_dump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Fc&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; before.dump   &lt;span class="c"&gt;# in another&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few gigabytes take ten to fifteen minutes. Copy the cron schedule out of the&lt;br&gt;
Railway dashboard while you wait.&lt;/p&gt;
&lt;h2&gt;
  
  
  13:30: A project, a database, and the data
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg login
gg init shop
gg resource add shop/database postgres &lt;span class="nt"&gt;--size&lt;/span&gt; m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;gg init&lt;/code&gt; prints the project's id. Write it into &lt;code&gt;.gagarin.json&lt;/code&gt; at the root&lt;br&gt;
of the repository and commit it, so the next session, yours or an agent's,&lt;br&gt;
knows which project this code is. A Postgres that a real application depends&lt;br&gt;
on starts at size &lt;code&gt;m&lt;/code&gt;; &lt;code&gt;s&lt;/code&gt; is for one you develop against.&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%2Fp1hvdtusfhyoyrjir6rg.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%2Fp1hvdtusfhyoyrjir6rg.png" alt="after gg resource add" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;after gg resource add&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Name the Postgres &lt;code&gt;database&lt;/code&gt;. A resource on Gagarin hands each service that&lt;br&gt;
depends on it a set of variables prefixed with the resource's own name, so one&lt;br&gt;
called &lt;code&gt;database&lt;/code&gt; publishes &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;DATABASE_HOST&lt;/code&gt;, &lt;code&gt;DATABASE_USER&lt;/code&gt;&lt;br&gt;
and the rest. Your code reads &lt;code&gt;DATABASE_URL&lt;/code&gt; today. Name the resource after the&lt;br&gt;
variable and your code stays as it is.&lt;/p&gt;

&lt;p&gt;The restore goes through a tunnel that lasts as long as the command does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg connect shop/database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the same variables a service would hold, rewritten to point at&lt;br&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt;, and keeps the tunnel open until you stop it. In another terminal,&lt;br&gt;
paste the printed URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_restore &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-acl&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"postgres://…@127.0.0.1:5432/…"&lt;/span&gt; before.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop &lt;code&gt;gg connect&lt;/code&gt; and the tunnel closes. A restore done this way exposed&lt;br&gt;
nothing, so there is nothing to clean up afterwards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While you are here, &lt;code&gt;gg resource backup shop/database&lt;/code&gt; takes a copy now, and&lt;br&gt;
nightly ones follow on their own. A restore on Gagarin always creates a new&lt;br&gt;
resource and never overwrites one, which is why nobody has to approve it at&lt;br&gt;
three in the morning, and the backups outlive the database by fourteen days&lt;br&gt;
if you ever destroy it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  14:10: The variables, then the two services
&lt;/h2&gt;

&lt;p&gt;On Railway your Stripe key is a line in a variables tab. On Gagarin it is a&lt;br&gt;
resource, the same kind of node as the database, and services declare that&lt;br&gt;
they need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg resource add shop/stripe external &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.stripe
gg resource add shop/smtp external &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.smtp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An external runs nothing. It holds values and publishes them under its own&lt;br&gt;
name: a &lt;code&gt;.env.stripe&lt;/code&gt; with &lt;code&gt;SECRET_KEY&lt;/code&gt; in it makes &lt;code&gt;shop/stripe&lt;/code&gt; publish&lt;br&gt;
&lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;. So the split by vendor at 13:00 was also a rename. Strip&lt;br&gt;
the vendor prefix from each key in the file, name the resource after it, and&lt;br&gt;
your code reads the same variable names it read on Railway. The plain settings&lt;br&gt;
in &lt;code&gt;web.env&lt;/code&gt; and &lt;code&gt;worker.env&lt;/code&gt; have no secret in them and belong to one service&lt;br&gt;
each, so they ride along on the deploy itself.&lt;/p&gt;

&lt;p&gt;Now the services. &lt;code&gt;gg ship&lt;/code&gt; builds the directory, pushes the image and deploys&lt;br&gt;
it. The number after the colon is the port the container listens on. Railway&lt;br&gt;
sets &lt;code&gt;PORT&lt;/code&gt; for you. Gagarin asks you to say it once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg ship shop/web:8080 &lt;span class="nt"&gt;--context&lt;/span&gt; ./web &lt;span class="nt"&gt;--env-file&lt;/span&gt; web.env &lt;span class="nt"&gt;--deps&lt;/span&gt; database &lt;span class="nt"&gt;--deps&lt;/span&gt; stripe &lt;span class="nt"&gt;--deps&lt;/span&gt; smtp
gg ship shop/worker:9090 &lt;span class="nt"&gt;--context&lt;/span&gt; ./worker &lt;span class="nt"&gt;--env-file&lt;/span&gt; worker.env &lt;span class="nt"&gt;--deps&lt;/span&gt; database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--deps&lt;/code&gt; is the rest of the configuration. Each one opens the route from the&lt;br&gt;
service to the resource and hands the service that resource's variables in one&lt;br&gt;
step. You write no &lt;code&gt;${{Postgres.DATABASE_URL}}&lt;/code&gt;, because the reference and the&lt;br&gt;
firewall rule are the same declaration. The worker never sees the Stripe key,&lt;br&gt;
because it did not ask for it.&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%2F0ak9pjqj7xrn9k5xu7ud.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%2F0ak9pjqj7xrn9k5xu7ud.png" alt="after the two ships" width="799" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;after the two ships&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every arrow in that drawing is a &lt;code&gt;--deps&lt;/code&gt;. Nothing else draws one. An arrow&lt;br&gt;
into the database is a route and a password; an arrow into a key is the key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gg deps add&lt;/code&gt; prints the variable names it handed over, so you never guess&lt;br&gt;
what a service now holds. A later deploy that forgets &lt;code&gt;--deps&lt;/code&gt; withdraws&lt;br&gt;
nothing: taking an edge away is its own command and needs your approval,&lt;br&gt;
because it is the one change that breaks something and reports nothing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When Stripe rotates your key, &lt;code&gt;gg resource rotate shop/stripe --env-file&lt;br&gt;
.env.stripe&lt;/code&gt; rolls every service that holds it. No deploy, and no hunt through&lt;br&gt;
services for the ones that had a copy. The values are never copied into a&lt;br&gt;
service's stored environment either, so &lt;code&gt;gg rollback shop/web&lt;/code&gt; puts back&lt;br&gt;
yesterday's image and settings and never yesterday's password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two habits from Railway to drop here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets are not deploy arguments.&lt;/strong&gt; &lt;code&gt;--env-file&lt;/code&gt; on a deploy is for a&lt;br&gt;
&lt;code&gt;LOG_LEVEL&lt;/code&gt;, and a deploy replaces that file's variables wholesale, so&lt;br&gt;
&lt;code&gt;web.env&lt;/code&gt; is committed and restated on each ship. Anything you would mind&lt;br&gt;
seeing in a commit, a shell history or an agent transcript goes in an external&lt;br&gt;
instead, and the graph in the console then shows which services hold it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing builds on push.&lt;/strong&gt; Railway watches your branch and builds when it&lt;br&gt;
moves. Gagarin does not build, on purpose, and &lt;a href="https://gagarin.cloud/docs/never" rel="noopener noreferrer"&gt;says why&lt;/a&gt;. Your CI&lt;br&gt;
runs &lt;code&gt;gg ship&lt;/code&gt; with a credential from &lt;code&gt;gg creds create&lt;/code&gt;, which can deploy,&lt;br&gt;
expires, and cannot delete. The secret goes to stdout alone, so it pipes&lt;br&gt;
straight into a CI secret and never touches a disk or a transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh secret &lt;span class="nb"&gt;set &lt;/span&gt;GAGARIN_TOKEN &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gg creds create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"github actions: shop"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this project the workflow itself is about twenty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  14:40: The cron job
&lt;/h2&gt;

&lt;p&gt;On Railway the schedule sits in a service's settings. On Gagarin a scheduled&lt;br&gt;
job is a &lt;code&gt;gg run&lt;/code&gt; with a schedule. It is a job rather than a service: it runs to&lt;br&gt;
completion, and nothing can reach it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg run shop/report report:v1 &lt;span class="nt"&gt;--deps&lt;/span&gt; database &lt;span class="nt"&gt;--schedule&lt;/span&gt; &lt;span class="s2"&gt;"0 3 * * *"&lt;/span&gt; &lt;span class="nt"&gt;--tz&lt;/span&gt; Europe/Berlin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing runs now, and &lt;code&gt;gg run&lt;/code&gt; returns at once. Tomorrow at three, check it with&lt;br&gt;
&lt;code&gt;gg logs shop/report&lt;/code&gt;, or open the project in the console at&lt;br&gt;
&lt;a href="https://my.gagarin.cloud" rel="noopener noreferrer"&gt;my.gagarin.cloud&lt;/a&gt;, which keeps a feed of how each&lt;br&gt;
firing ended.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Firings never overlap: a report that runs long delays the next one rather than&lt;br&gt;
running beside it. A firing that fails pushes one notification to your phone,&lt;br&gt;
and the next one that succeeds clears it. And a one-off &lt;code&gt;gg run&lt;/code&gt;, say a&lt;br&gt;
migration before a deploy, waits and exits with the script's own exit code, so&lt;br&gt;
CI can stop on a failed migration without a line of polling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  14:50: Check it before your users do
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gg status shop
project shop  (id k3qm7ebd)

     SERVICE   KIND      SIZE  READY          PORT  REACHES                   IMAGE
  ●  web       service   s     1/1            8080  database, stripe◆, smtp◆  web:1761480212
  ●  └ https://web-k3qm7ebd.apps.gagarin.cloud
  ●  worker    service   s     1/1            9090  database                  worker:1761480250
  ◷  report    job       s     ⏱ 0 3 * * *    —     database                  report:v1
     └ next in 12h
  ●  database  postgres  m     1/1            5432  —                         postgres:17
  ◆  stripe    external  —     —              —     —                         —
  ◆  smtp      external  —     —              —     —                         —

  ● running   ◷ scheduled   ◆ external (runs nothing; declaring it grants its variables, not egress)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;REACHES&lt;/code&gt; column is the firewall, printed. The plain edges open a route&lt;br&gt;
inside the project. The diamond edges hand over a key and open nothing, since&lt;br&gt;
anything in a project can already reach the internet. The worker has no&lt;br&gt;
address because nobody asked for one, and it cannot acquire one by accident.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gg&lt;/code&gt; is not the only window onto this. The console at&lt;br&gt;
&lt;a href="https://my.gagarin.cloud" rel="noopener noreferrer"&gt;my.gagarin.cloud&lt;/a&gt; draws the same project as a live&lt;br&gt;
graph, with each service's status, logs and history beside it, the bill for&lt;br&gt;
the month so far, and the switch that sends a push to your phone when a&lt;br&gt;
service goes down or the report fails at three in the morning. Everything the&lt;br&gt;
console shows is something &lt;code&gt;gg&lt;/code&gt; can read, and the drawings in this post are&lt;br&gt;
the console's drawings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If something is wrong at this point, &lt;code&gt;gg status&lt;/code&gt; tends to say what. A&lt;br&gt;
container killed for memory gets a line naming the size to move to. A crash is&lt;br&gt;
in &lt;code&gt;gg logs shop/web&lt;/code&gt;, with the crashed container's output kept rather than&lt;br&gt;
lost with the pod, and &lt;code&gt;--since 2h --grep "panic:"&lt;/code&gt; narrows it. A deploy you&lt;br&gt;
regret is &lt;code&gt;gg rollback shop/web&lt;/code&gt;, one call, recorded as a new revision so the&lt;br&gt;
history keeps the evidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The web service already has a generated address. Open it, sign in, and click&lt;br&gt;
through the pages that touch the database. Expect to find one thing of your&lt;br&gt;
own here. Ours was a report that assumed &lt;code&gt;TZ&lt;/code&gt; was set, because Railway's base&lt;br&gt;
image set it and our Dockerfile did not. One line in the Dockerfile, one more&lt;br&gt;
&lt;code&gt;gg ship&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  15:20: The address
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg domain add shop/web shop.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the DNS record to create. Make the change at your registrar, and the&lt;br&gt;
certificate arrives once the record resolves. The generated address keeps&lt;br&gt;
working in the meantime, so the service always has a door.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Both addresses answer from then on, and no deploy can ever take either away:&lt;br&gt;
an address is a standing declaration, and forgetting a flag will not remove&lt;br&gt;
one.&lt;/p&gt;
&lt;/blockquote&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%2Fyr8m7fghgm0yport1qii.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%2Fyr8m7fghgm0yport1qii.png" alt="the whole project, with an address" width="799" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;the whole project, with an address&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One public address, on the one service a browser opens. The worker, the job&lt;br&gt;
and the three resources stay private, and the arrows say who may reach what.&lt;/p&gt;

&lt;p&gt;Keep the Railway service running through the DNS change. Lower the TTL on the&lt;br&gt;
record the day before if you can. If you did not, some users reach the old&lt;br&gt;
service for an hour, and the two services write to two different databases&lt;br&gt;
while they do. That hour is the cost of this approach, and the reason the next&lt;br&gt;
step is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  15:40: Freeze, copy the delta, cut over
&lt;/h2&gt;

&lt;p&gt;For a side project, skip this and lose an hour of writes. For anything with&lt;br&gt;
users, do it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the Railway service into maintenance mode, or scale it to zero.&lt;/li&gt;
&lt;li&gt;Dump again and restore again, with the commands from 13:00 and 13:30. The
second dump is small.&lt;/li&gt;
&lt;li&gt;Flip the DNS record.&lt;/li&gt;
&lt;li&gt;Watch &lt;code&gt;gg logs shop/web&lt;/code&gt; for a few minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Budget twenty minutes between the freeze and the flip. Most of it is DNS, and&lt;br&gt;
most of that goes away if you lowered the TTL yesterday.&lt;/p&gt;

&lt;h2&gt;
  
  
  16:30: Keep Railway for a week
&lt;/h2&gt;

&lt;p&gt;Leave the Railway project in place with the service scaled to zero and the&lt;br&gt;
database intact. Idle, it costs close to nothing, and it is the only copy you&lt;br&gt;
have of the state before the move. Gagarin takes its first nightly backup&lt;br&gt;
tonight and keeps fourteen days of them, so by next week you hold a better&lt;br&gt;
copy.&lt;/p&gt;

&lt;p&gt;Then delete the Railway project. Railway asks you to confirm in the browser.&lt;br&gt;
Gagarin, should you ever leave in the other direction, refuses a destructive&lt;br&gt;
command until you press a button it emails you, whichever client asked, and&lt;br&gt;
your CI credential cannot ask at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even you cannot destroy the database while a service still declares it: the&lt;br&gt;
refusal names what depends on it. And when you do go, &lt;code&gt;gg eject shop&lt;/code&gt; writes&lt;br&gt;
the Kubernetes objects the project runs as, so leaving is a file rather than&lt;br&gt;
a rewrite. &lt;a href="https://gagarin.cloud/docs/model" rel="noopener noreferrer"&gt;The model&lt;/a&gt; makes the case for that design. It is&lt;br&gt;
in this post because someone who moved a production database an hour ago cares&lt;br&gt;
a great deal about what can delete things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The afternoon, totalled
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;th&gt;commands&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inventory, dump&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;railway variables&lt;/code&gt;, &lt;code&gt;pg_dump&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;project, database, restore&lt;/td&gt;
&lt;td&gt;40 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg init&lt;/code&gt;, &lt;code&gt;gg resource add&lt;/code&gt;, &lt;code&gt;gg connect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variables, two services&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg resource add&lt;/code&gt; ×2, &lt;code&gt;gg ship&lt;/code&gt; ×2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cron job&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gg run --schedule&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checks, one fix&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg status&lt;/code&gt;, &lt;code&gt;gg logs&lt;/code&gt;, &lt;code&gt;gg ship&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;address, cutover&lt;/td&gt;
&lt;td&gt;70 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg domain add&lt;/code&gt;, the delta dump, DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three and a half hours. One line of code changed, and it fixed a bug you were&lt;br&gt;
already carrying. Two new files in the repository: the CI workflow, and a&lt;br&gt;
&lt;code&gt;.gagarin.json&lt;/code&gt; that says which project this is.&lt;/p&gt;

&lt;p&gt;You give up builds on push and a second region. You get a graph that is the&lt;br&gt;
firewall and the secrets map at once, traffic that costs&lt;br&gt;
nothing, and a platform where the agent that runs your deploys cannot remove&lt;br&gt;
anything until you press a button.&lt;/p&gt;

&lt;p&gt;If you would rather not spend the afternoon yourself, the&lt;br&gt;
&lt;a href="https://gagarin.cloud/" rel="noopener noreferrer"&gt;coding agent you already use&lt;/a&gt; can run the same commands. Paste it the&lt;br&gt;
Railway project name and a link to this post. It will ask you to press a button&lt;br&gt;
once, when it wants to delete the old project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.railway.com/guides/cli" rel="noopener noreferrer"&gt;Railway CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.railway.com/guides/variables" rel="noopener noreferrer"&gt;Railway variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.railway.com/guides/github-autodeploys" rel="noopener noreferrer"&gt;Railway GitHub autodeploys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gagarin.cloud/blog/gagarin-vs-railway" rel="noopener noreferrer"&gt;Gagarin vs Railway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagarin.cloud/blog/leaving-railway-in-an-afternoon" rel="noopener noreferrer"&gt;gagarin.cloud&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>railway</category>
      <category>cloud</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Assembly required</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:59:01 +0000</pubDate>
      <link>https://dev.to/sharpvik/assembly-required-2d75</link>
      <guid>https://dev.to/sharpvik/assembly-required-2d75</guid>
      <description>&lt;p&gt;A cloud hands you IP ranges, security groups, service accounts, IAM policies,&lt;br&gt;
load balancer listeners and virtual machines. None of those is a place to put an&lt;br&gt;
application. You get to one by assembling them, and the assembly is work: a few&lt;br&gt;
days at the start of a project, an hour or two a week after that, for as long as&lt;br&gt;
the project lives. It bills nothing. It ships no feature. Each team that does it&lt;br&gt;
builds a private platform-as-a-service out of the same parts, and the one they&lt;br&gt;
finish is as good as their best engineer's spare attention, which is not very&lt;br&gt;
good. The other option is to pay an infrastructure salary for the same artifact.&lt;/p&gt;
&lt;h2&gt;
  
  
  The not-so-fun puzzle
&lt;/h2&gt;

&lt;p&gt;Count the objects in the smallest useful deployment. One container serving HTTP,&lt;br&gt;
one Postgres behind it, on a domain, with TLS.&lt;/p&gt;

&lt;p&gt;On AWS: a VPC, two subnets in two availability zones because the load balancer&lt;br&gt;
refuses to start with one, a route table, an internet gateway, a NAT gateway so&lt;br&gt;
the container can reach your package registry, two security groups, a task&lt;br&gt;
definition, an IAM task role, a second IAM role for the thing that launches the&lt;br&gt;
task, a target group, a listener, an ACM certificate with its validation record,&lt;br&gt;
a Route 53 record, an RDS subnet group, a parameter group, a secret, and a log&lt;br&gt;
group. Twenty resources, give or take your tolerance for defaults. Your&lt;br&gt;
application is one line inside one of them. Google Cloud and Azure sell the same&lt;br&gt;
box with different part names.&lt;/p&gt;

&lt;p&gt;None of the twenty is difficult on its own. The difficulty is that all twenty&lt;br&gt;
have to agree, and when two of them disagree the system reports a timeout. You&lt;br&gt;
forgot the NAT gateway, so image pulls hang for three minutes and then fail with&lt;br&gt;
a registry error that says nothing about routing. The security group allows 5432&lt;br&gt;
from a CIDR that looks right and covers the wrong subnet. The health check path&lt;br&gt;
returns 302 and the target group calls it unhealthy, so the load balancer drains&lt;br&gt;
the only task you have and the deploy rolls back clean, with no failure to read.&lt;/p&gt;

&lt;p&gt;Nowhere in the documentation does anyone promise that a web app with a database&lt;br&gt;
is easy to run. They promise a page for each primitive, and they deliver that.&lt;br&gt;
The convenience was never offered. We supplied it ourselves and then forgot that&lt;br&gt;
we had.&lt;/p&gt;
&lt;h2&gt;
  
  
  Your time or your money
&lt;/h2&gt;

&lt;p&gt;Google and Stripe employ people whose entire job is the twenty objects. They&lt;br&gt;
have platform teams, internal deploy tools and an on-call rotation to go with&lt;br&gt;
them, and that is a sane way to spend money at their size.&lt;/p&gt;

&lt;p&gt;Below that size you choose between two bills. Hire an infrastructure engineer,&lt;br&gt;
who costs about what a senior backend developer costs and writes none of your&lt;br&gt;
product. Or give the work to a backend developer, who does it in the gaps&lt;br&gt;
between features.&lt;/p&gt;

&lt;p&gt;Take the second option and look at what the repo collects. A Terraform module&lt;br&gt;
copied from a blog post. A &lt;code&gt;deploy.sh&lt;/code&gt; with a &lt;code&gt;set -e&lt;/code&gt; at the top and four &lt;code&gt;aws&lt;/code&gt;&lt;br&gt;
calls under it. A GitHub Actions workflow that works, then a second one for&lt;br&gt;
staging that drifts from the first within a month. A naming convention somebody&lt;br&gt;
documented in Notion in March. Six weeks later you own a private PaaS with one&lt;br&gt;
customer, no roadmap, and a maintainer who would rather be writing the product.&lt;/p&gt;
&lt;h2&gt;
  
  
  Yet another cloud wrapper
&lt;/h2&gt;

&lt;p&gt;Say the expertise is in the building and the wrapper is good. I have read&lt;br&gt;
several of these now, at companies with no connection to each other, and they&lt;br&gt;
contain the same verbs. Build an image and push it. Set environment variables&lt;br&gt;
for an environment. Deploy a version. Declare that this service may reach that&lt;br&gt;
database. Print status. Roll back.&lt;/p&gt;

&lt;p&gt;Six verbs, written from scratch at each of those companies, because the problem&lt;br&gt;
has one shape and everybody finds it. Writing them again re-derives something&lt;br&gt;
the industry settled years ago. No customer pays for your &lt;code&gt;deploy&lt;/code&gt; subcommand.&lt;/p&gt;

&lt;p&gt;And you keep it. The provider releases a major version that renames an attribute&lt;br&gt;
you set in nine places. The node image you pinned goes end-of-life. The engineer&lt;br&gt;
who understood the module takes another job, and the module becomes a thing the&lt;br&gt;
team edits by copying the block above and changing a string. Internal tools get&lt;br&gt;
the maintenance that nothing external demands, which is none.&lt;/p&gt;
&lt;h2&gt;
  
  
  Who's holding the bag?
&lt;/h2&gt;

&lt;p&gt;Most firms with twelve engineers do not want an infrastructure salary on the&lt;br&gt;
books, so the twenty objects land on developers who have never administered a&lt;br&gt;
network. From there it goes one of a few ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They learn, on project time.&lt;/strong&gt; Four days reading VPC documentation is four&lt;br&gt;
days of a developer's salary spent arriving at the line where the application&lt;br&gt;
work starts. They will be competent at it in a year and will have shipped less&lt;br&gt;
in the meantime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They guess, and some guesses cost money.&lt;/strong&gt; A security group open to 0.0.0.0/0&lt;br&gt;
because that made the timeout stop. A database whose backups have never been&lt;br&gt;
restored, so nobody knows whether they work. A secret passed as a build argument&lt;br&gt;
and printed into a public CI log. None of these announces itself on the day it&lt;br&gt;
is made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They hand it to an agent.&lt;/strong&gt; An agent writes plausible Terraform in a minute,&lt;br&gt;
and plausible is the output that hurts. The plan applies, the service answers&lt;br&gt;
200, and the bucket holding customer uploads is readable by anyone who knows the&lt;br&gt;
name. The agent is working from the same twenty-part box you are, with the same&lt;br&gt;
four ways to change one thing and no single source of truth among them, so it&lt;br&gt;
guesses in the places you would have guessed.&lt;/p&gt;

&lt;p&gt;Then the bill arrives, or the data does not stay where you put it, and somebody&lt;br&gt;
signs the incident report. The agent is not on call. Your developer, who was&lt;br&gt;
never an infrastructure engineer and was handed the work anyway, holds the bag.&lt;/p&gt;
&lt;h2&gt;
  
  
  There's a better way
&lt;/h2&gt;

&lt;p&gt;Raise the primitives to the things you say out loud when you describe the&lt;br&gt;
system. A project. Services inside it. Managed resources. Which service may&lt;br&gt;
reach which. A domain, for the ones the public should see.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg resource add saas/pg postgres
gg ship         saas/api:8080 &lt;span class="nt"&gt;--deps&lt;/span&gt; pg
gg ship         saas/web:3000 &lt;span class="nt"&gt;--deps&lt;/span&gt; api
gg domain   add saas/web
&lt;span class="c"&gt;# =&amp;gt; Live at https://web-3cnciet6.apps.gagarin.cloud/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No VPC, because you were never going to express a product decision as a CIDR&lt;br&gt;
block. No security groups: &lt;code&gt;--deps pg&lt;/code&gt; opens the one route and hands &lt;code&gt;api&lt;/code&gt; the&lt;br&gt;
credentials for &lt;code&gt;pg&lt;/code&gt; in the same call, so the route and the password arrive&lt;br&gt;
together or neither of them does. Every service stays private until &lt;code&gt;gg domain&lt;br&gt;
add&lt;/code&gt;, which puts the public internet behind a command you have to type rather&lt;br&gt;
than behind a default you have to remember.&lt;/p&gt;

&lt;p&gt;What this does not buy you: containers still crash-loop, and migrations still&lt;br&gt;
fail to apply. One class of failure leaves, the class where twenty parts have to&lt;br&gt;
agree and the system tells you only that something, somewhere, timed out.&lt;/p&gt;

&lt;p&gt;And when the project outgrows us, &lt;a href="https://gagarin.cloud/docs/eject" rel="noopener noreferrer"&gt;&lt;code&gt;gg eject&lt;/code&gt;&lt;/a&gt; writes out the&lt;br&gt;
Kubernetes you would have had, which you then own, knobs and all. The reasoning&lt;br&gt;
behind all of this is in &lt;a href="https://gagarin.cloud/docs/why" rel="noopener noreferrer"&gt;why coherence&lt;/a&gt;, and the objects themselves&lt;br&gt;
are in &lt;a href="https://gagarin.cloud/docs/model" rel="noopener noreferrer"&gt;the model&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We are on call for the platform. You are on call for your code. The line sits&lt;br&gt;
where you can see it, which is more than the twenty objects ever offered.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagarin.cloud/blog/assembly-required" rel="noopener noreferrer"&gt;gagarin.cloud&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>aws</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Two strangers' repos, deployed by agents, unaided</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:58:51 +0000</pubDate>
      <link>https://dev.to/sharpvik/two-strangers-repos-deployed-by-agents-unaided-2n98</link>
      <guid>https://dev.to/sharpvik/two-strangers-repos-deployed-by-agents-unaided-2n98</guid>
      <description>&lt;p&gt;The claim this platform is built on is that a coding agent can take an application&lt;br&gt;
to production on its own. That is easy to demonstrate on our own code, which is&lt;br&gt;
exactly why demonstrating it on our own code proves nothing.&lt;/p&gt;

&lt;p&gt;So: two repositories found on GitHub, cloned cold, neither of them ours and&lt;br&gt;
neither of them seen before. One a Node service with a Python ML sidecar, the&lt;br&gt;
other a .NET 8 app that needs Postgres. Two agents, each given the &lt;code&gt;gg&lt;/code&gt; skill and&lt;br&gt;
nothing else — no runbook, no hints, and nobody to ask.&lt;/p&gt;

&lt;p&gt;Both of them got there.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;the repo&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;th&gt;wall clock&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;gg&lt;/code&gt; calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node + Python ML sidecar&lt;/td&gt;
&lt;td&gt;1/1, verified serving&lt;/td&gt;
&lt;td&gt;15m20s&lt;/td&gt;
&lt;td&gt;19, zero errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET 8 + Postgres&lt;/td&gt;
&lt;td&gt;1/1, verified serving&lt;/td&gt;
&lt;td&gt;24m36s&lt;/td&gt;
&lt;td&gt;~48, half of them polls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The part worth more than the times: neither agent accepted &lt;code&gt;1/1&lt;/code&gt; as proof. A&lt;br&gt;
private service on Gagarin has no public address, so each one built a probe image,&lt;br&gt;
ran it as a job inside the project, and went and got a real HTTP response out of&lt;br&gt;
the thing it had just deployed. Nobody told either of them to do that. They did&lt;br&gt;
not trust the status line, which is the correct instinct about a status line.&lt;/p&gt;

&lt;p&gt;That is the central claim, tested against strangers' code, and it held. Worth&lt;br&gt;
saying plainly, because everything below is a list of what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six defects, in forty minutes of agent time
&lt;/h2&gt;

&lt;p&gt;Deploying someone else's unfamiliar app is the best bug-finder we have, and it is&lt;br&gt;
not close. Every one of these is ours, not theirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A crash reported as &lt;code&gt;SIGSEGV&lt;/code&gt; when it was nothing of the kind.&lt;/strong&gt; An ordinary&lt;br&gt;
unhandled exception in the app surfaced as exit 139, which reads like a corrupt&lt;br&gt;
cross-architecture build — so the agent went looking for an image problem that did&lt;br&gt;
not exist. &lt;code&gt;gg logs&lt;/code&gt; had the real stack trace the whole time. Status was lying&lt;br&gt;
with the truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dependency that was declared and did not route yet.&lt;/strong&gt; A service crash-looped on&lt;br&gt;
a database timeout, with the edge declared and visible in &lt;code&gt;gg status&lt;/code&gt; the whole&lt;br&gt;
time. The NetworkPolicy had not converged; it healed itself two minutes later. Our own&lt;br&gt;
diagnosis table attributes that exact symptom to a &lt;em&gt;missing&lt;/em&gt; edge, so the&lt;br&gt;
documentation pointed firmly away from the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No way to reach a private service.&lt;/strong&gt; The strongest signal of the whole exercise,&lt;br&gt;
because both agents hit it independently and both invented the identical&lt;br&gt;
workaround — build a probe, run it as a job, curl from inside. Two strangers&lt;br&gt;
converging on the same workaround is not a missing convenience. It is a missing&lt;br&gt;
feature, and it is ours to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undocumented Postgres privileges.&lt;/strong&gt; What the role we hand you can and cannot do&lt;br&gt;
is decisive for EF Core, and for Rails and Django after it, and the only way to&lt;br&gt;
find out is to connect and ask Postgres yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume ownership for a non-root container.&lt;/strong&gt; Undocumented, irreversible, and not&lt;br&gt;
something an agent can fix from outside the container. The agent read the&lt;br&gt;
situation correctly and deployed the app &lt;em&gt;without&lt;/em&gt; the volume it wanted, which is&lt;br&gt;
the right call and a bad outcome.&lt;/p&gt;

&lt;p&gt;And a sixth, fixed the same day: &lt;code&gt;gg destroy&lt;/code&gt; could not delete a job. It routed to&lt;br&gt;
the resource path and printed a raw API endpoint as the workaround, which is the&lt;br&gt;
platform admitting out loud that it has a seam in it.&lt;/p&gt;

&lt;p&gt;Five of those six are still open as we publish this. They are written down, they&lt;br&gt;
are ours, and none of them is the kind of thing a demo on our own repository would&lt;br&gt;
ever have found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Booting is not demonstrating
&lt;/h2&gt;

&lt;p&gt;The .NET app reached &lt;code&gt;1/1&lt;/code&gt; and cannot do anything.&lt;/p&gt;

&lt;p&gt;It needs six third-party API keys, and they fail lazily — not at boot, but the&lt;br&gt;
first time a request actually wants one. Those are a stranger's accounts. No agent&lt;br&gt;
can invent them and neither can we.&lt;/p&gt;

&lt;p&gt;This matters more than it looks, because "I deployed your project" is a sentence&lt;br&gt;
we were about to go and say to people. It is true for a self-contained repo and&lt;br&gt;
misleading for anything that calls paid APIs, where the honest version is "it&lt;br&gt;
builds and boots, and you would need your own keys" — a much weaker gift. The tell&lt;br&gt;
is greppable, which is the useful part: read the &lt;code&gt;.env.example&lt;/code&gt; before you spend&lt;br&gt;
the afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  And one thing this cloud cannot host
&lt;/h2&gt;

&lt;p&gt;A third candidate was examined and not attempted: a realtime voice bot, WebRTC,&lt;br&gt;
media over UDP. Signalling is HTTP and would pass our ingress fine. The media&lt;br&gt;
would not: a service here gets one TCP port behind an HTTPS ingress, and that repo&lt;br&gt;
expects to be handed real ports on the host.&lt;/p&gt;

&lt;p&gt;Kubernetes is not the constraint — a Service takes UDP, and plenty of clusters run&lt;br&gt;
media on one. The constraint is our own model, and it is a product decision rather&lt;br&gt;
than a limit of the substrate. Two true things follow from it. Native UDP is a real&lt;br&gt;
feature with a real cost, because ICE wants a port &lt;em&gt;range&lt;/em&gt; and a routable candidate&lt;br&gt;
address, which is the exact thing a managed platform exists to take away from you.&lt;br&gt;
And relayed media already works today, undocumented: egress here is open, so a&lt;br&gt;
service can dial an external TURN relay outbound and media flows&lt;br&gt;
browser → relay → service with no inbound UDP at all. That sentence belongs in our&lt;br&gt;
docs and is not in them yet.&lt;/p&gt;

&lt;p&gt;Better to say where the edge is than to find out from somebody who already left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we take from it
&lt;/h2&gt;

&lt;p&gt;Forty minutes of agent time bought six defects, a documented ceiling on what we&lt;br&gt;
can honestly promise, and a product boundary we had not named. We will run it&lt;br&gt;
again — as a product test, not as a marketing stunt, which is what it would quietly&lt;br&gt;
become if we let it.&lt;/p&gt;

&lt;p&gt;The claim survived. The platform around it picked up five open tasks, which is&lt;br&gt;
the trade we would make again.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagarin.cloud/blog/two-strangers-repos-deployed" rel="noopener noreferrer"&gt;gagarin.cloud&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>agents</category>
      <category>cloud</category>
    </item>
    <item>
      <title>A database, two services and an address, in five commands</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:58:42 +0000</pubDate>
      <link>https://dev.to/sharpvik/a-database-two-services-and-an-address-in-five-commands-272f</link>
      <guid>https://dev.to/sharpvik/a-database-two-services-and-an-address-in-five-commands-272f</guid>
      <description>&lt;p&gt;Everything below is one project, built from an ordinary repository with no&lt;br&gt;
Gagarin-specific anything in it. No manifest, no config file, no YAML: there is&lt;br&gt;
nowhere on this platform to put one, which is the point and is also why this post&lt;br&gt;
is short.&lt;/p&gt;

&lt;p&gt;The app is a Go API, a web front end that calls it, and a Postgres under both.&lt;br&gt;
Call the project &lt;code&gt;feed&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The Dockerfile is the only file we need
&lt;/h2&gt;

&lt;p&gt;If your repository already builds an image, you are done before you start. Ours&lt;br&gt;
is the usual two-stage build:&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.25-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; /out/feed ./cmd/feed

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static:nonroot&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /out/feed /feed&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nonroot&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/feed"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things about it matter here and nothing else does. It &lt;strong&gt;listens on one TCP&lt;br&gt;
port&lt;/strong&gt;, and it reads its configuration from the environment. That is the entire&lt;br&gt;
contract — anything that satisfies it runs here.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The project, and the database under it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg init feed
gg resource add feed/pg postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;gg init&lt;/code&gt; creates the project everything else is named for. &lt;code&gt;gg resource add&lt;/code&gt;&lt;br&gt;
provisions the Postgres and, more to the point, &lt;em&gt;keeps the credentials&lt;/em&gt;. Nobody&lt;br&gt;
types a password into anything after this, and no connection string goes near a&lt;br&gt;
repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Ship it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gg ship&lt;/code&gt; builds the current directory, pushes the image and deploys it — three&lt;br&gt;
commands' worth of work, and they are still there separately for the day you want&lt;br&gt;
them apart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg ship feed/api:8080 &lt;span class="nt"&gt;--deps&lt;/span&gt; pg
gg ship feed/web:3000 &lt;span class="nt"&gt;--deps&lt;/span&gt; api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number after the colon is the port the container listens on, and &lt;code&gt;--deps&lt;/code&gt; is&lt;br&gt;
the whole of the configuration: it says what this service may reach, and the&lt;br&gt;
platform hands it the connection variables of any resource in that list. The&lt;br&gt;
database password is never typed and never committed.&lt;/p&gt;

&lt;p&gt;Note what is &lt;em&gt;not&lt;/em&gt; in those commands. No replica count, no rollout strategy, no&lt;br&gt;
health-check path, no ingress class, no namespace, no service account, no&lt;br&gt;
NetworkPolicy. All of those exist underneath — this is Kubernetes down there —&lt;br&gt;
and none of them is a decision worth making at this point in the afternoon.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Nothing reaches anything it has not declared
&lt;/h2&gt;

&lt;p&gt;This is the part people are usually surprised by, and it is the part worth&lt;br&gt;
reading twice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A private service is default-denied. Until the edge is declared, the call is&lt;br&gt;
dropped — and a dropped packet hangs, it does not fail fast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;--deps&lt;/code&gt; declared them on the way past. The graph also moves on its own, without&lt;br&gt;
a deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg deps &lt;span class="nb"&gt;ls  &lt;/span&gt;feed/api
gg deps add feed/api pg
gg deps &lt;span class="nb"&gt;rm  &lt;/span&gt;feed/api pg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding an edge is yours. &lt;strong&gt;Taking one away asks a human to approve it&lt;/strong&gt;, because&lt;br&gt;
it is the one change that breaks something and reports nothing: the service keeps&lt;br&gt;
running, the status stays green, and the call simply stops arriving.&lt;/p&gt;

&lt;p&gt;The graph is not a picture drawn from variable references after the fact. &lt;strong&gt;The&lt;br&gt;
graph is the firewall.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A migration is not a service
&lt;/h3&gt;

&lt;p&gt;A job runs to completion and listens on nothing. Same image, run once, reaching&lt;br&gt;
the database the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg run feed/migrate feed:2026-09-01 &lt;span class="nt"&gt;--deps&lt;/span&gt; pg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gg run&lt;/code&gt; waits, prints what the job wrote, and exits with the job's own exit&lt;br&gt;
code — so CI can branch on it as though it had run the migration itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. An address
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg domain add feed/web feed.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Certificate and ingress come with it. Leave the domain off and the service gets a&lt;br&gt;
generated one on &lt;code&gt;apps.gagarin.cloud&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;A resource cannot have an address. Ask anyway, and the engine says so rather than&lt;br&gt;
doing something clever:&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;"error"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_a_service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pg is a postgres resource, not a service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fix_hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a resource is reached from inside the project by name and is never public"&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;Every error carries a stable &lt;code&gt;code&lt;/code&gt; and a &lt;code&gt;fix_hint&lt;/code&gt;. Branch on the code, never&lt;br&gt;
on the prose — &lt;a href="https://gagarin.cloud/docs/errors" rel="noopener noreferrer"&gt;the error contract&lt;/a&gt; is the whole list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like once it is up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gg status feed
project feed  (id eqteoeay)

    SERVICE  KIND      SIZE  READY  PORT  REACHES  IMAGE               VOLUME
  ● api      service   s     1/1    4000  pg       api:1788569515      —
  ● pg       postgres  s     1/1    5432  —        postgres:17-alpine  10GB
  ● web      service   s     1/1    8080  api      web:1788569576      —
    └ https://feed.gagarin.cloud
    └ https://web-eqteoeay.apps.gagarin.cloud

  ● running
  $0.789 today so far
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One screen: what runs, what it reaches, what it costs today. The &lt;code&gt;REACHES&lt;/code&gt;&lt;br&gt;
column is the firewall, printed.&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%2Fo95rhrts3ecxy8bznb85.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%2Fo95rhrts3ecxy8bznb85.png" alt="The Gagarin console: a project with two running services, five external resources, and the graph of what reaches what" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A different project in the console — the same reading, drawn. There is nothing to press that is not also a command: one source of truth, and a second way to change it is exactly what we are not building.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  And the command it will not run
&lt;/h2&gt;

&lt;p&gt;The database has a service depending on it now. Ask for it to be destroyed and&lt;br&gt;
the answer is no — not a confirmation prompt, not a &lt;code&gt;--force&lt;/code&gt;, just no:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gagarin.cloud/blog/a-database-two-services-and-an-address" rel="noopener noreferrer"&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%2Fxq88udcnow0tarh2nce0.jpg" alt="Screen recording: gg destroy feed/pg is refused with service_in_use, naming the service that depends on it" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;b&gt;gg destroy feed/pg&lt;/b&gt; → &lt;b&gt;409 service_in_use&lt;/b&gt;, naming what holds it, and the hint is the command that would let go first. Same graph as step 4, read from the other end. &lt;a href="https://gagarin.cloud/blog/a-database-two-services-and-an-address" rel="noopener noreferrer"&gt;Watch the recording on gagarin.cloud.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The whole afternoon:&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;command&lt;/th&gt;
&lt;th&gt;what it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;your Dockerfile, unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg init&lt;/code&gt; + &lt;code&gt;gg resource add&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a project, and a Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gg ship&lt;/code&gt; ×2&lt;/td&gt;
&lt;td&gt;two services, and the edges between them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gg deps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the graph, when it moves without a deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gg domain add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five commands, and the only decision in them was which service gets a port.&lt;/p&gt;

&lt;p&gt;If you want to follow along with your own repository, &lt;code&gt;gg login&lt;/code&gt; prints a link&lt;br&gt;
and a code, and there is &lt;a href="https://gagarin.cloud/pricing" rel="noopener noreferrer"&gt;$5 on a new account&lt;/a&gt; to spend. If you want to&lt;br&gt;
know what happens the day you want out, we wrote that down too:&lt;br&gt;
&lt;a href="https://gagarin.cloud/docs/eject" rel="noopener noreferrer"&gt;leaving&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gagarin.cloud/blog/a-database-two-services-and-an-address" rel="noopener noreferrer"&gt;gagarin.cloud&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>cloud</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Bloated Clouds, Anyone?</title>
      <dc:creator>Viktor Rozenko</dc:creator>
      <pubDate>Fri, 18 Sep 2026 19:25:58 +0000</pubDate>
      <link>https://dev.to/sharpvik/bloated-clouds-anyone-1jhc</link>
      <guid>https://dev.to/sharpvik/bloated-clouds-anyone-1jhc</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Conventional cloud platforms had to serve everyone, and they became bloated with features. Chasing the next big thing, they abandoned any attempt at improving developer experience, which is &lt;em&gt;the&lt;/em&gt; reason clouds blew up in the first place. Big tech companies have the money to pay for DevOps. The rest of us have to choose between wasting developer time on it or delegating it to an AI agent that can't be held responsible when it all comes crashing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can rent a &lt;em&gt;plain VM&lt;/em&gt;, &lt;em&gt;but&lt;/em&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;regular deployments have to arrive via SSH or some elaborate CI setup (we'll look at the options in the next post),&lt;/li&gt;
&lt;li&gt;you'll be paying for more compute than you actually use,&lt;/li&gt;
&lt;li&gt;and when your service becomes popular enough to use its entire CPU/RAM allowance, you have to scale it up manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You can go &lt;em&gt;serverless&lt;/em&gt; and it will scale up and down automatically, &lt;em&gt;but&lt;/em&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;servers with stateful functionality will underperform (for instance, &lt;a href="https://nextjs.org" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; has server-side caching, and it doesn't work when you run it as a scale-to-zero container),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://firebase.google.com/docs/functions" rel="noopener noreferrer"&gt;Firebase Cloud Functions&lt;/a&gt; are public by default and weird to set up (they have their own CLI just to package and deploy them, and only JS/Python are allowed),&lt;/li&gt;
&lt;li&gt;a few providers let you create a cloud function right in the cloud console. That's terrible, because it's a piece of code that lives in a weird online editor and isn't type-checked against your latest code changes. You &lt;em&gt;will&lt;/em&gt; forget to update it sooner or later, and it &lt;em&gt;will&lt;/em&gt; crash with a bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Suppose you &lt;em&gt;do&lt;/em&gt; want the scalability, smooth CI deploys that just make sense, and all your code living in a GitHub repo &lt;em&gt;you&lt;/em&gt; control. Welcome to &lt;a href="https://kubernetes.io" rel="noopener noreferrer"&gt;&lt;em&gt;Kubernetes&lt;/em&gt;&lt;/a&gt; and the 4,000 lines of YAML config that come with it.
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Welcome to provisioning a Docker image registry.&lt;/li&gt;
&lt;li&gt;Welcome to setting up &lt;a href="https://traefik.io" rel="noopener noreferrer"&gt;Traefik&lt;/a&gt; for TLS termination.&lt;/li&gt;
&lt;li&gt;Welcome to setting up security groups and service accounts.&lt;/li&gt;
&lt;li&gt;Welcome to configuring ingress and egress rules.&lt;/li&gt;
&lt;li&gt;Welcome to not understanding why this call from "web" to "api" times out. (Is it egress, ingress, or both? Or is it because the "api" service link you put into the "web" env is cluster-local, and "web" lives in a different cluster?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I can hear you thinking, "&lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; can do that for me, no problem," and &lt;em&gt;you're right&lt;/em&gt;... but every time? For every project you start from scratch? And remember, all this setup bought you a nicer dev experience (aside from having to set it up and maintain it), &lt;em&gt;but&lt;/em&gt; Kubernetes runs on top of real VMs! So the money problem isn't solved. It might even have gotten worse!&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's what I came up with
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Put your Next.js app on &lt;a href="https://vercel.com" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; (for most of us, their &lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;$20/mo tier&lt;/a&gt; is enough).&lt;/li&gt;
&lt;li&gt;Provision managed resources (&lt;a href="https://www.postgresql.org" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;, &lt;a href="https://www.mongodb.com" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt;, &lt;a href="https://kafka.apache.org" rel="noopener noreferrer"&gt;Kafka&lt;/a&gt;, &lt;a href="https://www.rabbitmq.com" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;, etc.) from &lt;a href="https://www.digitalocean.com" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;, &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;, &lt;a href="https://www.scaleway.com" rel="noopener noreferrer"&gt;Scaleway&lt;/a&gt;, &lt;a href="https://aws.amazon.com" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;, or &lt;a href="https://cloud.google.com" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;. It doesn't really matter which; they all offer good SLAs.&lt;/li&gt;
&lt;li&gt;Put your API on DigitalOcean or Scaleway, either as serverless or on &lt;a href="https://www.digitalocean.com/products/app-platform" rel="noopener noreferrer"&gt;App Platform&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set up a &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; workflow (&lt;code&gt;lint + test + build + deploy&lt;/code&gt;) as early as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this for every project is still a stretch, but when it's done properly, you can &lt;code&gt;git push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; and it'll just deploy, without having to run your own k8s.&lt;/p&gt;




&lt;p&gt;This rant covers only a fraction of what's wrong with today's cloud infra. As a developer, I have to live with it every day. So I said "enough," and now I'm building &lt;a href="https://gagarin.cloud/" rel="noopener noreferrer"&gt;Gagarin Cloud&lt;/a&gt;, where deploying a web app is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gg resource add gagarin/pg postgres
gg ship         gagarin/api:8080 &lt;span class="nt"&gt;--deps&lt;/span&gt; pg
gg ship         gagarin/web:3000 &lt;span class="nt"&gt;--deps&lt;/span&gt; api
gg domain   add gagarin/web
&lt;span class="c"&gt;# =&amp;gt; Live at https://web-3cnciet6.apps.gagarin.cloud/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a nice dependency graph is included:&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%2Fpz9poh8vpg1mrrr52hzk.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%2Fpz9poh8vpg1mrrr52hzk.png" alt="Gagarin Cloud Console Screenshot" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>kubernetes</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
