BLOG

>

docker-composeでTROCCOのSelf-Hosted Runnerを動かす構成例

はじめに

TROCCOのSelf-Hosted Runnerは、TROCCOのデータ転送をユーザー管理の処理環境で実行できる機能です。SaaSとしてのTROCCOの使いやすさを保ちつつ、実データを閉域内に留めることでセキュアなデータ転送を実現することができます。

Docker(Linux環境)があれば利用できるので、クラウドのContainer as a Serviceを使うことも多くあるでしょうが、要件によってはサーバ、VMでの利用も想定されます。今回は、シンプルな構成としてdocker-composeの構成例をご紹介します。

こんな方におすすめ

  • docker-composeを利用したTROCCO Self-Hosted Runnerの運用を検討したい方

Windows PCのDocker Desktop(WSL2)が利用可能な環境で検証していますが、Linux環境のDockerが使える環境であれば同じように利用できると思われます
Docker Engineおよびdocker-composeが利用可能であることが前提です

本記事で紹介する構成のサンプルコードはGitHubで公開しています。実際に動かしてみたい場合はご利用ください。

docker-composeとは

docker-composeとは、複数のコンテナで構成されるアプリケーションを YAML ファイル(docker-compose.yaml) に定義し、まとめて起動・停止・管理できるツールです。

Self-Hosted Runnerはdocker runで個別のコンテナを立てることでも利用できますが、docker-composeを利用することでコンテナの停止時に自動で再起動したり、リソースの違うRunnerをまとめて立てたり、Runnerのログをファイルに書き出ししたりなどすることができます。

docker-compose.yamlファイルが存在するディレクトリをカレントディレクトリとして、

  • docker-compose up -dでコンテナを起動
  • docker-compose down -vでコンテナを停止

できます。

docker-composeの構成例

それほど量は多くないので、まずコードをまとめて掲載します。ディレクトリ構成は以下の通りです。

.
├── .env.example
├── .gitignore
├── daily_rotate.lua
├── docker-compose.yaml
├── fluent-bit.conf
├── fluent-bit-json-parser.conf
├── Makefile
└── README.md

.env.example

docker-compose.yaml で参照する環境変数のテンプレートです。利用時はこのファイルを .env にコピーし、各値を実値に置き換えてください。Runnerを2台構成にしているため、Registration TokenはRunnerごとに別々の値を指定できます(同じ値でも問題ありません)。

TROCCO_IMAGE_URL=<TROCCO_SELF_HOSTED_RUNNER_IMAGE_URL>
TROCCO_REGISTRATION_TOKEN_NORMAL=<YOUR_REGISTRATION_TOKEN_NORMAL>
TROCCO_REGISTRATION_TOKEN_ENHANCED=<YOUR_REGISTRATION_TOKEN_ENHANCED>

docker-compose.yaml

インバウンド通信を受け付けない、メモリが異なるRunnerを2台と、Runnerのログをファイルに書き出すためのfluent-bitのコンテナ、古いログファイルを自動削除するlog-cleanerのコンテナを設定します。

services:
  trocco-self-hosted-runner-normal:
    image: ${TROCCO_IMAGE_URL}:latest
    mem_limit: 2G
    cpus: 2
    restart: always
    environment:
      - TROCCO_REGISTRATION_TOKEN=${TROCCO_REGISTRATION_TOKEN_NORMAL}
      - TROCCO_PREVIEW_SEND=true
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "127.0.0.1:24224"
        tag: "runner.log"
        fluentd-async: "true"
        labels: "com.docker.compose.service"
    networks:
      - egress
    depends_on:
      - fluent-bit

  trocco-self-hosted-runner-enhanced:
    image: ${TROCCO_IMAGE_URL}:latest
    mem_limit: 4G
    cpus: 2
    restart: always
    environment:
      - TROCCO_REGISTRATION_TOKEN=${TROCCO_REGISTRATION_TOKEN_ENHANCED}
      - TROCCO_PREVIEW_SEND=true
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "127.0.0.1:24224"
        tag: "runner.log"
        fluentd-async: "true"
        labels: "com.docker.compose.service"
    networks:
      - egress
    depends_on:
      - fluent-bit

  fluent-bit:
    image: fluent/fluent-bit:latest
    volumes:
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
      - ./fluent-bit-json-parser.conf:/fluent-bit/etc/fluent-bit-json-parser.conf
      - ./daily_rotate.lua:/fluent-bit/etc/daily_rotate.lua
      - ./log:/fluent-bit/log
    restart: always
    ports:
      - "127.0.0.1:24224:24224"
    logging:
      driver: "local"
      options:
        max-size: "50m"
        max-file: "10"
    networks:
      - log_ingest

  log-cleaner:
    image: alpine:latest
    volumes:
      - ./log:/log
    entrypoint: ["/bin/sh", "-c"]
    command: ["while true; do find /log -name '*.jsonl' -mtime +30 -delete; sleep 86400; done"]
    restart: always
    network_mode: none
    logging:
      driver: "local"
      options:
        max-size: "1m"
        max-file: "3"

