用 Docker 部署 ROS2 系統

2026-07-18
  • ros2
  • docker
  • deployment
  • devops

問題定義

前面所有章節的範例,都假設你在一台已經裝好 ROS2、裝好所有依賴套件的機器上工作。但實際把系統交付到別的機器上——無論是同事的開發機、CI 伺服器、還是最終要部署上去的機器人載板——都會遇到「這台機器上的環境設定跟我開發時不一樣」的問題:ROS2 版本不同、缺少某個系統套件、Python 套件版本衝突。Docker 把整個執行環境(作業系統基礎映像、系統套件、ROS2 安裝、你的程式碼)打包成一個可以在任何裝了 Docker 的機器上一致運行的單位,從根本上解決「在我機器上明明可以跑」的問題。

核心概念說明

分層建置:善用快取加速迭代

Docker 映像檔是由一層層疊加的檔案系統變更組成,每一行 Dockerfile 指令通常對應一層。Docker 會快取每一層的建置結果——只要某一層的輸入沒有改變,重新建置時會直接複用快取,不需要重新執行。這代表 Dockerfile 裡指令的順序很重要:把變動頻率低的操作(安裝系統套件、ROS2 基礎環境)放在前面,變動頻率高的操作(複製自己的原始碼、安裝專案依賴)放在後面,能讓日常開發迭代時,大部分建置時間都花在真正變動的那幾層,而不是每次都重新安裝一次完整的 ROS2 環境。

多階段建置:建置環境跟執行環境分開

編譯 ROS2 套件(colcon build)通常需要完整的開發工具鏈(編譯器、rosdep、各種 -dev 結尾的開發套件),但這些工具在執行階段完全用不到,只有編譯出來的結果(install/ 資料夾底下的檔案)才是實際運行需要的東西。**多階段建置(multi-stage build)**讓你在 Dockerfile 裡定義多個階段:第一個階段裝齊所有建置工具、完成編譯;最終階段只從第一個階段複製編譯完成的結果,不含任何建置工具本身。這能大幅縮小最終映像檔的體積——體積越小,傳輸、部署、啟動速度都更快,也降低了不必要的攻擊面(不需要的工具鏈也是潛在的安全風險來源,這個概念會在第 10.3 節深入)。

實作範例:把 my_package 打包成容器映像

1. 多階段 Dockerfile

檔案位置:~/ros2_ws/Dockerfile

dockerfile
# ---- 建置階段 ----
FROM ros:humble AS builder

RUN apt-get update && apt-get install -y \
    python3-colcon-common-extensions \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /ros2_ws
COPY src/ src/

RUN . /opt/ros/humble/setup.sh && \
    rosdep install --from-paths src --ignore-src -r -y && \
    colcon build --symlink-install

# ---- 執行階段 ----
FROM ros:humble-ros-base AS runtime

WORKDIR /ros2_ws
COPY --from=builder /ros2_ws/install ./install

RUN echo "source /opt/ros/humble/setup.bash" >> /root/.bashrc && \
    echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc

ENTRYPOINT ["/bin/bash", "-c", "source /opt/ros/humble/setup.bash && source /ros2_ws/install/setup.bash && ros2 run my_package talker"]

這份 Dockerfile 體現了上面提到的兩個核心概念:builder 階段用完整的 ros:humble 基礎映像(含開發工具),runtime 階段改用更精簡的 ros:humble-ros-base(不含開發工具、只有執行期需要的核心套件),並且只用 COPY --from=builder 複製編譯完成的 install 資料夾,建置階段用到的原始碼、編譯過程產生的中間檔案,完全不會出現在最終映像檔裡。

2. 建置映像檔

bash
docker build -t my_package:latest .

預期輸出

text
[+] Building 84.3s (14/14) FINISHED
 => [builder 1/5] FROM docker.io/library/ros:humble
 => [builder 4/5] RUN colcon build --symlink-install
 => [runtime 3/4] COPY --from=builder /ros2_ws/install ./install
 => exporting to image
 => naming to docker.io/library/my_package:latest

第二次建置時,如果只改動了 src/ 底下的原始碼、沒有動系統依賴,Docker 會直接複用前面幾層(安裝系統套件、rosdep install)的快取,只重新執行 colcon build 之後的步驟,建置時間會明顯縮短。

3. 執行容器,確認節點正常運作

容器間(或容器與主機之間)的 DDS 通訊,實務上最直接可靠的方式是使用 host 網路模式:

bash
docker run --rm --network host my_package:latest

