Docker 快速入門實操教程(完結)
Docker,啟動!
如果安裝好Docker不知道怎么使用,不理解各個名詞的概念,不太了解各個功能的用途,這篇文章應該會對你有幫助。
前置條件:已經安裝Docker并且Docker成功啟動。
實操內容:使用Docker容器替換本地安裝的程序并遷移數據(MySQL、redis)。
最終目的:熟練使用Docker各項功能。
理解概念
Docker官方提供了一個分發平臺DockerHub,可以從上面拉取已經提供好的鏡像直接構建容器運行。
這個過程會涉及到Docker的一些概念,在剛接觸的時候比較抽象,這里以烘焙出一個蛋糕為例子說明一下:
- Dockerfile: 蛋糕的配方。配方上詳細列出了需要的材料(如面粉、糖、雞蛋)以及烘焙的步驟(如先將面粉和糖混合,然后加入雞蛋攪拌)。
- 鏡像(Image): 按照配方做出了一個半成品蛋糕,這就是蛋糕的"鏡像" 。這個蛋糕可以被任何人復制,每一個復制品都會和原蛋糕一模一樣。
- 容器(Container): 將半成品蛋糕烘焙后,得到一個可食用的蛋糕。可以根據同一個鏡像制作出很多個完全一樣的蛋糕,也可以在烘焙時自己加一些材料。每個蛋糕都是獨立的,和其他蛋糕沒有關聯。
所以從DockerHub拉取鏡像并且跑起來的過程就可以理解為:
- 鏡像提供者編寫好了配方(
Dockerfile
),將其制成(構建
)了半成品蛋糕(鏡像
)。 - 用戶購買(
拉取
)這個半成品蛋糕。 - 烘焙(
創建
)后得到了一個可食用的蛋糕(容器
),食用蛋糕(運行容器
)。 - 通常創建容器和運行容器都會歸攏在同一步:創建并運行。
還有另外兩個比較重要的概念: 層(Layers)
和 緩存(Cache)
,目前不會接觸到,可以在看 構建/推送鏡像
這一節時再去深入理解。
創建/運行容器
每一步都提供了Docker desktop(簡稱桌面版)的操作截圖和終端命令(桌面版界面友好但局限較大,僅適合初步上手)。
拉取鏡像
從DockerHub拉取MySQL鏡像到本地,這一步可能會因為網絡原因失敗,可以配置其他鏡像源或者使用代理,網上教程很多。
終端命令
docker pull 倉庫地址/命名空間/鏡像名稱:標簽
- 倉庫地址: 沒有顯式指定倉庫地址時,默認會從DockerHub查找鏡像;拉取私有倉庫的鏡像,需要指定倉庫地址。
- 命名空間: 截圖最后有一個名為
ubuntu/mysql
的鏡像,其中ubuntu
是命名空間,用以區分不同的個人或組織發布的鏡像。沒有顯式指定命名空間時,默認會查找官方團隊發布的鏡像。 - 鏡像名稱: 需要拉取的鏡像的名稱。
- 標簽: 沒有顯式指定標簽時,默認會拉取
latest
標簽,latest
表示這是最新的版本。
通過 docker pull
拉取鏡像并不是必須的,在 docker run
時,如果本地不存在指定鏡像,Docker會自動拉取。
創建并運行容器
拉取完成后,通過 docker run
創建容器并運行前進行一些配置:
終端命令
# 截圖對應命令
docker run -d --name mysql_8.3.0 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:latest# 完整命令
docker run [選項參數] 倉庫地址/命名空間/鏡像名稱:標簽 [命令行] [命令行參數]
-
選項參數:
--name
:設置容器名稱,不能重復,這里使用的是鏡像名_版本號
的方式。-p
:設置端口映射,將宿主機的3306
端口映射到容器的3306
端口,宿主機上的其他進程通過該端口才能訪問到容器內的服務。如果不配置端口映射,則只能在容器內部訪問服務或通過虛擬網絡讓容器可以相互通信。-e
:設置環境變量,配置MYSQL_ROOT_PASSWORD=root
用以指定root用戶密碼,這是由鏡像創建者約定的,不同的鏡像配置項會有所不同。-v
:設置目錄掛載,用法參考目錄掛載
章節。-d
:讓容器在后臺運行 -
命令行: 在容器啟動時執行命令(如
ls
),可以省略。 -
命令行參數: 傳給 命令行 的額外參數(如
/etc
,這樣在容器啟動時就會執行ls /etc
),可以省略。
常用命令
容器已經創建好后就不再適用于 docker run
命令了, docker run
命令主要是用于創建新的容器并運行,如果需要啟動已經存在的容器,則使用 docker start
命令。
# 列出所有容器
docker ps -a# 列出所有鏡像
docker image ls
docker images# 啟動容器
docker start 容器名稱/容器ID# 停止容器
docker stop 容器名稱/容器ID# 強制停止容器
docker kill 容器名稱/容器ID# 重啟容器
docker restart 容器名稱/容器ID# 刪除容器
docker rm 容器名稱/容器ID# 刪除鏡像
docker rmi 容器名稱/容器ID
目錄掛載
現存問題:
- 數據沒有保存到宿主機中,當容器刪除后,數據就丟失了。
- 宿主機和容器之間的文件傳遞比較麻煩。
- 多個容器需要共享數據。
目錄掛載可以解決以上問題,Docker為目錄掛載提供了三種方式:
-
bind mount: 把宿主機目錄映射到容器內,雙向文件傳遞。適合變動比較頻繁的場景,比如代碼目錄、配置文件等。
-
volume: 由容器創建和管理,存儲在宿主機中,官方推薦,Linux 文件系統。適合存儲不需要關心的數據,如數據庫數據。
-
tmpfs mount: 適合存儲臨時文件,存儲在宿主機內存中。不可多容器共享。
以MySQL鏡像為例,其 Dockerfile
中寫了創建 volume
用于持久化保存數據的命令(其他鏡像也可以通過這種方式查看需要持久化的目錄)。
雖然 Dockerfile
中有創建 volume
的命令,但是如果創建容器時沒有主動為 volume
命名,其就是匿名 volume
,Docker會為匿名 volume
隨機生成一個名稱,當掛載該 volume
的容器被刪除后,該 volume
也會被刪除。
當創建容器時主動指定的 volume
路徑和 Dockerfile
約定的路徑一致,則該鏡像創建的 volume
就不會被掛載為匿名 volume
了,容器刪除后該 volume
也會保留,這也是最方便的一種命名方式。
終端命令
docker run -d --name mysql_8.3.0 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -v=mysql_volume:/var/lib/mysql -v D:\mount:/pc_mount mysql:latest
掛載目錄時,如果只賦予了名稱則是 volume
方式,如果指定了具體目錄就是 bind mount
方式。
所以在這個容器中:
- 將容器內的
/var/lib/mysql
目錄掛載為volume
并且命名為mysql_volume
。 - 將宿主機的
D:\mount
目錄映射至容器中的/pc_mount
。
在掛載目錄時,如果你指定的目錄不存在于容器中,則會自動創建,這里的 /pc_mount
目錄就會自動被創建。
遷移實操
現在需要將宿主機中MySQL數據遷移到容器中,打算采用navicat的數據遷移工具,那么就需要同時運行兩個數據庫,端口同為 3306
會沖突,宿主機上MySQL端口改起來并不方便,容器創建后端口也不能修改,那么就可以使用數據掛載的方式。
遷移方案:
- 停止運行端口為
3306
的MySQL容器。 - 新創建一個MySQL容器,端口指定為
3305
(或其他任意未被占用的端口),指定volume
的名稱和端口3306
的容器一致。 - 遷移數據,刪除端口為
3305
的容器,運行端口為3306
的容器,數據遷移成功。
當數據庫文件較大時,使用navicat遷移則會顯得有些性能不足了,這時候就需要通過命令行導入:
-
將需要導入的SQL文件放在宿主機掛載的目錄下(宿主機:
D:\mount
;容器:/pc_mount
)。 -
打開容器的終端
docker exec -it mysql_8.3.0 bash
-
登入MySQL并選擇需要導入的數據庫
mysql -u root -proot use test-base; source /pc_mount/001.sql;
從容器中導出SQL文件到宿主機同理,將SQL文件導出至掛載的 /pc_mount
目錄下,在宿主機的 D:\mount
就可以看到。
虛擬網絡
每個Docker容器都運行在自己的環境中,互不干擾,所以上述內容中都依賴宿主機的端口映射進行容器通信。但是有些時候我們只要讓這個項目能在宿主機上訪問到,并不在意其所依賴的服務是否能夠被宿主機操作和管理。就可以通過Docker提供的虛擬網絡實現容器之間的通信,再映射項目入口到宿主機即可。
桌面版并沒有為虛擬網絡提供較好的GUI支持,需要終端執行。
# 查看已存在的虛擬網絡
docker network ls
默認已經存在了三個虛擬網絡,這是由Docker創建的,對應著不同的網絡驅動類型,驅動類型的區別如下:
- Bridge網絡: 默認值。容器在獨立的網絡空間運行,可以相互通信并訪問外部網絡。容器內服務能通過端口映射被外部訪問。
- Host網絡: 容器共享宿主機的網絡空間,不再需要端口映射,直接使用宿主機的端口。這種模式提供了最高的網絡性能,但是失去了隔離性。
- None網絡: 容器擁有自己的網絡空間,但不配置任何網絡接口。它只有本地回環接口,沒有任何外部網絡訪問能力,提供了最高的網絡隔離性。
# 創建名為 test_net 的網絡
docker network create test_net# 在該網絡下創建兩個容器
docker run --name redis_temp --network=test_net -d redis:latest
docker run --name redisinsight -p 8001:8001 --network test_net -d redislabs/redisinsight:latest
創建了redis容器,但是并沒有為其映射端口。所以現在在宿主機中并不能訪問到這個redis容器。
創建了redisInsight容器并且映射了8001端口,這是一個redis的GUI工具,用于測試是否可以通過虛擬網絡訪問到redis容器。
訪問 http://localhost:8001/ 進入redisInsight的主頁,添加一個redis數據庫。
Docker內部的DNS服務會自動將容器名稱解析為容器對應的IP地址(即容器名稱就是域名),所以主機地址填寫容器名稱即可。
連接成功,這樣既可以操作容器內的redis數據,又不會占用宿主機自身的redis應用搶占端口。同理,部署其他項目時,如果項目容器需要連接數據庫容器,也可以通過虛擬網絡實現。
如果容器已經被創建,可以更改已存在的容器的連接的網絡
docker network connect 網絡名稱 容器名稱
使用技巧
查看軟件版本
部分鏡像的 Tag
是 latest
,并沒有明確指出具體的版本號,想要查看版本號就只能手動查看。
桌面版點擊容器右側 ···
打開更多選項,選擇 Open in terminal
進入容器的終端,執行該軟件查看版本的命令。
終端命令
docker exec mysql_8.3.0 mysql -V
但是問題就來了,如果需要版本號是為了給容器命名,這種方案需要先運行容器,將容器刪除,再重新創建容器,很麻煩。
通常鏡像的環境變量中會指明版本號,可以直接點開鏡像查看
終端命令
docker inspect mysql:latest
這個方式雖然比較方便,但是需要進行推測,并非一定正確。
保持容器運行
當在桌面版運行ubuntu等容器時,會發現容器啟動后就停止了,進入 Exited
狀態,如果想要容器持續運行,就需要需要在容器內部執行一個持續運行的進程。
桌面版已經不能滿足需求了,需要終端執行
docker run -it --name ubuntu_22.04 ubuntu:latest
-t
指令分配一個虛擬的終端或控制臺,可以讓容器持續運行不會關閉。
-i
指令可以讓打開的控制臺能夠接受用戶輸入。
構建/推送鏡像
想要通過Docker將項目部署到服務器上或是分發項目供他人使用,就需要將項目構建為鏡像,官方主要推薦通過 Dockerfile
構建鏡像。 Dockerfile
是一個文本文件(無文件后綴),由一系列的命令和參數構成,這些命令對應了在構建鏡像時的操作步驟。
編寫Dockerfile文件
Dockerfile常用指令:
- FROM: 指定基礎鏡像。所有后續的操作都是基于這個基礎鏡像進行的。
- WORKDIR: 設定后續命令的執行目錄。
- COPY: 復制文件、指定目錄中的所有內容(不含目錄本身)到鏡像中。
- ADD: 復制文件、指定目錄中的所有內容(不含目錄本身)到鏡像中。對tar格式的壓縮文件會自動解壓。
- RUN: 構建過程中執行命令。比如安裝一些軟件,創建一些文件等。
- CMD: 為容器提供默認的執行命令,會被
docker run
的命令行參數覆蓋。 - ENTRYPOINT: 為容器提供默認的執行命令,不會被
docker run
的命令行參數覆蓋。 - EXPOSE: 公開容器的一個端口供外部訪問。
通過maven執行 package
手動將項目打包,命名為 output-dem.jar
,在項目根目錄下新建一個 Dockerfile
文件:
# 使用JDK17基礎鏡像
FROM openjdk:17-jdk-slim# 設置工作目錄,容器也會使用該目錄作為工作目錄
WORKDIR /app# 將jar包復制到/app路徑下
COPY target/output-demo.jar app.jar# 設置在運行此鏡像后默認執行的命令,讓它運行剛才的jar包
ENTRYPOINT ["java", "-jar", "app.jar"]# 暴露端口,取決于項目實際使用的端口號
EXPOSE 8080
如果不是Java開發,設備上并沒有安裝 JDK
和 maven
等構建需要的環境(其他語言同理),但是又有打包項目的需求,則可以通過多階段構建的方式,在鏡像中完成編譯等操作:
# 使用包含JDK17和Maven3.8.5的基礎鏡像
# 將本構建階段命名為 build ,以便在后面的階段中引用
FROM maven:3.8.5-openjdk-17-slim AS build# 設置工作目錄,容器也會使用該目錄作為工作目錄
WORKDIR /app# 將當前目錄下的所有文件添加到工作目錄下(.和./都可以表示當前目錄)
ADD . .# 使用Maven構建項目為jar包
RUN mvn clean package
# 使用Maven構建項目為jar包(跳過測試階段)
# RUN mvn clean package -DskipTests=true# 新的構建階段
# 引入JDK17的基礎鏡像
FROM openjdk:17-jdk-slim# 設置工作目錄,容器也會使用該目錄作為工作目錄
WORKDIR /app# 將 build 階段構建jar包復制到新階段的/app路徑下
COPY --from=build /app/target/output-demo.jar app.jar# 設置在運行此鏡像后默認執行的命令,讓它運行剛才的jar包
ENTRYPOINT ["java", "-jar", "app.jar"]# 暴露端口,取決于項目實際使用的端口號
EXPOSE 8080
在Docker的多階段構建中,每次使用新的 FROM
指令,都會開始一個新的構建階段,上一階段的指令會創建為一個臨時的鏡像。在新階段中,前一階段的層和設置都被丟棄,只有 --from
指定的之前階段的內容被保留,就像是開始了一個全新的 Dockerfile
一樣。
構建鏡像
寫好 Dockerfile
后,就可以通過該文件構建鏡像了
docker build -t 倉庫地址/命名空間/鏡像名:標簽 .docker build -t 倉庫地址/命名空間/鏡像名:標簽 -f /path/myDockerfile .
-t
指定鏡像名稱(如果不推送到私有倉庫,僅本地使用, 倉庫地址/命名空間/
可以省略)。
-f
指定 Dockerfile
所在的目錄,也可以指定 自定義名稱的Dockerfile
( -f
參數可省略)。
.
使用當前目錄下作為上下文環境, COPY
等命令會從該目錄查找文件。未指定 -f
參數時,則使用上下文環境中名為 Dockerfile
的文件。
推送鏡像
每次發布更改內容都需要打包鏡像后上傳到生產環境再部署很麻煩,將鏡像推送至私有倉庫中,在生產環境直接從倉庫中拉取鏡像則更加高效。
# 登錄倉庫
docker login 倉庫地址# 推送鏡像
docker push 倉庫地址/命名空間/鏡像名:標簽
沒有顯式指定倉庫地址時,默認會將DockerHub作為倉庫地址。
層與緩存
層(Layers): 根據 Dockerfile
構建鏡像時,每一個會改變文件系統狀態的指令( RUN
、COPY
、 ADD
等)都會新建一個層 ,每個層都是前一層的改動的結果,并且每個層都只保留改動的部分,共享未改動的部分。依次將所有的層合并起來就是完成的鏡像。
根據這個例子可以方便理解層的概念:
# Dockerfile A
RUN apt-get install -y software-package # 第一層
RUN rm -rf /var/lib/apt/lists/* # 第二層
在這個片段中,第一層中安裝了一個軟件包,第二層中刪除了軟件包。
因為 每個層都是前一層的改動的結果
,所以第二層的刪除文件并不能影響到第一層,只是會在第二層中對該文件打上一個 刪除
的標記,這個文件會作為一個無用的文件存在于最終構建的鏡像中,增加了鏡像的體積。
# Dockerfile B
RUN apt-get install -y software-package && rm -rf /var/lib/apt/lists/*
在這個片段中,安裝和刪除軟件包是在同一個指令下執行的,所以他們是處于同一層的操作,當這一層的構建結束時,這些文件就會被清理掉,它們也就不會存在于最終的鏡像中了。
緩存: 緩存多個鏡像之間可以共享層,如果多鏡像都是基于同一個基礎鏡像進行構建的。那么,這個基礎鏡像的所有層都只需要存儲一次,就能在所有的鏡像中共享。如果一個鏡像的大部分層已經在本地存在,那么在拉取這個鏡像時,只有不存在的層需要被下載,這可以極大地節省時間和網絡帶寬。
Docker Compose
當項目依賴的服務較多時,每個容器都要單獨配置運行,指定網絡。使用Docker Compose,可以通過一個YAML文件定義服務,并同時運行它們。
Docker Compose將所管理的容器分為三層:工程(Project)、服務(Service)、容器(Container)。
通過一個例子來理解三層結構:
- 工程: 一個工程可以被視為一家公司,它為所有服務提供了整體的工作環境和資源配置。
- 服務: 公司內設有各種部門,如財務和行政等,每個部門有自己特定的職責和任務。每個部門都可以被看作一個服務。
- 容器: 每個部門由一個或多個員工組成。盡管每個員工都是獨立的,但他們共享同樣的環境。一個員工相當于一個容器。
所有部門都在同一家公司工作并使用該公司的資源,所以所有服務在工程中共享同樣的網絡、卷等資源。
各個部門之間還是會進行交流和協作,所以各個服務之間可以互相通信。
同一部門的所有員工都具有相同的工作環境,所以屬于同一服務的所有容器都有統一的配置。員工各自做自己的項目,所以容器間有一定的隔離性。
在要部署項目的目錄創建一個 docker-compose.yml
文件:
# 指定Docker Compose配置文件的版本
version: '3.8'services: # 定義應用服務,名為 appapp:image: '倉庫地址/命名空間/鏡像名稱:標簽'# 將容器的8080端口映射到宿主機的8080端口ports: - 8080:8080 volumes:# 將 docker-compose.yml 所在目錄映射到容器中的 /app 目錄(在 Dockerfile 中給定的工作目錄) - ./:/app # 定義啟動依賴,會先啟動 mysqldb 和 redisdb,再啟動 appdepends_on:- mysqldb- redisdb# 指定容器啟動后執行的命令command: ["java", "-jar", "app.jar"] # 如果服務非手動停止,Docker會自動嘗試重啟服務restart: always# 定義一個MySQL服務,名為 mysqldb # 其他服務連接MySQL數據庫時,主機地址就是 mysqldb:3306mysqldb:image: mysql:8.0.30environment:- MYSQL_ROOT_PASSWORD=rootvolumes:- db_data:/var/lib/mysql# 定義一個Redis服務,名為 redisdb# 其他服務連接Redis數據庫時,主機地址就是 redisdb:6379redisdb:image: redis:7.2.4volumes:- redis_data:/datavolumes:db_data:redis_data:
在 docker-compose.yml
文件所在的目錄執行
docker compose up -d
docker compose up
根據 docker-compose.yml
文件內容啟動、創建、連接服務。
-d
參數表示以后臺方式運行。
-f
如果文件名稱不是 docker-compose.yml
,可以通過 -f
命令指定,使用方法與 構建鏡像
章節一致。
每次更改了 docker-compose.yml
文件,都需要重新運行 docker-compose up -d
命令以應用更改。
結語
任何技術都有其深度與復雜性,難以通過一篇文章詳盡闡述。本文的初衷是為你在遭遇問題時,提供一個尋找解答的方向指引。