<?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>Forem: Murry Jeong</title>
    <description>The latest articles on Forem by Murry Jeong (@comchangs).</description>
    <link>https://forem.com/comchangs</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3850865%2F32a7308a-f4c4-414b-82f0-80dee6b27993.jpeg</url>
      <title>Forem: Murry Jeong</title>
      <link>https://forem.com/comchangs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/comchangs"/>
    <language>en</language>
    <item>
      <title>I maintained deployment bash scripts for 10 years. Then I rewrote everything in Go.</title>
      <dc:creator>Murry Jeong</dc:creator>
      <pubDate>Mon, 30 Mar 2026 07:34:31 +0000</pubDate>
      <link>https://forem.com/comchangs/i-maintained-deployment-bash-scripts-for-10-years-then-i-rewrote-everything-in-go-24ga</link>
      <guid>https://forem.com/comchangs/i-maintained-deployment-bash-scripts-for-10-years-then-i-rewrote-everything-in-go-24ga</guid>
      <description>&lt;p&gt;Every company I worked at had The Script.&lt;/p&gt;

&lt;p&gt;You know the one. &lt;code&gt;deploy.sh&lt;/code&gt;. 500 lines of bash. Written by someone who left 3 years ago. Nobody dares touch it, but everyone runs it in production every day.&lt;/p&gt;

&lt;p&gt;At one company, it was &lt;code&gt;server-manager&lt;/code&gt; — a 2,000-line monster managing 20+ services across Kafka clusters, Redis, MongoDB, Spring Boot microservices, and monitoring stacks. It worked. Until someone deployed a feature branch to production on a Friday evening. That's when we added branch checking. Then someone deployed two services simultaneously and half the servers got version A and the other half got version B. That's when we added deploy locking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every feature in the script existed because something had broken in production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After 10 years and multiple companies, the pattern was always the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;code&gt;ssh server &amp;amp;&amp;amp; git pull &amp;amp;&amp;amp; restart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add packaging (tar.gz)&lt;/li&gt;
&lt;li&gt;Add rollback (symlinks)&lt;/li&gt;
&lt;li&gt;Add health checks (port/HTTP)&lt;/li&gt;
&lt;li&gt;Add config per environment&lt;/li&gt;
&lt;li&gt;Add parallel deployment&lt;/li&gt;
&lt;li&gt;End up with an unmaintainable bash monster&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I rewrote it. In Go.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;neurosamAI/tap/tow

&lt;span class="nb"&gt;cd &lt;/span&gt;my-project
tow init                            &lt;span class="c"&gt;# scans project, generates config&lt;/span&gt;
tow auto &lt;span class="nt"&gt;-e&lt;/span&gt; prod &lt;span class="nt"&gt;-m&lt;/span&gt; api-server     &lt;span class="c"&gt;# build → deploy → health check&lt;/span&gt;
tow rollback &lt;span class="nt"&gt;-e&lt;/span&gt; prod               &lt;span class="c"&gt;# symlink switch, &amp;lt;1 second&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single binary. Zero dependencies on target servers. Just SSH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What &lt;code&gt;tow init&lt;/code&gt; does is the part I'm most proud of.&lt;/strong&gt; It auto-detects your project — language, framework, build tool, monorepo modules — and generates a complete deployment config. 10 languages, 40+ frameworks. No more writing YAML from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it clicked
&lt;/h2&gt;

&lt;p&gt;I tested Tow on real production — 22 servers running Spring Boot, Kafka, Redis, MongoDB, Prometheus, Grafana. The same infrastructure that the old bash scripts managed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tow doctor &lt;span class="nt"&gt;-e&lt;/span&gt; prod &lt;span class="nt"&gt;-m&lt;/span&gt; api-server
&lt;span class="go"&gt;  ✓ tow.yaml is valid
  ✓ SSH connection successful
  ✓ Remote dir exists
  ✓ Disk space — Available: 4.9G
  ✓ No active deploy lock
  9 passed, 0 failed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I checked the Kafka cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tow logs &lt;span class="nt"&gt;-e&lt;/span&gt; prod &lt;span class="nt"&gt;-m&lt;/span&gt; kafka &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 3