另開一個終端機,在主機(不在容器裡)確認能收到容器裡節點發布的訊息:

bash
ros2 topic echo /chatter

預期輸出

text
data: 'hello ros2, count=0'
---
data: 'hello ros2, count=1'
---

主機能直接收到容器內部節點發布的訊息,證明用 --network host 讓容器共享了主機的網路堆疊,DDS 的節點探索機制能正常運作。

常見錯誤與除錯技巧

錯誤一:容器裡的節點正常啟動、log 顯示有在發布,但主機(或另一個容器)完全收不到任何訊息

原因:這幾乎都是網路模式的問題——如果容器用的是 Docker 預設的橋接(bridge)網路模式,容器有自己獨立的網路命名空間跟 IP,DDS 底層依賴的多播(multicast)探索封包,很可能沒辦法正確地在容器網路跟主機網路之間傳遞,導致雙方各自運作正常、卻完全「看不到」彼此。

排除方式:優先確認是否可以直接使用 --network host(如本文範例),這是最簡單直接的解法,讓容器完全共享主機的網路堆疊;如果因為安全或架構考量無法使用 host 網路模式(例如需要同時運行多個容器、彼此網路要隔離),則需要進一步研究 DDS 廠商提供的路由或探索伺服器機制,這部分屬於更進階的網路架構設計,超出這篇文章的範圍。

錯誤二:建置映像檔時,colcon build 找不到某個依賴套件

text
CMake Error: Could not find a package configuration file provided by "some_dependency"

原因:常見是 Dockerfile 裡漏掉了 rosdep install 這個步驟,或是 package.xml 裡沒有正確宣告所有實際用到的依賴(在本機開發環境裡,這個依賴可能剛好因為裝過其他套件而順帶存在,掩蓋了 package.xml 宣告不完整的問題,直到換一個乾淨環境建置才會暴露出來)。

排除方式:確認 Dockerfile 裡有執行 rosdep install --from-paths src --ignore-src -r -y(如範例所示),這一步會讀取 package.xml 裡宣告的所有依賴並自動安裝;如果加了這一步仍然找不到,回頭檢查 package.xml 是否確實完整宣告了所有依賴,容器化過程本身其實是驗證依賴宣告是否完整的一個很好的機會——這正是 Docker 帶來的「環境一致性」價值的具體體現。

小結

Docker 分層建置搭配合理的指令順序,能讓日常開發迭代的建置時間維持在可接受範圍;多階段建置把編譯期跟執行期的環境分開,大幅縮小最終部署用的映像檔體積。容器化部署 ROS2 系統時,DDS 通訊的網路模式是最容易踩坑的環節,--network host 是最直接的起手式。下一節要介紹另一類完全不同的部署場景:資源極度受限、需要嚴格即時性保證的微控制器,這時候完整的 ROS2 就不適用了,需要 micro-ROS。

延伸閱讀

常見問題

faq_01.log
容器化之後,還需要在意 ROS_DOMAIN_ID 嗎?
需要,而且更容易出狀況。多個容器如果用預設的橋接網路模式,各自有獨立的網路命名空間,DDS 的節點探索機制在這種環境下可能無法正常運作(尤其是依賴多播的探索封包,經過容器網路轉譯後可能被過濾掉)。實務上容器間的 ROS2 通訊常見的解法是使用 host 網路模式讓容器共享主機的網路堆疊,或搭配額外的 DDS 路由設定,這在本文的常見錯誤小節會提到。
faq_02.log
每次程式碼小改動,都要重新建置整個 Docker 映像檔嗎?
如果 Dockerfile 的分層順序設計得當,不需要。Docker 會快取每一層建置結果,只有內容真正改變的那一層(以及之後所有層)才需要重新建置。把不常變動的部分(安裝系統依賴、ROS2 基礎套件)放在前面的層、把常變動的部分(自己的原始碼)放在後面的層,能讓大部分開發迭代只需要重新建置最後幾層,大幅縮短建置時間。
faq_03.log
在容器裡跑 Gazebo 這種需要圖形介面的模擬,可行嗎?
可行,但需要額外設定讓容器內的圖形程式能把畫面輸出到主機的顯示裝置(常見做法是掛載 X11 socket、設定對應的環境變數),設定細節依作業系統與顯示伺服器而異。純粹跑 ROS2 節點邏輯(不需要圖形介面)的容器化相對單純很多,這也是為什麼實務上常把『需要圖形介面的開發環境』跟『純邏輯運算的正式部署映像檔』分開處理,不強求同一套容器設定要兩者兼顧。