容器化技術 ch01 為什麼會有容器:從「在我電腦上可以跑」說起
下一章→
CH 01 入門

為什麼會有容器:從「在我電腦上可以跑」說起

環境地獄的真正成因前容器時代的三種解法容器的一句話定義你打的 compose up 到底做了什麼

你已經會「用」容器了——docker compose up 一下,一顆 SQL Server 就活過來。這門課要補的是中間那段黑箱:它到底做了什麼、為什麼做得到、你要怎麼把自己的程式也變成那樣一個東西。

第一章不碰任何指令,先回答最根本的問題:容器是為了解決什麼痛苦而誕生的?理解了這個「為什麼」,後面每一個看起來很怪的設計(為什麼容器刪掉資料就沒了、為什麼要做 port mapping、為什麼 image 要分層)才會變成理所當然,而不是死記硬背的規則。

幾乎每個工程師都遇過:同一份程式碼,在自己筆電上跑得好好的,交給同事或丟上測試機就爆炸。要理解容器,得先看清楚這件事為什麼會發生。

關鍵觀念:程式從來不是「單獨」執行的

你以為你交付的是一個 app.jar,但那個 jar 檔要跑起來,其實踩在一整疊「它假設一定存在」的東西上面:

  • 執行環境版本:JDK 17 還是 JDK 8?你本機是 17,測試機是 8,直接 UnsupportedClassVersionError。
  • 作業系統與系統函式庫:Windows 還是 Linux?Linux 的 glibc 版本對不對?很多套件(影像處理、加解密)底層是 C 寫的,會直接呼叫系統函式庫。
  • 環境變數與設定檔:你本機 application.yml 指向 localhost 的資料庫,測試機沒有那個資料庫。
  • 檔案路徑:你寫 C:\temp\upload,Linux 機器上根本沒有 C 槽。
  • 時區與語系:本機 UTC+8,伺服器 UTC,日期一整個差 8 小時;語系不同導致中文變亂碼。
  • 相依的外部服務版本:你本機接 SQL Server 2022,正式環境是 2019,某個語法不支援。
  • Port 佔用狀況:本機 8080 是空的,那台機器上已經有別的服務佔住了。
💡 食譜與廚房的類比:你的程式碼是食譜,環境是廚房。你把食譜寄給朋友,他卻做不出同樣的菜——因為他家的烤箱溫度不準、沒有那個牌子的鍋、瓦斯火力不同。問題從來不在食譜,在廚房。

所以真正的問題是:我們一直只交付食譜,卻假設每個人的廚房都一樣。容器的答案很暴力也很有效——那就把廚房一起寄過去。

「把廚房一起交付」不是新想法,容器只是最後的答案。看過前面三代的失敗,才知道容器的每個設計在補什麼洞。

解法一:寫一份安裝文件(Word / Confluence)

  • 做法:把「裝 JDK 17 → 裝 SQL Server → 改這個設定檔 → 開這個 port」寫成 30 步驟文件,交給對方照做。
  • 死因:① 人一定會漏步驟或看錯;② 文件永遠比現實舊——有人手動改了某台機器卻忘了更新文件;③ 同樣步驟在不同 OS 版本上結果不同。

解法二:寫自動化腳本(Shell Script / Ansible)

  • 做法:把那 30 步驟寫成腳本,執行就好,不靠人手。
  • 死因:腳本依賴機器的初始狀態。在一台全新乾淨的機器上跑,成功;在一台已經裝過別的東西的機器上跑,可能因為套件版本衝突而失敗。同一份腳本、不同機器、不同結果——這叫做不可重現。

解法三:整台虛擬機(VM)打包交付

  • 做法:把裝好裝滿的整台虛擬機做成映像檔,誰要用就複製一份開起來。(VM 是什麼見 基礎篇 ch05 虛擬化與容器。)
  • 這招真的解決了環境問題——連作業系統都一起帶走,不可能不一致。
  • 死因是「重」:① 一個 VM 映像動輒 10~40 GB,傳輸和儲存成本高;② 開機要跑完整開機流程,分鐘級;③ 每台 VM 都要獨占好幾 GB 記憶體來養一整套作業系統,一台筆電開不了幾台;④ 你只是想跑一個 50 MB 的 Java 程式,卻要為它搬一整套 Ubuntu。
