<?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: Roman</title>
    <description>The latest articles on DEV Community by Roman (@kield01).</description>
    <link>https://dev.to/kield01</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%2F35009%2F40ba281b-987e-4cea-ad1a-bec400be8cb7.jpeg</url>
      <title>DEV Community: Roman</title>
      <link>https://dev.to/kield01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kield01"/>
    <language>en</language>
    <item>
      <title>I built a self-hosted deploy tool that looks like tmux (and can be driven by an AI agent)</title>
      <dc:creator>Roman</dc:creator>
      <pubDate>Sat, 17 Oct 2026 22:22:50 +0000</pubDate>
      <link>https://dev.to/kield01/i-built-a-self-hosted-deploy-tool-that-looks-like-tmux-and-can-be-driven-by-an-ai-agent-3ldn</link>
      <guid>https://dev.to/kield01/i-built-a-self-hosted-deploy-tool-that-looks-like-tmux-and-can-be-driven-by-an-ai-agent-3ldn</guid>
      <description>&lt;p&gt;I deploy a handful of projects to a handful of my own servers. Not enough to justify Kubernetes, too many to keep doing &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;git pull&lt;/code&gt;, &lt;code&gt;docker compose up -d&lt;/code&gt; and hoping. I didn't want a PaaS, a cloud control plane, or an agent running on every server.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/KielD-01/deploy.me" rel="noopener noreferrer"&gt;deploy.me&lt;/a&gt;&lt;/strong&gt;: a local, Dockerized admin app. You run it on your own laptop (macOS, Linux, or Windows with WSL2) at &lt;code&gt;https://deployme.localhost&lt;/code&gt;. It reads your servers from &lt;code&gt;~/.ssh/config&lt;/code&gt;, imports your projects from GitHub or a local folder, and deploys with nothing on the other end but &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's MIT licensed, and I built it together with Claude. Here's what I think is worth telling about.&lt;/p&gt;

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

&lt;p&gt;A deploy is a pipeline of recorded steps, each with its own log:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight:&lt;/strong&gt; is the server reachable, is the folder locked by another run, is another Compose project already running from it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver:&lt;/strong&gt; an ordered list of strategies (GHCR image for the commit, push an archive, rsync, or let the server fetch from git). The first one that applies wins, and the trace tells you why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; &lt;code&gt;releases/&amp;lt;timestamp&amp;gt;&lt;/code&gt; hard-linked against the live one, shared folders and env files symlinked in, then an atomic switch of &lt;code&gt;current&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose:&lt;/strong&gt; &lt;code&gt;compose config&lt;/code&gt; first, so a broken file stops the deploy before anything is built. Then build or pull, &lt;code&gt;pg_dump&lt;/code&gt;, migrations in a one-off container, and &lt;code&gt;up -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health gate:&lt;/strong&gt; the services are healthy, the health URL answers, and your own command succeeds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the gate fails after the switch, &lt;strong&gt;it switches back by itself&lt;/strong&gt;. With blue/green on, the edge-facing services start &lt;em&gt;beside&lt;/em&gt; the running ones, and the old ones are removed only once the new colour is healthy, so a bad deploy costs no downtime.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9znp3kxxe7cq313oqtf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9znp3kxxe7cq313oqtf.webp" alt="A run whose health check failed and was switched back automatically" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI is a tmux session
&lt;/h2&gt;

&lt;p&gt;Early on I asked for a redesign, and ended up with something more committed than a dark mode: the whole app can be a &lt;strong&gt;tmux session&lt;/strong&gt;. Panes you split with &lt;code&gt;Ctrl+b %&lt;/code&gt; and &lt;code&gt;Ctrl+b "&lt;/code&gt;, a status bar with windows &lt;code&gt;0:ops 1:runs 2:servers…&lt;/code&gt;, F1–F6 to jump between them, and a shell pane where the real work happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy@local:~$ status
   TARGET                 LIVE     HEALTH            LAST RUN
 ● atelier@edge-fra       94115db  ||||XX|||||||||   #19 ✓
 ● warehouse@edge-fra     9fabf06  ||||||||||||||||  #20 ✓
 4 targets · 0 running · 0 failing
