駒形電産

TL;DR

Scale-to-zero can make review apps, staging environments, demos, and low-traffic internal tools much cheaper to keep around. I extended the existing draft in kamal-proxy#197 and tested it with a real Rails application on a standard 2 GB Ubuntu VPS.

  • The Rails container used roughly 200–315 MiB while running and 0 B while stopped.
  • The next request woke Rails and received a valid response in about 3.15 seconds.
  • Large POST bodies, 20 concurrent wake requests, proxy restarts, failures, and deploys while sleeping were tested successfully.
  • The exact experimental kamal-proxy and Kamal revisions are available for others to try. They are not part of an official release.
  • The main open design question is whether kamal-proxy should access the Docker socket directly or delegate container lifecycle operations to another service.

The rest of this article explains the motivation, test results, problems found on real Docker, and how to reproduce the experiment.

Why I wanted scale-to-zero

Many teams run applications that need to remain available but receive traffic only occasionally: review apps, staging environments, internal tools, demos, and customer-specific applications.

Keeping every Rails container running all the time is expensive. Each one can sit there using a few hundred megabytes of memory even when nobody has accessed it for days. Kubernetes or a PaaS can solve parts of this problem, but they may add much more cost and operational complexity than these small applications justify.

For this kind of workload, a cold-start delay of a few seconds is often a reasonable tradeoff. What I wanted was the old Passenger-style behavior: stop an application after it has been idle for a while, then start it again when the next request arrives.

I have the same problem in a more specific setting. I run a programming school called FJORD BOOT CAMP. Our students build Rails applications as graduation projects, and I would like to keep those applications online after they graduate. They are useful as portfolios and as a record of what each student made, but most receive almost no traffic. Paying for a separate PaaS instance for every old project does not make much sense.

Kamal already makes it easy to run many applications on one server, and kamal-proxy already knows which container belongs to each host. So scale-to-zero in kamal-proxy felt like a natural experiment.

Starting from an existing draft

I found basecamp/kamal-proxy#197, a draft implementation by @martijnenco, and its corresponding Kamal change, basecamp/kamal#1800. The basic design was already there:

  • stop a write container after an idle timeout
  • start it on the next request
  • hold the request until the health check succeeds
  • persist the sleeping state in the existing kamal-proxy state file

I used that work as the starting point instead of creating another unrelated implementation. I then fixed the concurrency and lifecycle problems I found, added tests, and tried it against real Docker and a real Rails application.

This work is still experimental and is not part of an official Kamal or kamal-proxy release. The biggest architectural question is still open: should kamal-proxy have direct access to the Docker socket, or should container start/stop operations be delegated to another service?

The direct socket approach is wonderfully simple, but mounting /var/run/docker.sock gives the proxy effectively host-level control over Docker. That is a serious security tradeoff, not a harmless configuration detail.

What happened on a real VPS

I tested it on a fairly ordinary 2 GB Ubuntu 24.04 VPS with no swap. The server happens to be hosted by Sakura Internet in Japan, but nothing in the test depended on that provider. It used Docker Engine 29.1.3 and Docker API 1.52.

The first real test immediately found something the unit tests had missed. The original tiny Docker client used API v1.41 in its URLs, but this Docker daemon required at least v1.44. The daemon rejected every start and stop request. I changed the client to query Docker's unversioned /version endpoint once and use the API version reported by the daemon.

I also verified that kamal-proxy could continue running as its non-root user. Kamal mounts the Docker socket only when idle mode is enabled and adds the socket's numeric group ID to the proxy container. This matters because the Docker group ID on the host is not guaranteed to match a group baked into the image.

The low-level validation covered more than a happy-path GET request:

  • a 65,536-byte chunked POST body survived a cold wake unchanged
  • 20 concurrent GET and POST requests caused exactly one Docker start
  • a wake timeout returned 503 and a later request could retry successfully
  • a Docker start failure returned 503 and did not permanently break the service
  • restarting kamal-proxy while the application was sleeping preserved the state
  • deploying a new target while the old target was sleeping did not wake the stale container
  • in-flight requests, WebSockets, and streaming responses prevent idle shutdown until they finish