⭐ 需求被逼出來了:我們想要 VM 的「整包帶走、環境一致」,但不要 VM 的「整包重量」。容器就是這個取捨的答案。

先給定義,再拆解:

容器 = 把「你的程式 + 它需要的整個使用者空間檔案」打包起來,然後共用宿主機的作業系統核心來執行。

拆解一:「使用者空間」和「核心」是什麼?

一台 Linux 電腦的軟體其實分成兩層:

  • 核心(Kernel):作業系統最底層那一小塊,直接管硬體——分配記憶體、排程 CPU、讀寫磁碟、收發網路封包。全機器只有一份。
  • 使用者空間(User Space):核心之上的所有東西——你的程式、JDK、各種函式庫(libssl、glibc)、設定檔、/usr/bin 底下的指令工具。「Ubuntu 和 CentOS 的差別」幾乎都在這一層,不在核心。

關鍵洞見來了:你的程式真正需要的,其實只是「一堆使用者空間的檔案」+「一個能執行它的核心」。既然同一台機器上的核心大家都能用,那只要打包使用者空間就夠了——這正是容器做的事。

拆解二:搬家類比

  • VM = 連地基、水管、電線一起挖走的整棟房子。搬過去絕對能住,但巨大、笨重、搬一次要好幾小時。
  • 容器 = 只打包你房間裡的所有家當(家具、衣服、書),到新地方接當地的水電(宿主機核心)就能住。輕、快、一間房子塞得下很多組。

拆解三:所以你得到什麼?

  • 體積小:映像檔幾十 MB 到幾百 MB(不含核心),而不是幾十 GB。
  • 啟動快:容器不需要「開機」——它本質上只是在宿主機上啟動一個 process(下一章會把這句話講透)。所以是毫秒到秒級,不是分鐘級。
  • 密度高:一台機器同時跑幾十個容器很正常。
🚨 最常見的誤解,現在就矯正:你在容器裡下 cat /etc/os-release 會看到 Ubuntu,於是以為「容器裡裝了一個 Ubuntu」。不對。那裡面只有 Ubuntu 的使用者空間檔案(指令工具、函式庫),沒有 Ubuntu 的核心。它借用的是你宿主機的核心。這也解釋了一個常見疑問:為什麼 Linux 容器不能直接在 Windows 核心上跑?——因為它要的是 Linux 核心。(Docker Desktop 的作法是偷偷幫你開一台輕量 Linux 虛擬機來提供核心。)

你現在的用法大概是這樣:寫個 docker-compose.yml,打一句指令,SQL Server 就活了。

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: 'Y'
      MSSQL_SA_PASSWORD: '請放在 .env,不要寫死在這裡'
    ports:
      - '1433:1433'
    volumes:
      - mssql-data:/var/opt/mssql

volumes:
  mssql-data:

你按下 docker compose up 的那幾秒鐘,底下依序發生了六件事。這六件事正好就是這門課的地圖——每一件都有專章拆解:

  1. 解析設定、算出要哪個映像:把 mcr.microsoft.com/mssql/server:2022-latest 這串座標翻譯成「哪個倉庫、哪個映像、哪個版本」。→ ch04
  2. 檢查本機有沒有,沒有就下載:而且是一層一層下載(你應該看過畫面上好幾條進度條同時跑)。為什麼會分層、分層有什麼好處?→ ch03
  3. 建立一個專屬的虛擬網路:Compose 會偷偷幫你開一個網路,讓這組服務可以用服務名稱互相連線(例如你的 App 直接連 sqlserver:1433)。→ ch07
  4. 建立 volume 掛載點:把資料庫檔案放到容器外面,這樣容器砍掉重建資料還在。→ ch06
  5. 用映像「造」出容器並啟動:建立隔離空間、換掉檔案系統視角、啟動裡面那個 process。→ ch02、ch05
  6. 做 port mapping:讓你在自己電腦上連 localhost:1433 的流量,被轉進容器內部。→ ch07