networks:
  egress:
    driver: bridge
  log_ingest:
    driver: bridge

fluent-bit.conf

fluent-bitのパイプラインを記載します。Dockerの fluentd ログドライバから受け取ったログに対し、JSON のフラット化・日付別のファイル分割・メタ情報のネストなどを行い、Runnerごとに別ディレクトリへ書き出します。

# Dockerのfluentdログドライバーからforwardプロトコルでログを受信
[INPUT]
    Name        forward
    Listen      0.0.0.0
    Port        24224

# カスタムパーサー定義の読み込み
[SERVICE]
    Parsers_File /fluent-bit/etc/fluent-bit-json-parser.conf

# 空文字のログ(stderrの空行)を除外
[FILTER]
    Name    grep
    Match   runner.log
    Exclude log ^$

# logフィールド内のJSON文字列をパースしてフラットなJSONLにする
[FILTER]
    Name         parser
    Match        runner.log
    Key_Name     log
    Parser       docker_json
    Reserve_Data On

# com.docker.compose.service ラベルから新しいタグ文字列 new_tag (= <service>/<date>.jsonl) を組み立てる
[FILTER]
    Name        lua
    Match       runner.log
    script      /fluent-bit/etc/daily_rotate.lua
    call        daily_rotate

# new_tag フィールドの値をそのまま新しいタグにする (タグ内の '/' は出力時にサブディレクトリになる)
[FILTER]
    Name          rewrite_tag
    Match         runner.log
    Rule          $new_tag ^(.+)$ $1 false
    Emitter_Name  date_rewriter

# タグ書き換え用の一時フィールドと、出力ファイルに残したくないラベル参照を除去
[FILTER]
    Name          modify
    Match         */*.jsonl
    Remove        new_tag
    Remove        com.docker.compose.service

# container_id, container_name, sourceをmetaにまとめる
[FILTER]
    Name          nest
    Match         */*.jsonl
    Operation     nest
    Wildcard      container_id
    Wildcard      container_name
    Wildcard      source
    Nest_under    meta