deploy@local:~$ deploy warehouse edge-fra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy&lt;/code&gt; is a small wizard: pick the commit, see the plan (what changes, which strategy and why, whether a commit brings a migration), then it asks &lt;code&gt;y/N&lt;/code&gt;. A flag is never taken as the yes. It's a theme, not a fork: the same Livewire components sit behind a Classic UI with cards and tables, and you switch in Settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1h3o8dokgxlggm4yxs0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1h3o8dokgxlggm4yxs0.webp" alt="The Console theme: a shell, the latest run's log, htop for a server, git log for a project" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first page loads the frame and the shell immediately; the panes that need data (tail, htop, git) load right after, each in its own request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting an AI agent in, carefully
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;php artisan deployme:mcp&lt;/code&gt; is an MCP server over stdio. Point Claude, Cursor or VS Code at it and the agent can look around, explain why a run failed, and plan a deploy. It's &lt;strong&gt;read-only by default&lt;/strong&gt;. The tools that change anything (deploy, roll back, cancel) appear only with &lt;code&gt;--allow-deploys&lt;/code&gt;, and your MCP client asks you before each call. Answers never carry a token, an env file's contents, or the &lt;code&gt;APP_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security decisions I'd make again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets never go in argv or logs.&lt;/strong&gt; Remote scripts get them over stdin, and every log line is redacted before it is stored or broadcast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkey or password sign-in, one account.&lt;/strong&gt; Revealing an env file, trusting a new host key, or changing where alerts go asks for a fresh confirmation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host keys are trusted on first use and then pinned.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An append-only audit log.&lt;/strong&gt; A database trigger refuses &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; only inside the nightly prune. Every remote command is in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server you can't reach directly&lt;/strong&gt; goes through a jump host, a SOCKS5/HTTP proxy or your VPN, and that route is checked before every probe and deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building it with an agent
&lt;/h2&gt;

&lt;p&gt;A good part of this was written with Claude Code, and the most useful thing I did was not asking for features. I pointed an agent at the codebase with one rule: &lt;strong&gt;every suspected bug needs a failing test before it gets a fix&lt;/strong&gt;. It came back with dozens of fixed bugs, each one proven: a deploy that could run in two workers at once, secrets leaking into a failed step's error text, a health alert that fired on every check instead of on change. A rule that makes the agent show its work turned out to matter more than which model was running.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/KielD-01/deploy.me/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That clones the repo, configures it for your OS, builds, and prints a one-time link to set up a passkey or password. The PHP base image is prebuilt for amd64 and arm64 on Docker Hub, so the first build takes minutes, not a coffee break. &lt;code&gt;bin/update&lt;/code&gt; keeps it current (backup, rebuild, doctor, and back out if it fails), and can do the patch releases by itself at night if you switch that on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/KielD-01/deploy.me" rel="noopener noreferrer"&gt;https://github.com/KielD-01/deploy.me&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Screenshots: &lt;a href="https://github.com/KielD-01/deploy.me/blob/main/docs/gallery.md" rel="noopener noreferrer"&gt;https://github.com/KielD-01/deploy.me/blob/main/docs/gallery.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP guide: &lt;a href="https://github.com/KielD-01/deploy.me/blob/main/docs/mcp.md" rel="noopener noreferrer"&gt;https://github.com/KielD-01/deploy.me/blob/main/docs/mcp.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd like to hear what's missing for the way you deploy. Issues and PRs are welcome.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>laravel</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Time Optimization</title>
      <dc:creator>Roman</dc:creator>
      <pubDate>Thu, 29 Aug 2019 10:16:05 +0000</pubDate>
      <link>https://dev.to/kield01/time-optimization-1b03</link>
      <guid>https://dev.to/kield01/time-optimization-1b03</guid>
      <description>&lt;h2&gt;
  
  
  Why for?
&lt;/h2&gt;

&lt;p&gt;From time to time I used to use some web projects, which helps me to find something out within the game I play (Ingress Prime).&lt;/p&gt;

&lt;p&gt;So, recently I have started collection banners, images, with the missions.&lt;br&gt;
To see the banners, I use &lt;a href="https://ingressmosaik.com" rel="noopener noreferrer"&gt;Ingress Mosaic&lt;/a&gt; website.&lt;/p&gt;