💡 至於「怎麼把我自己的 Spring Boot 程式也變成一個這樣的映像」——那是 ch08~ch10 的主線:寫 Dockerfile、建置、瘦身。而 ch11~ch12 會把 Compose 整個拆開,讓你不只會用,還能自己設計一整套開發環境。

技術細節可以慢慢學,但有個思維一定要先換過來,否則後面每一章都會覺得彆扭。

寵物 vs 牛群(Pets vs Cattle)

  • 傳統伺服器像寵物:每台都有名字,生病了你要細心照顧、想辦法治好,因為牠獨一無二、死了就沒了。(伺服器出問題 → 遠端登入、慢慢查、手動修)
  • 容器像牛群:只有編號,沒有名字。某一頭出問題?淘汰掉,從同一個範本再生一頭一模一樣的——因為牠們本來就該長得一樣,而且再生一頭只要幾秒鐘。

由此推導出三條實務原則

  1. 不要進容器裡手動改東西。你 docker exec 進去裝了個套件、改了個設定,下次容器重建就全部消失。這不是 bug,是刻意的設計——它保證了「每次長出來的都一樣」。
  2. 所有改動都要回寫到檔案裡:要多裝一個套件 → 寫進 Dockerfile;要改一個設定 → 寫進 docker-compose.yml 或環境變數。這種「把最終狀態寫成檔案,讓工具去實現它」的風格叫做宣告式(declarative),也是後面 Kubernetes(ch14)的靈魂。
  3. 凡是要留下來的資料,都必須明確放到容器外面(volume)。沒放的東西,預設就是隨容器一起消失。→ ch06
⭐ 這個思維對你的實際好處:開發環境弄壞了?不用花一小時排查,docker compose down -v 砍掉重來,30 秒後回到乾淨狀態。「壞了不修,重建就好」——這是容器化最大的生產力來源,而它建立在「環境完全由檔案定義」這個前提上。
📌 這個特性有個正式名稱叫不可變基礎設施(Immutable Infrastructure):機器一旦建立就不再修改,要變更就換一台新的。你在 AWS 學到的 Auto Scaling 從 AMI 開新機器(AWS ch03)也是同一個思維的產物。
四代「環境交付」方式對照
方式怎麼做致命問題體積/速度
安裝文件寫 30 步驟給對方照做人會漏步驟、文件會過期— / 數小時
自動化腳本Shell / Ansible 自動執行依賴機器初始狀態,不可重現KB / 數分鐘
虛擬機映像整台機器(含 OS)打包太重:每台養一套完整 OS10~40 GB / 分鐘級
容器映像只打包使用者空間,共用核心隔離不如 VM 徹底(見 ch02)數十~數百 MB / 秒級
容器「不是」什麼:新手誤解澄清
常見誤解實際上是
容器是一種輕量虛擬機不是。VM 靠 hypervisor 模擬硬體;容器只是一個被隔離、被限制的普通 process(ch02 詳解)
容器裡裝了一個完整的 Ubuntu只有 Ubuntu 的使用者空間檔案,沒有核心——核心是跟宿主機借的
容器啟動=在開一台機器沒有開機流程,只是啟動一個 process,所以是秒級甚至毫秒級
容器比較安全隔離度其實比 VM 弱(共用核心=共用攻擊面)。安全是靠正確設定換來的,不是預設送的
資料放容器裡就好容器刪掉,裡面寫的資料就消失。要留的必須放 volume(ch06)
Docker = 容器容器是技術概念,Docker 只是最流行的實作工具之一(還有 Podman、containerd 等)

練習題 點選選項查看解析

