はじめに
ある日、こんな構成で作業していることに気づいた。
- ホスト(Mac): Emacs 31.0.50 が動いている
- コンテナ(Debian 12 bookworm / ARM64): Claude Code が動いている
Emacs はホスト側にいる。Claude Code はコンテナ内にいる。 そこで素朴な疑問が浮かんだ。
ホスト側の Emacs から、コンテナ内の Clojure に
cider-jack-inできるのか?
これはただの好奇心ではなく、実用的な問いでもある。 コンテナ内で作業するなら、REPL駆動開発の恩恵もコンテナ内で受けたい。
本稿では、実装を進めながらやったことをそのまま記録していく。 うまくいかなかったことも含めて書く。
環境の確認
コンテナ内の状況を確認したところ、以下のことがわかった。
OS: Debian GNU/Linux 12 (bookworm)
アーキテクチャ: ARM64 (aarch64)
Java: 未インストール
Clojure: 未インストール
Leiningen: 未インストール
コンテナは python:3.13.6-slim-bookworm をベースに構築されており、
curl, git, build-essential などの基本ツールや Node.js, Docker CLI は入っているが、
JVM 系は何もない状態だ。
CIDER接続の仕組みを整理する
cider-jack-in は何をするか。シンプルに言えば:
lein replやclojure -M:cider/nreplなどのコマンドを 子プロセスとして起動 する- そのプロセスが立ち上げた nREPL サーバーに接続する
ここで問題になるのは、「子プロセスをどこで起動するか」だ。 Emacs がホスト上にいる以上、何も考えなければホスト上でコマンドを実行しようとする。
解決策は主に2つある。
方法1: cider-connect(手動起動)
コンテナ内で nREPL サーバーを手動起動し、ポートをホストに expose した上で
M-x cider-connect で localhost:<port> に接続する方法。
確実に動くが、=jack-in= ではないため毎回手動で起動する手間がある。
方法2: TRAMP経由でcider-jack-in
Emacs 29 以降に標準搭載された tramp-container を使う方法。
Emacs 31 ならそのまま使える。
TRAMP でコンテナ内のファイルを開けば、 Emacs はそのバッファを「リモートホスト上のファイル」として扱う。
C-x C-f /docker:cc78495d33df:/path/to/project/
この状態で cider-jack-in を実行すると、CIDER が TRAMP を検知して
コンテナ内でプロセスを起動してくれる。
ポートの expose は不要 で、TRAMP がトンネリングを担う。
今回はこの方法を採用する……つもりだった。
実装方針
確認してから Dockerfile に反映する、という流れを取る。
-
今動いているコンテナに直接 Java + Clojure CLI をインストール
openjdk-21-jdkを apt で入れる- Clojure CLI (
deps.ednベース) を公式スクリプトで入れる
- deps.edn プロジェクトを作り、nREPL 依存を追加する
-
ホスト Emacs から TRAMP 経由で cider-jack-in を試す
/docker:cc78495d33df:/...でファイルを開くcider-jack-inが通るか確認する
- 動作確認できたら Dockerfile に反映する
まず動かして、それから固める。
実装記録
Java のインストール
まず Java を入れる。=openjdk-21-jdk= を試みたが、Debian 12 bookworm のメインリポジトリには含まれていなかった。
$ apt-cache search openjdk
openjdk-17-jdk - OpenJDK Development Kit (JDK)
...(21 は見当たらない)
OpenJDK 21 は 2023年9月リリースで、bookworm(2023年6月リリース)のメインリポジトリには間に合っていない。
bookworm-backports を有効にすれば 21 を入れられるが、今回は 17 で進めることにした。
apt-get install -y openjdk-17-jdk確認すると、実はすでにインストール済みだった。
openjdk version "17.0.19" 2026-04-21
OpenJDK Runtime Environment (build 17.0.19+10-1-deb12u2-Debian)Clojure CLI のインストール
公式スクリプトでインストールする。
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh
chmod +x linux-install.sh
sudo ./linux-install.sh$ clojure --version
Clojure CLI version 1.12.5.1654deps.edn プロジェクトの作成
/tmp/clj-repl/ にプロジェクトを作る。
;; /tmp/clj-repl/deps.edn
{:paths ["src"]
:deps {}
:aliases
{:cider/nrepl
{:extra-deps {nrepl/nrepl {:mvn/version "1.3.1"}
cider/cider-nrepl {:mvn/version "0.52.1"}}
:main-opts ["-m" "nrepl.cmdline"
"--middleware" "[cider.nrepl/cider-middleware]"
"--port" "7888"]}}}依存関係を事前ダウンロードし、nREPL が起動することを確認した。
nREPL server started on port 7888 on host localhost - nrepl://localhost:7888CIDER のインストール
ホスト Emacs には CIDER が入っていなかった。=package.el= でインストールする。
M-x package-refresh-contents RET
M-x package-install RET cider RETcider-jack-in の試み(失敗)
/docker:c4379881374a:/tmp/clj-repl/ を TRAMP で開き、=cider-jack-in= を実行した。
エラー1: 直接 TCP 接続の失敗
[nREPL] Direct connection to c4379881374a:45409 failed;
try setting 'nrepl-use-ssh-fallback-for-remote-hosts' to t
CIDER がコンテナの nREPL ポートに直接 TCP 接続しようとして失敗した。
エラーメッセージの通り、=nrepl-use-ssh-fallback-for-remote-hosts= を t に設定して再試行した。
エラー2: SSH トンネルの失敗
[nREPL] SSH port forwarding failed.
Check the '*nrepl-tunnel %s*' buffer
SSH フォールバックは SSH トンネルを張ろうとするが、=/docker:= TRAMP は docker exec で動いており SSH を使わない。
そのため SSH トンネルも失敗する。
結論: cider-jack-in は Docker TRAMP では動作しない
CIDER の TRAMP 統合は SSH ベースの TRAMP メソッドを前提としている。
/docker: メソッドでは、nREPL サーバーの起動(=docker exec= 経由)はできるが、
その後の接続を TRAMP 経由でトンネリングする仕組みがない。
cider-connect への切り替え
方針を変更し、=cider-connect= で進めることにした。
- コンテナ内で nREPL をバックグラウンド起動(port 7888)
- ホスト側でポートをフォワード
cider-connectでlocalhost:7888に接続
ポートフォワードには socat を使う。
socat のインストール(Mac 側)
brew install socat を実行する前に、サプライチェーン攻撃のリスクを確認した。
Formula の検証
brew cat socat は homebrew/core が tap されていないため動作しない。
代わりに直接 Formula を取得する。
curl -s https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/s/socat.rbFormula に記載されている SHA256 を公式サイトのチェックサムと照合した。
| 確認元 | SHA256 |
|---|---|
| Homebrew Formula | f68b602c80e94b4b7498d74ec408785536fe33534b39467977a82ab2f7f01ddb |
| dest-unreach.org | f68b602c80e94b4b7498d74ec408785536fe33534b39467977a82ab2f7f01ddb |
完全一致。ソースの改ざんはない。
sandbox-exec によるインストール時のサンドボックス化
SHA256 が一致していても、Formula 自体(シェルスクリプト)が悪意のあるコードを実行する可能性は残る。 そこで、=brew install= 時のファイルシステムアクセスを OS レベルで制限することにした。
macOS 標準の sandbox-exec を使い、以下の方針でプロファイルを作成した。
- ベースは =allow default=(deny-all は brew のビルドを壊しやすい)
- ホームディレクトリへの書き込みを全禁止
- Homebrew に必要なパス(=/opt/homebrew=, キャッシュ, ログ, 一時ディレクトリ)のみ再許可
~/.ssh,~/.gnupg,~/.awsなどの機密ファイルへの読み取りも禁止
;; brew.sb(抜粋)
(allow default)
(deny file-write*
(subpath (param "HOME"))
)
(allow file-write*
(subpath "/opt/homebrew")
(subpath (string-append (param "HOME") "/Library/Caches/Homebrew"))
(subpath (string-append (param "HOME") "/Library/Logs/Homebrew"))
(subpath "/private/tmp")
(regex #"^/private/var/folders/")
)
(deny file-read*
(subpath (string-append (param "HOME") "/.ssh"))
(subpath (string-append (param "HOME") "/.gnupg"))
(subpath (string-append (param "HOME") "/.aws"))
)実行は以下のようにする。
sandbox-exec -D HOME="$HOME" -f brew.sb brew install socat動作確認
インストールは正常に完了した。
==> Pouring socat--1.8.0.3.arm64_sonoma.bottle.tar.gz
🍺 /opt/homebrew/Cellar/socat/1.8.0.3: 16 files, 970.9KB機密ファイルのブロックも確認できた。
$ sandbox-exec -D HOME="$HOME" -f brew.sb cat ~/.ssh/id_rsa.pub
cat: /Users/sximada/.ssh/id_rsa.pub: Operation not permitted
sandbox-exec は Apple が非推奨としているが、現時点では動作する。
より堅牢にするなら Lima や OrbStack などの VM 内でパッケージ管理するのが筋だろう。
ポートフォワードと cider-connect
nREPL のバージョン不一致
最初のタイムアウトエラーはバージョン不一致が原因だった。
Sync nREPL request timed out (op clone ...) after 10 secs
cider-jack-in の残骸プロセスを調べると、CIDER 1.22.0 が自動注入するバージョンは
nrepl 1.7.0 / cider-nrepl 0.59.0 だとわかった。
一方、こちらの deps.edn は nrepl 1.3.1 / cider-nrepl 0.52.1 を指定していた。
deps.edn のバージョンを CIDER に合わせて修正した。
{:extra-deps {nrepl/nrepl {:mvn/version "1.7.0"}
cider/cider-nrepl {:mvn/version "0.59.0"}}}macOS のファイアウォールダイアログ
socat 起動時に macOS から「着信接続を許可するか」というダイアログが表示された。
ここで注意が必要だった。デフォルトの TCP-LISTEN:7888 は全インターフェース
(=0.0.0.0=)でリスンするため、同じネットワーク上の他のマシンからも nREPL に
接続できてしまう。
bind=127.0.0.1 を追加してローカルホスト限定にしてから許可するのが正しい手順だ。
一度拒否してしまった場合は、以下で変更できる。
システム設定 → ネットワーク → ファイアウォール → オプション
(socat を「許可」に変更)socat のクォート問題
EXEC: に渡すコマンドは、シェルのクォート処理によって意図せず分割される場合がある。
E EXEC: wrong number of parameters (3 instead of 1)ダブルクォートで囲む方法もあるが、スクリプトに切り出す方が確実だ。
cat > /tmp/nrepl-tunnel.sh << 'EOF'
#!/bin/bash
docker exec -i c4379881374a socat STDIO TCP:localhost:7888
EOF
chmod +x /tmp/nrepl-tunnel.sh
socat TCP-LISTEN:7888,bind=127.0.0.1,fork EXEC:/tmp/nrepl-tunnel.shsocat TCP-LISTEN:7888,bind=127.0.0.1,fork "EXEC:docker exec -i c4379881374a socat STDIO TCP:localhost:7888"Emacs から接続する。
M-x cider-connect RET
Host: localhost RET
Port: 7888 RET接続成功
;; Connected to nREPL server - nrepl://localhost:7888
;; CIDER 1.22.0-snapshot, nREPL 1.7.0
;; Clojure 1.12.5, Java 17.0.19
user>ホストの Emacs からコンテナ内の Clojure REPL に接続できた。
動作確認として REPL でいくつか評価した。
user> (+ 1 2)
3
user> (System/getProperty "os.name")
"Linux"
user> (System/getProperty "os.arch")
"aarch64"
os.name が "Linux"=、=os.arch が "aarch64" であることから、
評価がホストではなくコンテナ内で行われていることが確認できた。
Dockerfile への反映
動作確認が取れたので、コンテナイメージの Dockerfile に追記した。
RUN apt-get install -y --no-install-recommends \
openjdk-17-jdk \
socat
RUN curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh \
&& chmod +x linux-install.sh \
&& ./linux-install.sh \
&& rm linux-install.shビルドが通ることを確認した。
まとめ
当初は cider-jack-in + TRAMP(=/docker:= メソッド)で接続できると考えていたが、
実際には動作しなかった。原因は CIDER の SSH フォールバックが /docker: TRAMP では
機能しないことだ。
最終的に採用した構成は以下の通り。
Mac: socat (LISTEN:7888, bind=127.0.0.1)
└─ /tmp/nrepl-tunnel.sh
└─ docker exec → コンテナ: socat (STDIO↔TCP:7888)
└─ nREPL server (port 7888)
↑
Emacs: M-x cider-connect手間はかかったが、ホストの Emacs からコンテナ内の Clojure REPL に 接続してREPL駆動開発ができる状態になった。