はじめに
今までは「Dockerfile」にuvやnode.jpのインストールとかファイルのコピーとかを記述していました。使い方が変わるたびにDockerイメージを作り直していたのですが面倒くさいことに気づきました。イメージからコンテナを作成するときにインストールとかファイルのコピーとかを実行するように変更しました。各種ファイル
Dockerfile
ごくごくシンプルなものにしました。FROM python:3.12-bullseye
# 必要な依存関係のインストールと環境設定
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential net-tools
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir 'smolagents[litellm,openai,gradio,mcp]'
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/*
# プロジェクト設定
WORKDIR /app
# 起動設定
CMD ["python", "-c", "print('Container ready')"]sandbox.py
ここにいろいろ記述するようにしました。# コンテナ内にディレクトリを作成 self.container.exec_run("mkdir -p /app/mcp-project/") # filesystem.pyを/app/mcp-projectにコピー tar_stream1 = io.BytesIO() with tarfile.open(fileobj=tar_stream1, mode='w') as tar: local_path1 = '/home/hoge/gemini-works/filesystem.py' arcname1 = 'filesystem.py' tar.add(local_path1, arcname=arcname1) tar_stream1.seek(0) self.container.put_archive('/app/mcp-project/', tar_stream1.read()) print("filesystem.pyを/app/mcp-projectにコピーしました") # .envを/appにコピー tar_stream2 = io.BytesIO() with tarfile.open(fileobj=tar_stream2, mode='w') as tar: local_path2 = '/home/hoge/gemini-works/.env' arcname2 = '.env' tar.add(local_path2, arcname=arcname2) tar_stream2.seek(0) self.container.put_archive('/app/', tar_stream2.read()) print(".envを/appにコピーしました") # 仮想環境のセットアップコマンドを実行 setup_commands = [ "pip install --no-cache-dir --root-user-action=ignore uv", "uv init mcp-project", "cd mcp-project && uv venv", "cd mcp-project && uv add 'mcp[cli]'" ] for cmd in setup_commands: result = self.container.exec_run( ["/bin/sh", "-c", cmd], workdir="/app", # コマンドを実行するディレクトリ ) exit_code = result.exit_code # 終了コードが0以外の場合のみエラーとして扱う if exit_code != 0: raise Exception(f"コマンド '{cmd}' の実行に失敗しました。終了コード: {exit_code}") else: print(f"コマンド '{cmd}' は正常に実行されました") print("仮想環境のセットアップが完了しました")
最終的にはこのようになっています。
import docker import time import io import tarfile class DockerSandbox: def __init__(self, image_name="agent-sandbox"): self.client = docker.from_env() self.container = None self.image_name = image_name def create_container(self): try: # コンテナを作成(ポートマッピングを追加) self.container = self.client.containers.run( self.image_name, command="tail -f /dev/null", # コンテナを実行状態に保つ detach=True, tty=True, extra_hosts={"host.docker.internal": "host-gateway"}, network_mode="bridge", ports={'7860/tcp': 7860}, # Gradioのデフォルトポート volumes={ "/home/hoge/data": {"bind": "/app/data", "mode": "rw"} } ) # コンテナ内にディレクトリを作成 self.container.exec_run("mkdir -p /app/mcp-project/") # filesystem.pyを/app/mcp-projectにコピー tar_stream1 = io.BytesIO() with tarfile.open(fileobj=tar_stream1, mode='w') as tar: local_path1 = '/home/hoge/gemini-works/filesystem.py' arcname1 = 'filesystem.py' tar.add(local_path1, arcname=arcname1) tar_stream1.seek(0) self.container.put_archive('/app/mcp-project/', tar_stream1.read()) print("filesystem.pyを/app/mcp-projectにコピーしました") # .envを/appにコピー tar_stream2 = io.BytesIO() with tarfile.open(fileobj=tar_stream2, mode='w') as tar: local_path2 = '/home/hoge/gemini-works/.env' arcname2 = '.env' tar.add(local_path2, arcname=arcname2) tar_stream2.seek(0) self.container.put_archive('/app/', tar_stream2.read()) print(".envを/appにコピーしました") # 仮想環境のセットアップコマンドを実行 setup_commands = [ "pip install --no-cache-dir --root-user-action=ignore uv", "uv init mcp-project", "cd mcp-project && uv venv", "cd mcp-project && uv add 'mcp[cli]'" ] for cmd in setup_commands: result = self.container.exec_run( ["/bin/sh", "-c", cmd], workdir="/app", # コマンドを実行するディレクトリ ) exit_code = result.exit_code # 終了コードが0以外の場合のみエラーとして扱う if exit_code != 0: raise Exception(f"コマンド '{cmd}' の実行に失敗しました。終了コード: {exit_code}") else: print(f"コマンド '{cmd}' は正常に実行されました") print("仮想環境のセットアップが完了しました") print(f"コンテナを作成しました (ID: {self.container.id[:8]}...)") except Exception as e: raise Exception(f"コンテナ作成エラー: {e}") def gradio_run(self, code: str) -> None: if not self.container: self.create_container() # バックグラウンドでPythonスクリプトを実行 self.container.exec_run( cmd=["python", "-c", code], detach=True ) # ポート待機確認 print("Gradioサーバーを起動中...", end="", flush=True) max_attempts = 10 for attempt in range(max_attempts): time.sleep(1) print(".", end="", flush=True) # netstatを使用してポートのリスニング状態を確認 netstat_result = self.container.exec_run( cmd=["bash", "-c", "netstat -tulpn 2>/dev/null | grep 7860 || echo ''"] ) if netstat_result.output: print(" 完了!") print("\n✅ Gradioアプリが起動しました") print("📊 http://localhost:7860 でアクセスできます") return None print("\n❌ サーバー起動に失敗しました") return None def _safe_decode(self, data, encoding='utf-8', errors='strict'): """バイト列か文字列かを判断して適切に処理する""" if isinstance(data, bytes): return data.decode(encoding, errors=errors) return data def cleanup(self): if self.container: try: self.container.stop() self.container.remove() print("Container stopped and removed successfully") except Exception as e: print(f"エラー: {e}") finally: self.container = None def get_logs(self): """コンテナ内のプロセス状態とログを取得""" if not self.container: return "コンテナが起動していません" # プロセス確認 ps_cmd = "ps aux | grep python | grep -v grep" ps_result = self.container.exec_run(cmd=["bash", "-c", ps_cmd]) ps_output = self._safe_decode(ps_result.output).strip() # ポート確認 port_cmd = "netstat -tulpn 2>/dev/null | grep 7860 || echo 'ポートが開いていません'" port_result = self.container.exec_run(cmd=["bash", "-c", port_cmd]) port_output = self._safe_decode(port_result.output).strip() return f"プロセス状態:\n{ps_output}\n\nポート状態:\n{port_output}" def exec_command(self, command): """コンテナ内でコマンドを実行""" if not self.container: return "コンテナが起動していません" result = self.container.exec_run(cmd=["bash", "-c", command]) return self._safe_decode(result.output, errors='ignore')
agent_runner.py
from sandbox import DockerSandbox # DockerSandboxのインスタンスを作成 sandbox = DockerSandbox() agent_code = """ try: from smolagents import CodeAgent, GradioUI, ToolCollection from mcp import StdioServerParameters import os from dotenv import load_dotenv from smolagents import LiteLLMModel load_dotenv() api_key = os.environ.get("GOOGLE_API_KEY") model = LiteLLMModel( "gemini/gemini-2.0-flash", api_key=api_key ) server_parameters = StdioServerParameters( command="/usr/local/bin/uv", args=[ "--directory", "/app/mcp-project", "run", "filesystem.py" ] ) with ToolCollection.from_mcp(server_parameters, trust_remote_code=True) as tool_collection: agent = CodeAgent( model=model, tools=[*tool_collection.tools], additional_authorized_imports=["PIL", "os", "json"] ) # エージェントの実行 GradioUI(agent).launch(server_name='0.0.0.0', server_port=7860, share=False) except Exception as e: print(f"エラーが発生しました: {str(e)}") with open('/tmp/error.log', 'w') as f: f.write(f"スタートアップエラー: {str(e)}\\n") """ try: # エージェントコンテナに関する情報を確認 print("\n⚙️ コンテナ環境を確認しています...") # コンテナを作成して基本的な情報を確認 sandbox.create_container() # Pythonとパッケージの確認 print("\nPython環境:") print(sandbox.exec_command("which python || which python3 || echo 'Pythonが見つかりません'")) print("\nPythonバージョン:") print(sandbox.exec_command("python --version || python3 --version || echo 'バージョン情報を取得できません'")) # 必要なパッケージの確認 print("\n必要なパッケージ確認:") print(sandbox.exec_command("pip list | grep -E 'smolagents|gradio' || echo 'パッケージが見つかりません'")) # Gradioアプリを起動 print("\n🚀 Gradioアプリを起動します...") sandbox.gradio_run(agent_code) # ユーザーが終了するまで待機 print("\nアプリ実行中... Ctrl+C で終了します") while True: try: cmd = input("\n> ") if cmd.lower() == "exit" or cmd.lower() == "quit": break elif cmd.lower() == "status": print("\n" + sandbox.get_logs()) elif cmd.lower() == "exec": command = input("実行するコマンド: ") print("\n" + sandbox.exec_command(command)) elif cmd.lower() == "help": print("\nコマンド一覧:") print(" status - サーバー状態を確認") print(" exec - コンテナ内でコマンドを実行") print(" exit - アプリを終了") print(" help - このヘルプを表示") elif cmd.strip() == "": pass else: print(f"不明なコマンド: {cmd}. 'help'と入力してコマンド一覧を表示") except KeyboardInterrupt: print("\n終了します...") break except Exception as e: print(f"エラーが発生しました: {e}") finally: # 終了処理 sandbox.cleanup()
実行時の画面
⚙️ コンテナ環境を確認しています... filesystem.pyを/app/mcp-projectにコピーしました .envを/appにコピーしました コマンド 'pip install --no-cache-dir --root-user-action=ignore uv' は正常に実行されました コマンド 'uv init mcp-project' は正常に実行されました コマンド 'cd mcp-project && uv venv' は正常に実行されました コマンド 'cd mcp-project && uv add 'mcp[cli]'' は正常に実行されました 仮想環境のセットアップが完了しました コンテナを作成しました (ID: 05fde52c...) Python環境: /usr/local/bin/python Pythonバージョン: Python 3.12.9 必要なパッケージ確認: gradio 5.25.0 gradio_client 1.8.0 smolagents 1.13.0 🚀 Gradioアプリを起動します... Gradioサーバーを起動中.... 完了! ✅ Gradioアプリが起動しました 📊 http://localhost:7860 でアクセスできます アプリ実行中... Ctrl+C で終了します