Then I deployed a real Rails application called Calltodo. It uses Rails, Puma, SQLite, and Solid Queue. Here are the useful numbers from the final runs:

Measurement Result
Rails boot until /up became healthy 3.36 seconds
Settled Rails container memory 205.6 MiB
Memory after a normal request 298.0 MiB
Memory after 40 requests, 10 concurrent 314.4 MiB
Memory while the Rails container was stopped 0 B
Cold wake through kamal-proxy 3.15 seconds
kamal-proxy memory about 4–5 MiB

I also ran three independent Calltodo services, each with its own container and SQLite volume. Together they used about 606 MiB while idle-but-running. After scale-to-zero, all three application containers reported 0 B. Waking only one service left the other two stopped. Waking all three at the same time completed in roughly 3.00–3.23 seconds, with one Docker start per container.

On this particular 2 GB server, three simultaneously active instances look like a sensible initial limit. The useful part is that many more applications can be installed as long as most of them are asleep. The real capacity limit becomes simultaneous wake-ups rather than the total number of deployed projects.

Of course, stopping a web container also stops anything running inside it. An application with an always-on job worker, scheduled work, WebSockets that must stay connected, or another background requirement needs a different arrangement or must leave idle mode disabled. Scale-to-zero is explicitly opt-in.

Trying the experimental forks

I published the exact code used for the experiment in two branches:

There is deliberately no upstream PR for this version yet. These branches are available for testing while the upstream design is still being discussed.

First, build the experimental kamal-proxy image and push it to a registry your VPS can access:

git clone https://github.com/komagata/kamal-proxy.git
cd kamal-proxy
git checkout b6290ce039d4565474955bb6ca3918c12324016a

docker build -t YOUR_DOCKERHUB_USER/kamal-proxy:scale-to-zero-b6290ce .
docker push YOUR_DOCKERHUB_USER/kamal-proxy:scale-to-zero-b6290ce

In the Rails application's Gemfile, use the matching experimental Kamal revision:

gem "kamal",
  git: "https://github.com/komagata/kamal.git",
  ref: "5064ede15fa7d4bed0c0917e23f61fe6f53564cf"

Run bundle update kamal, then configure the proxy in config/deploy.yml:

proxy:
  host: app.example.com
  ssl: true
  idle:
    timeout: 300
    wake_timeout: 30
  run:
    repository: YOUR_DOCKERHUB_USER/kamal-proxy
    version: scale-to-zero-b6290ce

timeout and wake_timeout are seconds. Enabling proxy.idle makes this Kamal branch mount /var/run/docker.sock, add its numeric group ID to the proxy container, and pass the idle settings to kamal-proxy. If rootless Docker or a nonstandard socket is in use, set proxy.run.docker_socket explicitly.

After updating the proxy image, deploy normally:

bundle exec kamal proxy reboot
bundle exec kamal deploy

Start with a disposable environment, a generous wake timeout, and an application whose health check is reliable. Do not put this straight into an important production server. To disable the feature, remove proxy.idle, switch back to an official Kamal and kamal-proxy release, reboot the proxy, and manually start any application container that is currently sleeping before the switch.

The design and upstream direction are being discussed in basecamp/kamal-proxy Discussion #222. Test results from other environments—especially review apps—would be much more useful than another parallel implementation.

For my use case, the result is already pretty exciting: a Rails application that consumed 200–315 MiB while nobody was using it now consumes nothing, then comes back in about three seconds. That is exactly the tradeoff I wanted.

OSSがなくなるか、少なくとも大きく衰退するのではないかという危機感を持っています。

AIがOSSを不可視化する

Vibe Coding Kills Open Source

生成AIはOSSを利用者から見えなくしていく。

これまでは利用者自身がOSSを探してREADMEを読み、作者やプロジェクトを知ったうえで利用していた。便利だと思えば感謝を伝えたり貢献したりすることもあった。