# Path = /fluent-bit/log にタグをそのままパスとして連結
# 例: タグが trocco-self-hosted-runner-normal/20260424.jsonl なら /fluent-bit/log/trocco-self-hosted-runner-normal/20260424.jsonl に出力
[OUTPUT]
    Name        file
    Match       */*.jsonl
    Path        /fluent-bit/log
    Mkdir       On
    Format      plain

fluent-bit-json-parser.conf

fluent-bit.conf の [FILTER] parser から参照されるパーサー定義です。Runnerが標準出力に書き出すJSONログを構造化データとして読み取るために利用します。

[PARSER]
    Name        docker_json
    Format      json

daily_rotate.lua

fluent-bit.conf の [FILTER] lua から呼ばれるLuaスクリプトです。com.docker.compose.service ラベル(= Docker Composeのservice名)とレコードのタイムスタンプから <service>/YYYYMMDD.jsonl 形式の完成済みタグ文字列を生成し、new_tag フィールドとして付加します。後続の rewrite_tag がこの値を新しいタグに採用することで、ログがservice別・日付別のファイルに書き出されます。

function daily_rotate(tag, timestamp, record)
    -- docker-composeのservices名は com.docker.compose.service ラベル経由でレコードに乗ってくる
    local service_name = record["com.docker.compose.service"] or ""
    local date_str = os.date("!%Y%m%d", timestamp)
    -- fluent-bit 側の rewrite_tag では $1 だけで使うので、ここで完成形のタグを作る
    record["new_tag"] = service_name .. "/" .. date_str .. ".jsonl"
    return 1, timestamp, record
end

ポイントの解説

上記のコードに関して、ポイントは以下の通りです。

Runnerの設定

以下のシンプルなコードでRunnerの設定を定義できます。メモリやCPUは実行するワークロードに応じて調整してください。

また、転送の内容によっては強めのリソースが必要になるかもしれませんが、その場合はSelf-Hosted Runner Clusterを分けて、それに対応したRegistration TokenをRunnerに登録することで、転送設定ごとのRunnerの使い分けができます。

  trocco-self-hosted-runner-normal:
    image: ${TROCCO_IMAGE_URL}:latest
    mem_limit: 2G
    cpus: 2
    restart: always
    environment:
      - TROCCO_REGISTRATION_TOKEN=${TROCCO_REGISTRATION_TOKEN_NORMAL}
      - TROCCO_PREVIEW_SEND=true
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "127.0.0.1:24224"
        tag: "runner.log"
        fluentd-async: "true"
        labels: "com.docker.compose.service"
    networks:
      - egress
    depends_on:
      - fluent-bit

tag はfluent-bit側のパイプラインを起動するための識別子で、複数Runner間で共通の runner.log を指定しています。labels: "com.docker.compose.service" を指定することで、Docker Composeが自動で各コンテナに付与する com.docker.compose.service ラベル (= サービス名) がログレコードのフィールドとして送出され、fluent-bit側で どのRunnerからのログかを判別する のに使えます。

ログの連携

コンテナの稼働の確認や、何かエラーが起きたときのデバッグをするために、ログを適切に取り扱うことは重要です。今回はコンテナでfluent-bitを動かし、Runnerのログをそちらに向けて、ファイルとしてエクスポートするようにしています。

  fluent-bit:
    image: fluent/fluent-bit:latest
    volumes:
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
      - ./fluent-bit-json-parser.conf:/fluent-bit/etc/fluent-bit-json-parser.conf
      - ./daily_rotate.lua:/fluent-bit/etc/daily_rotate.lua
      - ./log:/fluent-bit/log
    restart: always
    ports:
      - "127.0.0.1:24224:24224"
    logging:
      driver: "local"
      options:
        max-size: "50m"
        max-file: "10"
    networks:
      - log_ingest

今の設定のままで、以下のような形でログがローカルで取扱えるようになります。1行1イベントのJSON Linesで、Runnerが出力したフィールド(msg / level / time など)はトップレベルに展開され、Docker由来のメタ情報(container_id / container_name / source)は meta.* にまとめられます。

.
├── log
│   ├── trocco-self-hosted-runner-enhanced
│   │   └── 20260424.jsonl
│   └── trocco-self-hosted-runner-normal
│       └── 20260424.jsonl
├── .env
├── .gitignore
├── daily_rotate.lua
├── docker-compose.yaml
├── fluent-bit-json-parser.conf
├── fluent-bit.conf
├── Makefile
└── README.md

なお、ログファイルを無制限に保持すると容量が膨らんでいくため、今回の構成では log-cleaner という補助コンテナを動かし、30日以上経過した .jsonl を1日1回自動削除しています。保持期間を変えたい場合は docker-compose.yaml の find ... -mtime +30 を調整してください。

  log-cleaner:
    image: alpine:latest
    volumes:
      - ./log:/log
    entrypoint: ["/bin/sh", "-c"]
    command: ["while true; do find /log -name '*.jsonl' -mtime +30 -delete; sleep 86400; done"]
    restart: always
    network_mode: none

この構成はあくまでサンプルなので、実運用の際には要件に合わせてログの設計をしてください。ローカルで保持する場合は、

  • 保持したファイルをどのように保管/利用するのか
  • ファイルの保存期間をどうするか

などが検討観点になります。また、そもそもローカルで保持するのかというのも論点です。クラウドへの連携もできるので、運用を考えるとそちらの方を取ることも有力な選択肢になるでしょう(fluent-bitの場合は fluent-bit.conf の [OUTPUT] を差し替えることでS3等への転送に切り替えられます)。

ちなみに、TROCCOのジョブという意味ではSaaSのTROCCOにもログは連携されていきますので、最低限そちらで確認する形でも大丈夫です。ただし、その場合はコンテナ側に問題があるときのログが確認できないことにはご留意ください。

イメージバージョンの調整

現在使われているバージョンを確認したい場合は、Self-Hosted Runnerのクラスター一覧から該当のクラスターを選択して表示してください。

過去にlatestタグで利用したことがある場合は、利用時の最新イメージがlatestタグに対応するものとして保持されています。その後に更新された新しいバージョンを利用したい場合は、新しいイメージを再度pullする必要があるので、以下のいずれかの対応をしてください。

  • docker-compose up -d --pull alwaysコマンドを実行する
  • 以下のコマンドを実行する
    • docker-compose down
    • docker image list
    • docker image rm <上記の一覧に出てくるID>

また、特定のバージョンを実行したい場合は、docker-compose.yamlで指定するタグを:latestではなく、:vX.X.Xの形で直接指定することもできます。

今回やっていないこと

上記とは別の観点として、

  • docker-composeのプロセス自体の死活監視
  • 稼働環境としてのサーバ/VMの監視
  • サーバ/VMでのネットワークの監視

といったことも、場合によっては必要になってきます。コンテナ自体の管理と、コンテナの外部環境の管理の両側面から、要件に合わせて必要な対応をしてください。

おわりに

構成自体はシンプルなものですが、Runnerの仕組み自体がインバウンド通信を受けずにRunner主導で動くもののため、十分に稼働するものになります。コンテナ自体の起動/稼働数の変更/停止などを柔軟にやろうとするとクラウドサービスが望ましいかもしれませんが、構成の一つとしてご活用ください。

目次

その他のブログ情報

他にも様々なブログ情報を公開しておりますので是非ご覧ください