0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dagger を使って、Docker Compose のように複数サービスのポートをホストに公開する

Posted at

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 | DaggerCookbook | 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 で環境を作ったほうが便利だな、と感じました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?