しかしVibe CodingによってAIが必要なソフトウェアを選び、組み合わせ、改変するようになると、利用者は元になったOSSの名前すら知らなくてよくなる。

OSS自体は使われ続けても、誰がどんな思想で作ったのか、どれほど役に立ったのかは見えなくなる。結果として作者への感謝や評価、支援、貢献も減っていく。

オーダーメイドソフトがOSSを不要にする

生成AIによって一人ひとりの用途や好みに合ったオーダーメイドのソフトウェアを簡単に作れるようになった。

既存のOSSを探して使い方を覚え、自分に合わない部分を我慢するより、最初から自分専用のものを作るほうが早い。

そうなれば多くの人が同じソフトウェアを共有する必要は薄れ、OSSの存在価値も小さくなる。

ソフトウェアは増えるがOSSは減る。

何が困るの?

僕はOSS文化そのものが大好きなので、なくなると非常に困る。

ただ一般の人から見れば「それって困ることなの?」と思うかもしれない。

しかしこれは単に僕が寂しいというだけではなく、社会全体にとっても良くないことだと思う。

共有地の悲劇

コモンズの悲劇 - Wikipedia

一人ひとりにとってはOSSを探して使うより、AIで自分専用のソフトウェアを作るほうが早くて便利かもしれない。

しかしみんながその選択をすると、改善やバグ修正、設計上の知見が共有されなくなる。社会全体で同じものを何度も作ることになり、品質を共同で検証できる共通基盤も育たない。ソフトウェア技術の蓄積も弱くなる。

一人ひとりにとって合理的な選択でも、その積み重ねによって誰もが利用できる共有資産が失われていく。

これがAI時代のOSSにおける「共有地の悲劇」ではないだろうか。

悲劇を避けるには

共有地の悲劇に対しては、利用者の善意だけに頼るのではなく、資源を守るための仕組みを作るのが一般的だ。

たとえば漁場なら漁業組合や政府が魚を捕ってよい時期や量を決める。利用者同士でルールを作って監視し、ときには違反に罰則を設ける。

漁場から利益を得る人が、その維持にも責任を持つようにする。

どうすればいいのか

OSSでも同じような考え方が必要になるのかもしれない。

AIや企業がOSSから大きな利益を得るなら、その一部がOSSの維持に戻る仕組みを作る。AIがどのOSSを利用したのか分かるようにする。企業や政府、業界団体が基金を作り、共有資産として支える。AIによって生まれた改善を、元のOSSへ戻しやすくする仕組みや標準を用意する。

もちろん漁場とOSSは同じではない。魚は捕れば減るが、ソフトウェアはコピーしても減らない。

減っていくのは作る人の意欲や改善を共有する習慣、コミュニティの活力だ。

それでも共有地を利用する人が、その維持にも参加する仕組みを作るという考え方は参考になる。

AI時代のOSSにも、漁業組合のように共有資産を共同で守る新しい制度や運動が必要になるのかもしれない。

部品や道具をちまちま集めてきて時間かかったけど、自作アケコンがやったとできた。嬉しい〜😭

自作アケコン

散らかり机。

自作アケコン完成直後

かなりギリギリのサイズで中に収めるのに苦労しました。無理やり詰め込んでパンパンです。格闘ゲーム用にはもっと大きいほうが安定するのでシューティングやレトロゲーム用にしようかな。

3Dプリントとか、塗装とか、ラズパイPICOとか色々と苦戦しましたがとっても勉強になりました。

使ったパーツ

基盤

ケーブル類

レバー

三和電子 JLX2-TP-8Y ジョイスティック

ボタン

ケース

レトロアーケードスティック

次は・・・

なんとかできましたが、色々改善したい点があるので次のを作りたい。

  • ピンヘッダが真横に出てる形だけど上面に出てる形の方が収まりがいい
  • マットな仕上げにしたが、ツルツル仕上げもやってみたい
  • 既存の3Dデータを使ったが、自分でデザインしたモデルで作りたい
  • 格闘ゲーム用にもっとデカくて重りとかを入れて安定したのを作りたい

さっさとスト6始めたほうがいいと思います。