0 / 8
01 / 8
同一份 Java 程式在你筆電跑得好好的,丟到同事電腦就掛掉。從容器的角度看,根本原因是什麼?
A 程式碼寫得不夠嚴謹
B 程式執行時依賴一整層「它假設存在」的環境(JDK 版本、系統函式庫、設定、路徑、時區),而我們只交付了程式碼、沒交付環境
C 同事的電腦效能太差
D 缺少網路連線
解析
程式從來不是單獨執行的。它踩在一疊假設上:JDK 版本、glibc、環境變數、檔案路徑、時區、相依服務版本、port 是否被佔用。我們一直只寄「食譜」(程式碼),卻假設每個人的「廚房」(環境)都一樣。容器的解法就是把廚房一起寄過去。
02 / 8
用 Shell Script 自動安裝環境,比手寫安裝文件好,但它仍然無法保證一致。為什麼?
A 腳本執行速度太慢
B 腳本的結果依賴機器的初始狀態——在乾淨機器和用過的機器上跑,可能得到不同結果,也就是不可重現
C 腳本無法安裝資料庫
D 腳本只能在 Linux 上執行
解析
腳本是「一串步驟」,不是「一個結果」。它的成敗取決於機器現在長什麼樣:有沒有裝過衝突的套件版本、某個目錄存不存在。容器的作法完全不同——不管你電腦原本是什麼樣,映像裡的檔案系統就是那個樣子,跟宿主機的狀態無關。
03 / 8
虛擬機確實解決了環境一致性問題,那為什麼還需要容器?
A VM 不安全
B VM 太重:映像動輒數十 GB、開機分鐘級、每台都要獨占記憶體養一整套 OS。我們想要 VM 的「整包帶走」但不要它的「整包重量」
C VM 無法在雲端執行
D VM 不支援 Java
解析
VM 的問題不是做不到,是代價太高。只為了跑一個 50 MB 的程式,卻要搬一整套作業系統。容器的取捨是:只打包使用者空間(程式+函式庫+設定),核心跟宿主機共用——換來數十 MB 的體積與秒級啟動。
04 / 8
容器能做到「又小又快」的根本原因是什麼?
A 使用了壓縮率更高的檔案格式
B 容器只打包使用者空間(程式、函式庫、設定),共用宿主機的作業系統核心,因此不需要搬運也不需要啟動一整套 OS
C 容器把程式編譯成機器碼
D Docker 公司開發了特殊的加速晶片
解析
關鍵在「核心 vs 使用者空間」的分界。核心(管硬體那層)全機器共用一份;Ubuntu 和 CentOS 的差別幾乎都在使用者空間。既然核心可以借宿主機的,那只打包使用者空間就夠了——不含核心所以小,不用開機所以快(本質上只是啟動一個 process)。
05 / 8
你在容器裡執行 cat /etc/os-release,看到寫著 Ubuntu。這代表什麼?
A 容器裡跑著一個完整的 Ubuntu 作業系統,含自己的核心
B 容器裡只有 Ubuntu 的使用者空間檔案(指令工具、函式庫),核心是跟宿主機借的
C 宿主機一定是 Ubuntu
D 這個容器實際上是一台虛擬機
解析
這是最常見的誤解。容器內看到的「發行版」只是那套發行版的使用者空間檔案;核心永遠是宿主機的。所以宿主機是 CentOS、容器裡是 Ubuntu 完全沒問題(都是 Linux 核心);但 Linux 容器不能直接跑在 Windows 核心上——Docker Desktop 是偷偷開一台輕量 Linux VM 來提供核心的。
06 / 8
你 docker exec 進容器裡手動裝了一個套件,重建容器後它消失了。這是?
A Docker 的 bug,應該回報
B 刻意的設計:容器是可拋棄的,所有變更都必須寫回 Dockerfile 或 compose 檔,才能保證每次長出來的都一樣
C 因為沒有存檔
D 因為權限不足
解析
這正是「不可變基礎設施」的核心:容器像牛群不像寵物——出問題不是進去修,而是淘汰後從同一個範本重生一個。手動改動會消失,是為了保證可重現性。要多裝套件就寫進 Dockerfile,要改設定就寫進 compose 檔或環境變數,這種風格叫「宣告式」。
07 / 8
下列哪一項是容器相對於 VM 的「代價」而非優點?
A 啟動速度較慢
B 隔離程度較弱——容器共用宿主機核心,等於共用攻擊面,安全性不如 hypervisor 層級的隔離
C 映像檔體積較大
D 無法保證環境一致性
解析
天下沒有白吃的午餐:共用核心換來了輕量與速度,代價就是隔離不如 VM 徹底。所以雲端商用 VM 隔離「不同客戶」,客戶用容器隔離「自己的多個服務」。也因此,容器安全需要額外設定(非 root 執行、最小基底映像、資源限制),不是預設就送給你的。
08 / 8
「docker compose up 一顆 SQL Server」時,Compose 會自動建立一個虛擬網路。這件事的主要用意是?
A 加快下載速度
B 讓這組服務可以用「服務名稱」互相連線(例如 App 直接連 sqlserver:1433),不必知道對方的 IP
C 隱藏容器不被防毒軟體發現
D 節省記憶體
解析
Compose 建立的專屬網路內建 DNS:服務名稱就是主機名稱。這解決了「容器 IP 每次重啟都會變」的問題——你的連線字串可以寫死 sqlserver:1433 而永遠有效。細節在 ch07 容器網路。