&lt;span class="go"&gt;[kafka-1] 2026-03-30 14:35:49 GC Pause Young 765M→702M 17ms
[kafka-2] 2026-03-30 14:35:55 GC Pause Young 340M→292M 18ms
[kafka-3] 2026-03-30 14:36:01 GC Pause Young 797M→723M 13ms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three servers, color-coded, one terminal. No tmux. No separate SSH sessions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tow ssh &lt;span class="nt"&gt;-e&lt;/span&gt; prod &lt;span class="nt"&gt;-m&lt;/span&gt; kafka &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"free -h | head -2"&lt;/span&gt;
&lt;span class="go"&gt;[kafka-1] Mem:  1.9Gi  1.7Gi  66Mi
[kafka-2] Mem:  1.9Gi  1.7Gi  77Mi
[kafka-3] Mem:  1.9Gi  1.7Gi  70Mi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the moment I realized the 10 years of bash scripts were worth it — not because the scripts were good, but because every painful failure taught me what a deployment tool actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned (the hard way)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Production incident&lt;/th&gt;
&lt;th&gt;Feature it became&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Two devs deployed simultaneously → inconsistent state&lt;/td&gt;
&lt;td&gt;Deploy locking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature branch deployed to prod on Friday&lt;/td&gt;
&lt;td&gt;Branch policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy "succeeded" but app was crash-looping&lt;/td&gt;
&lt;td&gt;Health checks (HTTP, TCP, log, command)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server 1 needed 4GB heap, server 2 needed 2GB&lt;/td&gt;
&lt;td&gt;Hierarchical config per server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback took 15 minutes (rebuild + redeploy)&lt;/td&gt;
&lt;td&gt;Symlink switch (&amp;lt;1 second)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New dev spent 2 days writing deploy scripts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tow init&lt;/code&gt; auto-detection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The part nobody else has
&lt;/h2&gt;

&lt;p&gt;Tow has a built-in MCP server. That means Claude, Cursor, or Windsurf can deploy your code:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Check prod status"&lt;/em&gt; → &lt;code&gt;tow status -e prod&lt;/code&gt;&lt;br&gt;
&lt;em&gt;"Show error logs from kafka"&lt;/em&gt; → &lt;code&gt;tow logs -e prod -m kafka -f ERROR&lt;/code&gt;&lt;br&gt;
&lt;em&gt;"Roll back the API"&lt;/em&gt; → &lt;code&gt;tow rollback -e prod -m api-server&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No other deployment tool does this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use...
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ansible?&lt;/strong&gt; — I tried. Ended up with 2,000 lines of YAML playbooks that nobody fully understood. Tow is one config file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capistrano?&lt;/strong&gt; — Great, but I run Spring Boot + Node.js + Python + Kafka + Redis. Not a Ruby shop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kamal?&lt;/strong&gt; — Love the philosophy, but Docker on every server is a non-starter for JVM apps that already manage their own lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes?&lt;/strong&gt; — For 5 servers? No.&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;brew &lt;span class="nb"&gt;install &lt;/span&gt;neurosamAI/tap/tow
&lt;span class="c"&gt;# or: npm install -g @neurosamai/tow&lt;/span&gt;
&lt;span class="c"&gt;# or: go install github.com/neurosamAI/tow-cli/cmd/tow@latest&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
tow init
&lt;span class="c"&gt;# edit tow.yaml with your server IPs&lt;/span&gt;
tow auto &lt;span class="nt"&gt;-e&lt;/span&gt; dev &lt;span class="nt"&gt;-m&lt;/span&gt; your-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/neurosamAI/tow-cli" rel="noopener noreferrer"&gt;github.com/neurosamAI/tow-cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://tow-cli.neurosam.ai" rel="noopener noreferrer"&gt;tow-cli.neurosam.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code&lt;/strong&gt;: &lt;a href="https://marketplace.visualstudio.com/items?itemName=neurosamai.tow-cli" rel="noopener noreferrer"&gt;neurosamai.tow-cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT License. 35 bundled infrastructure plugins. &lt;a href="https://github.com/neurosamAI/tow-cli/issues" rel="noopener noreferrer"&gt;Open issues&lt;/a&gt; welcome.&lt;/p&gt;

&lt;p&gt;If you've ever maintained The Script, you know why I built this.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>go</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