Bambu Lab A2L買った!

去年末のAmazonブラックフライデーに定番のA1 miniを買って使ってたんだけど、僕の印刷したい大きめのデータが軒並み印刷できないのと、ブロブ発生からの直そうとして破壊してしまっていてしばらく印刷できてませんでした。

A1 mini blob

A1を買おうと思ってBambu Labのサイトを見に行ったら2日後に次のモデルが出るらしいということを知って新モデルのA2Lを買いました。

Image from Gyazo

デカくてめっちゃカッコいい😭

A1の256mmでも十分だったのにさらにデカい330mm×320mm×325mmは最高です。またブロブ発生を検知する機能がついたらしく、それでA1 miniを破壊した僕にとってはうってつけ。

A1 miniで使ってたAMS liteもそのまま使えるということで快適さに日々感動しております。

蛇足

A1 miniはほとんどのパーツがバラで買えるみたいなので、直すべきか、ジャンクとしてメルカリにでも出すべきか、捨てるべきか…。

Android用のシンプルなテキストエディター、TweekTextを作った。

TweekText

Androidゲーム機で設定ファイルを編集したい

RG RotateみたいなAndroidゲーム機を使っていると、ちょっとした設定ファイルを編集したくなることがある。XMLとかINIとかYAMLとかJSONとか、そういうやつ。

別にプログラムを書きたいわけではない。ただファイルを開いて、ちょっと直して、保存したいだけ。

しかし、Androidでそういう用途にちょうどいい無料・広告なしのプレーンなエディターが見つからなかったので作りました。

TweekText

TweekTextはAndroid 12以上で動く、シンプルなプレーンテキストエディターです。

  • Android標準のファイルピッカーで開く・保存
  • UTF-8
  • 既存ファイルの改行コードを維持
  • 検索・置換
  • XML / INI / YAML / JSON などの設定ファイル向けのシンタックスカラー
  • 無料
  • 広告なし

インストール方法

Google Playではまだ公開していない。

GitHub ReleaseにAPKを置いてあるので、そこから直接ダウンロードしてインストールする方式。

https://github.com/komagata/TweekText/releases/latest

インデントベースのコンパイル言語 Tya を作りました。

Tya — A language without hesitation

Image from Gyazo

Pythonっぽいインデントベースの見た目で、goみたいに1バイナリにコンパイルできる感じの言語です。

# hello.tya
user = { name: "komagata", age: 20 }

greet = user -> "Hello, {user["name"]}!"

if user["age"] >= 20
  print(greet(user))
$ tya build hello.tya -o hello
./hello
Hello, komagata

変わった特徴