&lt;p&gt;To reach the SpecOps Onyx medal, I have to complete 500 missions, but to get in top - at least 1400. So, to count the missions and length I am using Laravel 5.8 with the CLI.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I did?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Made an artisan command with the &lt;code&gt;php artisan make:command&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Installed dependency of &lt;code&gt;fabpot/goutte&lt;/code&gt; to make an HTTPS Requests and work with the content efficiently&lt;/li&gt;
&lt;li&gt;Learned the structure of the pages I need to work with&lt;/li&gt;
&lt;li&gt;Go Code!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firstly, I had to make an alias for the name of the command to be executed from the CLI. I've came up with the next shorthand: &lt;code&gt;profile:parser {nickname}&lt;/code&gt;.&lt;br&gt;
30 minutes - and I've came with the next code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        /** Fetching profile page **/
        $this-&amp;gt;http = new Client([
            'base_uri' =&amp;gt; 'https://ingressmosaik.com'
        ]);

        /** Transforming the response to the DOM **/
        $dom = new Crawler($this-&amp;gt;http-&amp;gt;get("/profil/{$this-&amp;gt;argument('nickname')}")-&amp;gt;getBody()-&amp;gt;getContents());
        $todoItems = $dom-&amp;gt;filter('div#this_todo &amp;gt; div');

        $mosaics = [];

        /** Fetching no. of the mosaics **/
        $todoItems-&amp;gt;each(function (Crawler $node) use (&amp;amp;$mosaics) {
            $link = $node-&amp;gt;filter('h5 &amp;gt; a')-&amp;gt;attr('href');
            list(, , $no) = explode('/', $link);
            $mosaics[] = intval($no);
        });

        $targets = [
            'cities' =&amp;gt; [],
            'missions' =&amp;gt; 0,
            'distance' =&amp;gt; 0.00
        ];

        collect($mosaics)
            -&amp;gt;each(function (int $id) use (&amp;amp;$targets) {
                /** Fetching mission page **/
                $missionPage = new Crawler($this-&amp;gt;http-&amp;gt;get("/mosaic/{$id}")-&amp;gt;getBody()-&amp;gt;getContents());
                $title = $missionPage-&amp;gt;filter('div.panel-heading-1 &amp;gt; h4');
                $block = $missionPage-&amp;gt;filter('div#mo_img &amp;gt; div.panel-body-1');

                list($title,) = explode(' - ', $title-&amp;gt;text());

                $city = $block-&amp;gt;filter('div &amp;gt; span')-&amp;gt;eq(1)-&amp;gt;text();
                $mc = $block-&amp;gt;filter('div &amp;gt; span')-&amp;gt;eq(6)-&amp;gt;text();
                $lt = $block-&amp;gt;filter('div &amp;gt; span')-&amp;gt;eq(7)-&amp;gt;text();
                $startPortal = $missionPage-&amp;gt;filter('div#portals &amp;gt; table')-&amp;gt;eq(0)
                    -&amp;gt;filter('a')
                    -&amp;gt;eq(1);

                $title = trim($title);

                $this-&amp;gt;output-&amp;gt;text("Parsing '{$title}'");

                $targets['cities'][$city][] = [
                    'mosaic' =&amp;gt; trim($title),
                    'first_portal' =&amp;gt; $startPortal-&amp;gt;attr('href'),
                    'missions' =&amp;gt; intval($mc),
                    'distance' =&amp;gt; floatval($lt)
                ];

                $targets['missions'] += intval($mc);
                $targets['distance'] += floatval($lt);
            });

        $targets['distance'] = (float)number_format($targets['distance'], 2);

        foreach ($targets['cities'] as $city =&amp;gt; $mosaicsList) {
            $targets['cities'][$city] = collect($mosaicsList)
                -&amp;gt;sortBy('missions', SORT_DESC, true);
        }

        file_put_contents(public_path('missions.json'), json_encode($targets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, this code is ugly, as all of it is in the &lt;code&gt;handle()&lt;/code&gt; method. But it's a quick solution to calculate all to-do banners and save all data into the JSON file.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>cli</category>
      <category>web</category>
    </item>
  </channel>
</rss>
