Coding Agent に専用のコンテナ化された sand box 環境を提供する、Container Useが話題ですが、「Container Use はコンテナ化に Dagger を利用している」と聞いて、Dagger を触ってみました。
(Container Use は、現時点(2025-07-17)では Dagger API を使って複数のサービスを明示的に構築する方法は提供されていないらしいのですが、プロンプトからサービスの追加を依頼すると、依頼通りの構築をしてくれる、という最低限の機能は実装されているそうです)
Dagger とは
Dagger は、CI/CDパイプラインを構築するためのツールで、(yamlではなく、)Go/Python/TypeScript/PHPなどのプログラミング言語で書いた関数を組み上げて、CI/CDロジックを書くことができます。
同じコードを、ローカル上で動かすことも、CIサービス上で動かすこともできます。
また、 Daggerverse というレジストリで、作成済みのモジュールを再利用することもできます。
詳細は下記ページを参照してください。
Intro to Dagger | Dagger
今回試したいこと
本来の CI 用途と離れていますが、Dagger を使って、ダミーのSMTPサーバー(maildev)と Django のアプリケーションを同時に起動し、Django から送信されたメールがSMTPサーバーに送られているか確認できる環境を作成します。
Dagger をインストールする
Installation | Dagger を参照して dagger をインストールします。
私は macOS を利用したので、以下コマンドを実行しました。
% brew install dagger/tap/dagger
Dagger module を初期化する
Dagger を利用したいプロジェクトのルートフォルダにて、以下のコマンドを実行します。
今回は Django のプロジェクトに適用してみたので、 Python を利用しました。
% dagger init --sdk=python --name=django-test
proxy モジュールを追加する
Daggerverse から proxy というモジュールをインストールします
% dagger install github.com/kpenfound/dagger-modules/proxy@v0.2.5
このモジュールは、ホストとの通信を proxy するサービスを起動してくれるものです。Dagger そのものには、複数のサービスのポートをホスト側に公開する API を備えていないため、こちらのモジュールを使って実現します。
dagger-modules/proxy at main · kpenfound/dagger-modules · GitHub
初期化とインストールが完了すると、下記のようなフォルダとファイルが作成されます。
.
├── dagger.json
├── .dagger
│ └── pyproject.toml
│ └── src
│ └── django_test
│ ├── __init__.py
│ └── main.py
...
dagger.json は以下のような内容です。
{
"name": "django-test",
"engineVersion": "v0.18.12",
"sdk": {
"source": "python"
},
"dependencies": [
{
"name": "proxy",
"source": "github.com/kpenfound/dagger-modules/proxy@proxy/v0.2.5",
"pin": "3e9129a119151eb122e5fe380bd2d4313d8fc001"
}
],
"source": ".dagger"
}
追加された proxy モジュールと、初期化時に作成された django-test モジュール、そのソースが格納されたパスなどの情報が記載されています。
Dagger のモジュール django-test を実装する
Build a CI Pipeline | Dagger や Cookbook | Dagger を見ると雰囲気がわかります。
Python の SDK リファレンス はこちらです。
Dagger Python SDK — Dagger Python SDK documentation
以下はベースを Claude Code に書いてもらい、一部修正したものです。
- Python のパッケージマネージャーには uv を使っています
- maildev 関数では、maildev のサービスを構成しています
- runserver 関数の最後で、django と maildev のポートを proxy モジュールを使ってホスト側に公開しています
import dagger
from dagger import dag, function, object_type
@object_type
class DjangoTest:
@function
def prepare_uv(
self,
source: dagger.Directory,
) -> dagger.Container:
"""Prepare Base Image For Testing
Args:
source: Django project directory
Returns:
Container with Base Image
"""
# Note: with_directory() is the correct method for Python SDK
# It copies the directory into the container's filesystem
return (
dag.container()
.from_("python:3.12-slim")
.with_exec(["apt-get", "update"])
.with_exec(["apt-get", "install", "-y", "curl", "git"])
.with_exec(
[
"curl",
"-LsSf",
"https://astral.sh/uv/install.sh",
"-o",
"/tmp/install-uv.sh",
]
)
.with_exec(["chmod", "u+x", "/tmp/install-uv.sh"])
.with_exec(["sh", "/tmp/install-uv.sh"])
.with_env_variable("PATH", "/root/.local/bin:$PATH", expand=True)
# .with_directory("/app", source)
.with_mounted_directory("/app", source)
.with_workdir("/app")
)
@function
def maildev(self) -> dagger.Service:
"""Run maildev SMTP test server for email testing
Returns:
Service running maildev SMTP server
Example:
dagger call maildev up --ports 1080:1080 --ports 1025:1025
Access Maildev Web UI at: http://localhost:1080
SMTP server available at: localhost:1025
"""
return (
dag.container()
.from_("maildev/maildev:latest")
.with_exposed_port(1025) # SMTP port
.with_exposed_port(1080) # Web UI port
.as_service()
)
@function
def runserver(
self,
source: dagger.Directory,
) -> dagger.Service:
"""Run Django development server with maildev SMTP test server
Args:
source: Django project directory
Returns:
Service running Django development server with maildev dependency
Example:
# Start both services in parallel:
dagger call runserver --source=. up
Access Django at: http://localhost:8080
Access Maildev Web UI at: http://localhost:1080
"""
# Create cache volume for uv packages
uv_cache = dag.cache_volume("uv-cache")
# Create maildev service for internal communication
maildev_service = self.maildev()
# Prepare container with Django
container = (
self.prepare_uv(source)
# Mount uv cache
.with_mounted_cache("/root/.cache/uv", uv_cache)
# Install dependencies
.with_exec(["uv", "sync"])
# Run migrations
.with_exec(["uv", "run", "python", "manage.py", "migrate"])
# Load initial data
.with_exec(["uv", "run", "python", "manage.py", "loaddata", "initial_data"])
# Bind to maildev service for internal communication
.with_service_binding("maildev", maildev_service)
# Configure Django email settings to use maildev
.with_env_variable("EMAIL_HOST", "maildev")
.with_env_variable("EMAIL_PORT", "1025")
.with_env_variable("EMAIL_HOST_USER", "")
.with_env_variable("EMAIL_HOST_PASSWORD", "")
.with_env_variable("EMAIL_USE_TLS", "False")
.with_env_variable("EMAIL_USE_SSL", "False")
.with_env_variable("DEFAULT_FROM_EMAIL", "test@example.com")
.with_env_variable("SERVER_EMAIL", "test@example.com")
# Expose Django's default port
.with_exposed_port(8000)
)
# Convert container to service
djang_service = container.as_service(
args=["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
)
return (
dag.proxy()
.with_service(maildev_service, "maildev_service", 1080, 1080)
.with_service(djang_service, "django_service", 8000, 8000)
.service()
)
使ってみる
以下コマンドで利用できる function の一覧が表示されます
% dagger functions
▶ connect 1m51s
▶ load module: . 29.6s
Name Description
maildev Run maildev SMTP test server for email testing
prepare-uv Prepare Base Image For Testing
runserver Run Django development server with maildev SMTP test server
dagger.Service を返す function を実行する場合、 up というサブコマンドが使えます。
% dagger call runserver --help
▶ connect 0.9s
▶ load module: . 4.3s
● parsing command line arguments 0.0s
Run Django development server with maildev SMTP test server
Args:
source: Django project directory
Returns:
Service running Django development server with maildev dependency
Example:
# Start both services in parallel:
dagger call runserver --source=. up --ports 8080:8000 &
dagger call maildev up --ports 1080:1080 --ports 1025:1025 &
wait
# Or use separate terminals:
# Terminal 1: dagger call runserver --source=. up --ports 8080:8000
# Terminal 2: dagger call maildev up --ports 1080:1080 --ports 1025:1025
Access Django at: http://localhost:8080
Access Maildev Web UI at: http://localhost:1080
USAGE
dagger call runserver [arguments] <function>
FUNCTIONS
endpoint Retrieves an endpoint that clients can use to reach this container.
hostname Retrieves a hostname which can be used by clients to reach this container.
ports Retrieves the list of ports provided by the service.
start Start the service and wait for its health checks to succeed.
stop Stop the service.
up Creates a tunnel that forwards traffic from the caller's network to this service.
with-hostname Configures a hostname which can be used by clients within the session to reach this container.
ARGUMENTS
--source Directory A directory. [required]
Use "dagger call runserver [command] --help" for more information about a command.
Setup tracing at https://dagger.cloud/traces/setup. To hide set DAGGER_NO_NAG=1
up を実行すると、localhost の 8000 と 1080 にアクセスできるようになります。
% dagger call runserver --source=. up
● djangoTest: DjangoTest! 1.5s
▼ .runserver(
│ ┆ source: Host.directory(path: "/Users/m-nakamura/Documents/partner_manager_with_claudecode"): Directory!
│ ): Service! 1m12s
├─● Container.from(address: "maildev/maildev:latest"): Container! 2.6s
│
├─▼ Container.asService: Service! 1m2s
│ ): Service! 1m13s
│ ┃ MailDev webapp running at http://localhost:1080/
│ ┃ (node:14) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
│ ┆ source: Host.directory(path: "/Users/m-nakamura/Documents/partner_manager_with_claudecode"): Directory!
│ ): Service! 1m33s
├─● Container.from(address: "maildev/maildev:latest"): Container! 2.6s
│
├─▼ Container.asService: Service! 1m23s
│ ┃ MailDev using directory /tmp/maildev-14
│ ┃ MailDev webapp running at http://localhost:1080/
│ ┃ (node:14) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
│ ┃ (Use `node --trace-deprecation ...` to show where the warning was created)
│ ┃ MailDev SMTP Server running at localhost:1025
│
├─● Container.withExposedPort(port: 1025): Container! 0.0s
├─● .withExposedPort(port: 1080): Container! 0.0s
│
├─● Container.from(address: "python:3.12-slim"): Container! 1.5s
├─▶ .withExec(args: ["apt-get", "update"]): Container! 1.9s
├─▶ .withExec(args: ["apt-get", "install", "-y", "curl", "git"]): Container! 3.6s
├─● .withExec(args: ["curl", "-LsSf", "https://astral.sh/uv/install.sh", "-o", "/tmp/install-uv.sh"]): Container! 0.6s
├─● .withExec(args: ["chmod", "u+x", "/tmp/install-uv.sh"]): Container! 0.1s
├─▶ .withExec(args: ["sh", "/tmp/install-uv.sh"]): Container! 1.6s
├─● .withEnvVariable(name: "PATH", value: "/root/.local/bin:$PATH", expand: true): Container! 0.0s
├─● .withMountedDirectory(
│ ┆ path: "/app"
│ ┆ source: Host.directory(path: "/Users/m-nakamura/Documents/partner_manager_with_claudecode"): Directory!
│ ): Container! 0.0s
├─● .withWorkdir(path: "/app"): Container! 0.0s
├─● .withMountedCache(
│ ┆ path: "/root/.cache/uv"
│ ┆ cache: cacheVolume(key: "uv-cache"): CacheVolume!
│ ): Container! 0.0s
├─▶ .withExec(args: ["uv", "sync"]): Container! 4.4s
├─▶ .withExec(args: ["uv", "run", "python", "manage.py", "migrate"]): Container! 1.4s
├─▶ .withExec(args: ["uv", "run", "python", "manage.py", "loaddata", "initial_data"]): Container! 0.4s
├─● .withServiceBinding(
│ ┆ alias: "maildev"
│ ┆ service: Container.asService: Service!
│ ): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_HOST", value: "maildev"): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_PORT", value: "1025"): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_HOST_USER", value: ""): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_HOST_PASSWORD", value: ""): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_USE_TLS", value: "False"): Container! 0.0s
├─● .withEnvVariable(name: "EMAIL_USE_SSL", value: "False"): Container! 0.0s
├─● .withEnvVariable(name: "DEFAULT_FROM_EMAIL", value: "test@example.com"): Container! 0.0s
├─● .withEnvVariable(name: "SERVER_EMAIL", value: "test@example.com"): Container! 0.0s
├─● .withExposedPort(port: 8000): Container! 0.0s
├─▼ .asService(args: ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]): Service! 1m0s
│ ┃ Watching for file changes with StatReloader
│ ┃ Performing system checks...
│ ┃
│ ┃ System check identified no issues (0 silenced).
│ ┃ July 22, 2025 - 15:41:41
│ ┃ Django version 3.2.25, using settings 'partnermanager.settings'
│ ┃ Starting development server at http://0.0.0.0:8000/
│ ┃ Quit the server with CONTROL-C.
│
├─▶ proxy: Proxy! 3.3s
├─▶ .withService(
│ ┆ service: Container.asService: Service!
│ ┆ name: "maildev_service"
│ ┆ frontend: 1080
│ ┆ backend: 1080
│ ): Proxy! 0.7s
├─▶ .withService(
│ ┆ service: Container.asService(args: ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]): Service!
│ ┆ name: "django_service"
│ ┆ frontend: 8000
│ ┆ backend: 8000
│ ): Proxy! 14.6s
╰─▼ .service: Service! 55.0s
╰─▶ Container.asService(args: ["nginx", "-g", "daemon off;"]): Service! 54.4s
▼ .up: Void 1m23s
┃ 06:41:47 INF tunnel started port=1080 protocol=tcp http_url=http://localhost:1080 description="tunnel 0.0.0.0:1080 -> 4orahp2mspm3i.pjr31aula2t34.dagger.local:1080"
┃ 06:41:47 INF tunnel started port=8000 protocol=tcp http_url=http://localhost:8000 description="tunnel 0.0.0.0:8000 -> 4orahp2mspm3i.pjr31aula2t34.dagger.local:8000"
まとめ
yaml ファイル以外でコンテナ環境を構築できるのは大変面白い仕組みだな、と思いました。Container-Use で Dagger API が利用できるようになれば、より用途が広がるのではないかと思いました。
また、sourceフォルダを .with_mounted_directory() でマウントしても、ソースコード変更後の自動リロードが働きませんでした。この用途だと、素直に Dev Containers + Docker Compose で環境を作ったほうが便利だな、と感じました。