概要
コンテナとは、アプリケーションとその依存ライブラリ・設定ファイルをひとまとめにして、ホストOSとは隔離された環境で動かす技術だ。仮想マシン(VM)がハードウェアを仮想化するのとは異なり、コンテナはホストOSのカーネルを共有しながら、ユーザー空間だけを分離する。これにより、VMと比べてオーバーヘッドが少なく起動が速い。
Dockerは2013年に登場したコンテナランタイムで、コンテナ技術を一般に普及させたツールだ。Linuxカーネルのnamespace(プロセス・ネットワーク・ファイルシステムの隔離)とcgroups(CPU・メモリ等のリソース制限)を組み合わせることでコンテナを実現する。
組み込み・エッジコンピューティングの分野でも近年コンテナの採用が広がっている。Raspberry Pi、NVIDIA Jetson、SOMを搭載した産業用ゲートウェイなど、Linuxが動く組み込みデバイスであればDockerが動作し、クラウドで開発・テストしたアプリをそのままエッジに展開できる。YoctoやBuildrootでフルカスタムのOSイメージを作る代わりに、ベースLinuxをシンプルに保ちアプリをコンテナで管理するアプローチが現実的な選択肢として定着してきた。
歴史・背景
コンテナの基礎技術はLinuxカーネルで段階的に整備された。2002年にnamespaceの一部(mount namespace)が、2006〜2008年にcgroupsが追加された。2013年にDockerがこれらを統合した使いやすいツールとして登場し、コンテナが爆発的に普及した。
2015年にはOpen Container Initiative(OCI)が設立され、コンテナイメージフォーマット(OCI Image Spec)とランタイム仕様(OCI Runtime Spec)が標準化された。これによりDockerだけでなくPodman、containerd、runCなど複数のランタイムが相互運用できる基盤が整った。
組み込み分野では、Balena(旧Resin.io)が2014年にDockerベースのエッジデバイス管理プラットフォームとして登場し、ARM向けコンテナのOTA(Over The Air)アップデートを実現した。その後、AWS IoT Greengrass v2、Azure IoT Edge、Google Cloud IoT Coreなど主要クラウドのエッジランタイムがいずれもコンテナベースのデプロイを採用している。
技術仕様
コンテナの構成要素
| コンポーネント | 役割 | Linuxカーネル機能 |
|---|---|---|
| プロセス隔離 | 各コンテナが独立したPIDを持つ | PID namespace |
| ネットワーク隔離 | 独立したネットワークスタック | Net namespace |
| ファイルシステム隔離 | コンテナ専用のルートファイルシステム | Mount namespace + overlayfs |
| ユーザー隔離 | コンテナ内のroot≠ホストのroot | User namespace |
| リソース制限 | CPU・メモリ・I/Oの上限設定 | cgroups v1/v2 |
| セキュリティ | システムコールの制限 | seccomp, AppArmor/SELinux |
コンテナイメージの階層構造
コンテナイメージはUnionFS(overlayfsなど)を使った読み取り専用レイヤーの積み重ねで構成される。
┌──────────────────────────┐ ← 書き込み可能レイヤー(コンテナ実行時)
├──────────────────────────┤ ← アプリレイヤー(COPY命令等)
├──────────────────────────┤ ← 依存ライブラリレイヤー(RUN apt install等)
├──────────────────────────┤ ← ベースイメージ(FROM ubuntu:22.04等)
└──────────────────────────┘ ← ホストOSのカーネル(共有)
各レイヤーはSHA256ハッシュで管理されており、同じレイヤーを複数のイメージで共有できる。これによりストレージ効率が高くなる。
ARM向けのマルチアーキテクチャ対応
コンテナイメージはCPUアーキテクチャに依存する。組み込みLinux環境(ARM32/ARM64)向けには対応アーキテクチャのイメージが必要だ。
# 利用可能なアーキテクチャ確認
docker buildx ls
# ARM64向けイメージのビルド
docker buildx build --platform linux/arm64 -t myapp:arm64 .
# マルチアーキテクチャイメージ(arm64/amd64両対応)のビルドとプッシュ
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t myregistry/myapp:latest \
--push .
# 実行環境のアーキテクチャ確認
uname -m # aarch64 = ARM64, armv7l = ARM32, x86_64 = x86-64
Dockerfileの基本(組み込みLinux向け)
# ARM64 Ubuntu 22.04をベースとした組み込みアプリ用コンテナ
FROM arm64v8/ubuntu:22.04
# 不要なパッケージをインストールしないようにする
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
libgpiod2 \
libi2c-dev \
&& rm -rf /var/lib/apt/lists/*
# アプリケーションバイナリをコピー
WORKDIR /app
COPY ./sensor_daemon /app/sensor_daemon
COPY ./config.json /app/config.json
# 最小権限で実行(rootではない)
RUN useradd -m -u 1000 appuser
USER appuser
# ヘルスチェック(コンテナが正常動作しているか確認)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD /app/sensor_daemon --health-check || exit 1
ENTRYPOINT ["/app/sensor_daemon"]
docker-compose(マルチコンテナ構成)
エッジデバイスでよく使われる複数コンテナの構成例。
# docker-compose.yml(産業用IoTゲートウェイの例)
version: '3.8'
services:
# センサーデータ収集コンテナ(デバイスアクセス権が必要)
data-collector:
image: myregistry/data-collector:arm64-latest
privileged: false
devices:
- /dev/i2c-1:/dev/i2c-1 # I2Cバスへのアクセス
- /dev/ttyUSB0:/dev/ttyUSB0 # UARTへのアクセス
group_add:
- dialout # UARTアクセス用グループ
volumes:
- sensor-data:/data
restart: always
networks:
- internal
# データ処理・MQTT送信コンテナ
mqtt-publisher:
image: myregistry/mqtt-publisher:arm64-latest
depends_on:
- data-collector
environment:
- MQTT_BROKER=mqtt://broker.example.com:1883
- DEVICE_ID=${DEVICE_ID}
volumes:
- sensor-data:/data:ro # 読み取り専用マウント
restart: always
networks:
- internal
- external
# ローカルウェブUI(設定・監視)
webui:
image: myregistry/webui:arm64-latest
ports:
- "8080:8080"
restart: always
networks:
- external
volumes:
sensor-data:
networks:
internal: # コンテナ間通信
external: # 外部ネットワーク
driver: bridge
動作原理
namespace(名前空間)による隔離の詳細
Linuxのnamespaceはカーネル内でリソースの「見え方」を分離する仕組みだ。
# namespaceの確認(コンテナ内とホストで比較)
# ホスト側
ls -la /proc/self/ns/
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 net -> 'net:[4026531992]'
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 pid -> 'pid:[4026531836]'
# コンテナ内(異なるinode番号→別のnamespace)
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 net -> 'net:[4026532456]'
# lrwxrwxrwx 1 root root 0 Jun 14 00:00 pid -> 'pid:[4026532399]'
cgroups(コントロールグループ)によるリソース制限
# コンテナのCPU・メモリ制限(docker runオプション)
docker run \
--cpus="0.5" \ # CPUを0.5コア分に制限
--memory="256m" \ # メモリを256MBに制限
--memory-swap="256m" \ # スワップなし(組み込みではスワップ不使用が多い)
--restart=always \ # 自動再起動
myapp:arm64
# cgroupsの実際の設定ファイル(cgroups v2)
# /sys/fs/cgroup/docker/<container_id>/memory.max
# /sys/fs/cgroup/docker/<container_id>/cpu.max
コンテナとデバイスアクセス
組み込みLinuxでは、GPIOや各種バスデバイスへのアクセスが必要になる。
# GPIOへのアクセス(gpiodを使う方法)
docker run \
--device /dev/gpiochip0 \ # GPIOデバイスファイル
--group-add gpio \ # gpioグループに追加
myapp:arm64
# SPI/I2Cデバイスへのアクセス
docker run \
--device /dev/spidev0.0 \
--device /dev/i2c-1 \
myapp:arm64
# privilegedモード(全デバイスにアクセス可能、セキュリティ上非推奨)
docker run --privileged myapp:arm64
# コンテナ内からI2Cデバイスにアクセスする例(Python)
import smbus2
bus = smbus2.SMBus(1) # /dev/i2c-1
BME280_ADDR = 0x76
# 温度データを読み取る
temp_raw = bus.read_word_data(BME280_ADDR, 0xFA)
print(f"Raw temperature: {temp_raw}")
用途・ユースケース
エッジAIアプリケーション
NVIDIA Jetsonや高性能SBCでのAI推論にコンテナが活用される。CUDAやTensorRTのライブラリ依存関係をコンテナにカプセル化することで、デプロイが大幅に簡素化される。
# NVIDIA Jetson向けの公式コンテナを使う例
# JetPackバージョンに合わせたコンテナが提供されている
docker run --runtime nvidia \
nvcr.io/nvidia/l4t-pytorch:r35.3.1-pth2.0-py3 \
python3 -c "import torch; print(torch.cuda.is_available())"
OTA(Over The Air)アップデート
Dockerイメージとしてアプリを管理すると、コンテナイメージのプルだけでアップデートが完結する。BalenaやAWS IoT Greengrassはこの仕組みを使ってエッジデバイスの遠隔管理を実現している。
# Balena CLIでのデプロイ例
balena push myfleet
# デバイス側では自動的に新しいコンテナイメージがプルされ起動される
# ロールバックも容易(古いイメージのタグを指定して戻す)
開発環境とエッジのパリティ
開発用PCのコンテナと本番エッジデバイスのコンテナが同じイメージを使うため「自分のPCでは動く」問題が解消される。クロスプラットフォームのマルチアーキテクチャビルドにより、x86_64の開発機でビルドしてarm64のデバイスにデプロイできる。
マイクロサービス分離
IoTゲートウェイで複数のプロトコル変換(Modbus→MQTT、CAN→HTTP等)を行う場合、機能ごとにコンテナを分割することで、個々の機能を独立してアップデート・再起動できる。
実装・開発のポイント
組み込みLinuxへのDockerインストール
# Raspberry Pi OS / Ubuntu (arm64) へのDockerインストール
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# armv7l(32ビットARM)の場合はraspbian向けの手順を確認
# Yoctoでのコンテナ対応:meta-virtualizationレイヤーを追加
イメージサイズの最小化
組み込みデバイスはストレージ容量・ネットワーク帯域が限られる。マルチステージビルドでイメージを小型化する。
# マルチステージビルドの例(C言語アプリケーション)
# ステージ1: ビルド環境(大きなイメージ)
FROM arm64v8/gcc:12 AS builder
WORKDIR /src
COPY . .
RUN gcc -O2 -o sensor_app main.c -lm
# ステージ2: 実行環境(最小イメージ)
FROM arm64v8/debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libgpiod2 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /src/sensor_app /usr/local/bin/sensor_app
# さらに小さくする場合はscratchやdistrolessを使う
# FROM gcr.io/distroless/base-debian12:arm64
ENTRYPOINT ["sensor_app"]
ヘルスチェックと自動復旧
# docker-compose.ymlでのヘルスチェックと再起動ポリシー
services:
sensor-app:
image: myapp:arm64
healthcheck:
test: ["CMD", "/usr/local/bin/sensor_app", "--health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
restart: unless-stopped # 手動停止以外は自動再起動
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
ストレージの永続化
コンテナは停止するとファイルシステムの変更が失われる(デフォルト)。設定ファイルや収集データはボリュームに永続化する。
# ボリュームによる永続化
docker volume create sensor-data
docker run -v sensor-data:/data myapp:arm64
# ホストディレクトリのバインドマウント(設定ファイル用)
docker run -v /etc/myapp/config.json:/app/config.json:ro myapp:arm64
# tmpfsマウント(再起動で消えるメモリ上のファイルシステム、一時データ用)
docker run --tmpfs /tmp:rw,noexec,nosuid,size=64m myapp:arm64
他技術との比較
| 比較項目 | Docker(コンテナ) | 仮想マシン(VM) | ベアOSへの直接インストール | Yocto/Buildroot |
|---|---|---|---|---|
| 起動時間 | 秒以内 | 数十秒〜分 | 瞬時(プロセス起動) | 秒〜分(OS起動) |
| オーバーヘッド | 小(namespaceのみ) | 大(ハイパーバイザー) | なし | なし |
| 隔離レベル | プロセスレベル | OSレベル | なし | なし |
| ポータビリティ | 高い | 高い | 低い | 低い |
| ストレージ消費 | 中(レイヤー共有) | 大 | 小 | 最小 |
| セキュリティ | 中(カーネル共有) | 高い | 設計依存 | 設計依存 |
| リアルタイム性 | 制限あり | 制限あり | 最良 | 良い |
| 組み込み向け実績 | 増加中 | 少ない | 主流 | 主流 |
Yocto・Buildrootとの使い分け
YoctoやBuildrootはOSイメージ全体をカスタムビルドし、必要なパッケージだけを含んだ最小構成のLinuxを作れる。起動時間・メモリ・ストレージの最適化が極限まで可能だ。一方コンテナは既存のLinuxディストリビューション上で動作し、開発効率・デプロイ柔軟性が高い。最近はYoctoベースのホストOSにDockerを乗せてアプリをコンテナで管理するハイブリッド構成も普及している。
Embedded Linuxの要件
コンテナはLinuxカーネルのnamespaceとcgroupsに依存するため、RTOSには適用できない。FreeRTOSやZephyrなどのRTOSベースのシステムではコンテナは使えず、Linuxが動くSBCやSOMが前提となる。