點擊卡片翻面查看答案,共 9 張。

QUESTION
「在我電腦上可以跑」的根本成因?
點擊翻面
ANSWER
程式不是單獨執行的,它踩在一整層環境假設上
(JDK 版本、系統函式庫、環境變數、路徑、時區、相依服務版本、port)
我們只交付食譜,卻假設每個人的廚房一樣
點擊翻回
QUESTION
前容器時代三種解法各死在哪?
點擊翻面
ANSWER
① 安裝文件 → 人會漏步驟、文件會過期
② 自動化腳本 → 依賴機器初始狀態,不可重現
③ 虛擬機 → 真的解決了,但太重(數十 GB、分鐘級、每台養一套 OS)
點擊翻回
QUESTION
容器的一句話定義?
點擊翻面
ANSWER
打包「程式 + 它需要的整個使用者空間」
共用宿主機的作業系統核心來執行
= VM 的「整包帶走」,但沒有 VM 的「整包重量」
點擊翻回
QUESTION
核心 vs 使用者空間,差在哪?
點擊翻面
ANSWER
核心:管硬體那層(記憶體、CPU、磁碟、網路),全機器一份
使用者空間:程式、函式庫、設定、指令工具
Ubuntu 和 CentOS 的差別幾乎都在使用者空間
點擊翻回
QUESTION
容器又小又快的原因?
點擊翻面
ANSWER
小:不含核心,只有使用者空間檔案
快:沒有開機流程,本質上只是啟動一個 process
→ 數十~數百 MB、秒級甚至毫秒級
點擊翻回
QUESTION
容器裡看到 Ubuntu,代表什麼?
點擊翻面
ANSWER
只有 Ubuntu 的使用者空間檔案,沒有核心
核心永遠是跟宿主機借的
(所以 Linux 容器要 Linux 核心;Docker Desktop 偷開一台輕量 Linux VM 提供)
點擊翻回
QUESTION
Pets vs Cattle 是什麼意思?
點擊翻面
ANSWER
傳統伺服器像寵物:有名字、生病要細心醫治
容器像牛群:只有編號,出問題就淘汰、從範本重生一個
→ 壞了不修,重建就好
點擊翻回
QUESTION
「不可變基礎設施」的三條實務原則?
點擊翻面
ANSWER
① 不要進容器手動改東西(重建會消失,這是特性不是 bug)
② 所有改動回寫檔案:套件寫 Dockerfile、設定寫 compose(宣告式)
③ 要留的資料必須明確放到容器外(volume)
點擊翻回
QUESTION
compose up 背後的六件事?
點擊翻面
ANSWER
① 解析設定、算出映像座標(ch04)
② 沒有就分層下載(ch03)
③ 建立專屬網路(ch07)
④ 建立 volume(ch06)
⑤ 造出容器並啟動 process(ch02、ch05)
⑥ 做 port mapping(ch07)
點擊翻回