ちょっと変わってるところは、動的型付け言語なのにコンパイル言語というところと、ソースコードとASTが情報の欠落なく相互に変換可能(Lossless AST)で、コメントも含めてソースコード表現が一意に決まるところです。(Canonical syntax

要するにgoで言えばgo fmtした結果だけが唯一の正解で、それ以外はSyntax Errorみたいな感じです。

迷わない言語をうたっていて、Syntaxだけじゃなくて、選択肢をなるべく1つだけ用意してそれ以外はできないようにしています。(迷わない言語 = 極端に自由度の少ない言語)

開発のモチベーション

なぜ作ったかというと、僕は以前、coffeescriptの書き味が好きだったんですが、周りでは評判が悪く、「もしかして誰も好きじゃない?」と悲しい思いでいました。僕だけrailsプロジェクトでcoffeescriptを書き続けてたら周囲の迷惑になっちゃうので書かなくなりました。

coffeescriptみたいな書き味でgoみたいに1バイナリにコンパイルできる言語があったらいいなぁと思ってたのでAIの力を借りて作ってみた次第です。

一応、自分でも小さいツールやwebアプリ、小さめのGUIアプリなんかを書くのに使えているので興味がある方がいらっしゃったら使ってみていただけるとありがたいです。感想などをいただけるとさらにありがたいです。

簡単に自撮り動画を撮って、そのままDiscordやSlackに貼るための小さいアプリを作った。

mugvideo

mugvideo

何これ

Web会議ではなく、チャットにちょっとした動画を貼りたい時用のアプリ。

録画ボタンを押すとカメラとマイクでMP4を録画して、停止すると保存した動画ファイルそのものをクリップボードに入れる。あとはDiscordやSlackにペーストすればアップロードできる。

ファイル選択ダイアログを開いて、動画の保存先を探して、添付して……みたいなのが毎回ダリィので、

「撮る → 止める → 貼る」

だけにしたかった。

インストール

OmarchyなどのArch Linux系ならAURから入る。

yay -S mugvideo-git

Arch系以外は下記。 curl -fsSL https://raw.githubusercontent.com/komagata/mugvideo/main/install.sh | sh

モチベーション

リモートワークで、ちょっとした内容なんだけどテキストだけだと説明が難しいとか、感情が伝わりずらい事がよくある。テキストだとな〜んか感じ悪い表現になっちゃうとか。そういう場合に動画は有用だと思うけど、ハドルみたいに同期コミュニケーションは重過ぎたり、大勢に一度に伝えたい場合とかに自撮り動画って便利だなと思ってちょくちょくやってた。それを手軽にするために作りました。

自分用の小さいツールだけど、こういう「毎回ちょっと面倒」みたいなやつを雑に作ってすぐ使えるのはLinuxデスクトップの楽しいところだと思う。

しばらく使ってみて、録画サイズや保存先周りで不便が出たら直していく。

Hyprland上でMarkdownをサッと見るためのアプリ、mark-previewを作った。

mark-preview

OmarchyでMarkdownファイルを見るとき、これまではObsidianで開いてたんだけど、もっとシンプルに確認したかったのでpreview専用でhyperlandにマッチする装飾の少ないやつを作りました。

シンプルにした

Hyprlandの世界観に合うようにメニューバーなしのほぼ全画面ビュー。ステータスバーに開いてるファイルパスがちらっと出るだけ。操作はキーボードショートカット(Ctrl+O / Ctrl+R / Ctrl+W / Ctrl+Q)でやる。スクロールバーもNautilus風にホバーで薄く出る感じにした。

編集は別アプリでやる

mark-preview自体には編集機能がない。Neovimでなどの別アプリで編集する。元ファイイルの変更を監視してリアルタイムにプレビューする。

各OSで自然な見た目になる

wxWidgetsを使ってるのでOS毎のネイティブな見た目になる。Electronとかは重いので使わなかった。

Arch / Omarchyなら簡単インストール

AURに登録したので、

yay -S mark-preview-git

で入る。/usr/bin/mark-previewにバイナリが入って、.desktopファイルも一緒に入るのでwalkerからも起動できるし、.mdファイルを右クリックして「他のアプリで開く」にも出てくる。

omarchy-coding-agent-launcher running multiple coding agents in tmux

Claude Code用に作っていたomarchy向けのプロジェクトランチャーを、特定のコーディングエージェントに依存しない形に作り直しました。

omarchy-coding-agent-launcher

以前の名前は omarchy-claude-launcher でしたが、今はClaude Codeだけではなく、Codex、Gemini CLI、OpenCodeも扱えるので、新しい名前でリリースし直しました。

Claude Code専用ではなくなった

最初に作ったときは、Claude CodeをOpenClawっぽく使いたい、というかなり個人的な動機でした。

Super+I を押すとwalkerのポップアップが出て、プロジェクトを選ぶと、そのプロジェクト用のClaude Codeがtmuxの中で開く。複数プロジェクトを行き来しても、ターミナルが散らからない。そういうものです。

ただ、Claude Codeが週のLimitに達して数日使えなくなって発狂してCodexやGemini CLIでも全く同じように使えるようにしました。

プロジェクトごとにエージェントを選べる

プロジェクトごとにコーディングエージェントを切り替えられるので、1画面に9個立ち上げて、5個がClaude Code、3個がCodex、2個がGeminiみたいな使い方ができます。(というかCodexも頻繁にリミットに行くので仕方なくそうなっている・・・)

グローバルのデフォルトは環境変数で指定できます。

export CODING_AGENT_LAUNCHER_AGENT=codex

さらに、プロジェクトの中に .agents/agent を置くと、そのプロジェクトだけ別のエージェントを使えます。

claude

とか、

gemini

みたいに書くだけです。

ランチャーのメニューにも + Set project agent... を追加したので、ファイルを手で作らなくても、walker上でプロジェクトを選んで claude / codex / gemini / opencode を選べます。

インストール

omarchy / Hyprland向けです。walker, hyprctl, tmux と、使いたいコーディングエージェントのCLIが必要です。

git clone https://github.com/komagata/omarchy-coding-agent-launcher.git
cd omarchy-coding-agent-launcher
./install.sh
hyprctl reload

詳しくはREADMEを見てください。

まとめ

最初は「Claude Codeを複数プロジェクトで使いやすくするための個人用ランチャー」でしたが、今は「どのコーディングエージェントを使うかをプロジェクト単位で切り替えるランチャー」になりました。

Claude Code、Codex、Gemini CLI、OpenCodeを同じ操作感で扱えて、プロジェクトごとに向き不向きを選べるのが良いところです。

最悪でもOpenCodeで無料のローカルLLMを使えるので発狂は防げますが、Qwenとかじゃあまり満足できないのでイライラはするかも。

前回の記事で紹介したomarchy-claude-launcherをアップデートした。

一番大きい変更は、プロジェクトの管理がtmuxの「ウィンドウ(タブ)」から「ペイン(分割画面)」に変わったこと。

Image from Gyazo

全エージェントを同時に見る

前のバージョンは1プロジェクト=1タブで、Alt+1Alt+9で切り替えてた。

これはこれで良かったんだけど、Claude Codeを4つ5つ並行して走らせてると一覧したくなる。タブだと表示されるのは常に1つだけで、他は見えない。

Claude Squadみたいに分割画面で全部見たいなと思って、ペインベースに作り直した。

今は全プロジェクトが1つのtmuxウィンドウ内のペインとして並ぶ。select-layout tiledで自動的にグリッド状に配置されて、4個なら2×2、6個なら2×3になる。Ctrl+Space zで1つにズームして集中、もう一回押して全体に戻る、という使い方。

ペインのボーダーにプロジェクト名が表示されるようにした。tmuxの@projというユーザー独自オプションに名前を入れることで、claude等のTUIアプリがエスケープシーケンスでタイトルを上書きしてくる問題を回避してる。ボーダーはheavyにして見やすく。

その他の変更

他にもちょこちょこ追加した:

  • ghostty対応: 前回はNVIDIA+mesa問題でalacrittyしか使えなかったけど、ghostty 1.3.1で解消。--titleベースのウィンドウ識別に変更
  • 稼働状態インジケーター: walkerの一覧で(ツール実行中)、(入力待ち)が表示される。pstreeでプロセスツリーを見てclaude配下にbashが生えてたら作業中、という判定
  • 名前を打つだけでプロジェクト作成: 一覧にない名前を入力すると自動でディレクトリを作ってclaudeを起動。CLAUDE_LAUNCHER_DEFAULT_NSを設定しておけばchatと打つだけで~/Works/komagata/chat/ができる
  • git worktree管理: walkerから親リポジトリを選んでworktree名を入れるだけ。docker compose連携で.env.localの自動生成やポートの自動割り当てもやる
  • 削除機能: 不要になったプロジェクトやworktreeをwalkerから選んで削除。tmuxのペインkill→ファイル削除まで一発
  • CLAUDE_LAUNCHER_CLAUDE_ARGS: --dangerously-skip-permissionsみたいな引数を環境変数で渡せるようにした

感想

ペインベースにしてから明らかに並行作業の見通しが良くなった。5個のclaudeが同時に動いてるのを眺めながら、止まったやつに指示を出して、また全体を見る、という流れがスムーズ。

tmuxのtiled layoutが地味に賢くて、ペインの増減に合わせて勝手にいい感じに再配置してくれるのがありがたい。