1. Docker容器介紹
Docker是一個開源的容器引擎,可以讓開發者把他的應用和依賴環境打包到一個可移植的容器環境中。
容器: 可以理解為一個輕量級的“虛擬機”,應用程序的運行環境。
Docker的特點:
- 應用隔離
- 輕量級的虛擬化方案
- 擴展性,可以輕松擴展出成千上萬的容器實例。
- 移植性,統一開發、測試、生產環境,可以在任意環境運行容器實例。
2. 容器與虛擬機的區別
虛擬機和容器的架構對比:
通過對比容器和虛擬機的架構圖,虛擬機擁有獨占的操作系統,虛擬化比較徹底,容器是共享操作系統通過資源隔離使得容器擁有獨立的環境;虛擬機和容器都共享硬件資源。
因此容器比較“輕”,啟動一個容器可能就是一個進程,可以秒級啟動容器。
3. Docker架構
主要包含下面幾個部分:
- Docker守護進程 (Docker daemon)
負責管理鏡像、容器、容器網絡、數據卷等。 - Client
負責發送Docker操作指令, 日常主要通過client完成鏡像和容器的管理。 - 鏡像 (Image)
即容器的模版,鏡像是可以繼承的,鏡像主要通過Dockerfile 文件定義。 - 鏡像倉庫 (Registry)
類似git倉庫, 只不過鏡像倉庫用於存儲鏡像和管理鏡像的版本。 - 容器
容器是通過鏡像創建的,所以說容器是一個鏡像運行的實例,類似面向對象編程中類和對象的關系。
4. 安裝docker
下面以centos安裝docker ce社區版本為例:
//先安裝一些依賴的驅動和配置yum $ yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 $ yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo //安裝docker $ yum install docker-ce docker-ce-cli containerd.io //啟動docker $ systemctl start docker //測試下是否安裝成功, 這里運行一個hello-world鏡像,正常的話會輸出 Hello from Docker! $ docker run hello-world
5. 容器基本用法
5.1. 快速入門
下面通過打包一個靜態網站講解容器的基本用法。
首先對於一個靜態網站,我們需要什么樣的環境才能跑起來?
對於靜態網站,我們只需要一個Nginx軟件就能跑起來,下面一步步講解怎么通過容器部署這個網站。
下面是網站的項目目錄結構:
├── Dockerfile // Docker鏡像定義文件 └── site //網站代碼目錄,只是一個簡單的靜態網站,只有一個頁面 └── index.html
5.1.1. 定義鏡像
創建鏡像,我們首先需要定一個鏡像,定義鏡像的目的說白了就是我們要在容器里面安裝什么東西。
下面是Dockerfile的定義:'#' 井號表示注釋.
#FROM 指令表示要繼承的鏡像,這里繼承nginx官方鏡像,版本號是1.15.6 #定義鏡像,都是通過繼承某個已經存在的鏡像開始 FROM nginx:1.15.6 #設置工作目錄 WORKDIR /workspace #/usr/share/nginx/html目錄是nginx官方鏡像的默認網站目錄。 #復制site目錄的內容, 到/usr/share/nginx/html目錄。 COPY ./site/* /usr/share/nginx/html #刪除掉/workspace的內容,因為上面的指令已經把網站內容復制到指定的目錄,這里的內容現在沒用了 RUN set -ex \ && rm -rf /workspace #從新設置工作空間 WORKDIR /usr/share/nginx/html #聲明容器需要暴露的端口 #EXPOSE 80 #容器啟動命令,這里啟動nginx, 參數-g daemon off; 的意思是關閉nginx的守護進程模式。 #CMD ["nginx", "-g", "daemon off;"] # EXPOSE 和 CMD命令都被注釋掉了,因為nginx基礎鏡像已經定義過了,所以這里不需要重復定義,這里只是為了講解目的,說明下容器是怎么運行nginx的。
5.1.2. 創建鏡像
下面我們通過docker客戶端創建鏡像。
//先切換到項目的根目錄 //通過docker build命令創建鏡像 //格式: docker build -t 鏡像名:版本號 上下文路徑 //上下文路徑的意思就是我們需要把那個目錄上傳到docker守護進程作為鏡像創建的工作目錄。 //這里把當前目錄作為上下文環境目錄 $ docker build -t demo1:v1.0.0 . 執行成功會輸出如下結果: Sending build context to Docker daemon 3.584kB //先打包上傳上下文目錄內容 Step 1/3 : FROM nginx:1.15.6 1.15.6: Pulling from library/nginx //下載nginx鏡像 a5a6f2f73cd8: Already exists 67da5fbcb7a0: Pull complete e82455fa5628: Pull complete Digest: sha256:31b8e90a349d1fce7621f5a5a08e4fc519b634f7d3feb09d53fac9b12aa4d991 Status: Downloaded newer image for nginx:1.15.6 ---> e81eb098537d //下面開始執行我們Dockerfile定義的命令 Step 2/3 : WORKDIR /usr/share/nginx/html ---> Running in 155da3e24b72 Removing intermediate container 155da3e24b72 ---> 608d1e7fc6cd Step 3/3 : COPY --chown=nginx:nginx . /usr/share/nginx/html ---> 894f22f15a7c Successfully built 894f22f15a7c Successfully tagged demo1:v1.0.0 //查看當前機器上的鏡像,這個時候就看到剛才創建的demo1鏡像 $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE demo1 v1.0.0 894f22f15a7c 11 minutes ago 109MB
5.1.3. 啟動容器/運行網站
啟動容器的過程是: 根據鏡像啟動容器 -> 容器根據Dockerfile配置CMD命令啟動我們應用。
下面啟動demo1鏡像,docker會創建一個容器運行nginx。
//通過docker run命令運行一個鏡像 //命令格式: docker run -p 容器綁定的外部端口:容器內部端口 鏡像:鏡像版本 $ docker run -p 8080:80 demo1:v1.0.0
通過瀏覽器訪問:http://localhost:8080 即可訪問網站
5.2. Dockfile詳解
下面介紹Dockfile常用的指令
5.2.1. FROM
FROM是Dockerfile 文件的第一條指令,標示要繼承的鏡像。
格式:
FROM image[:tag]
參數說明:
image : 鏡像名或者鏡像地址。
tag : 可選參數,鏡像版本或者叫鏡像標簽。
例子:
#這里使用的是官方nginx鏡像,標簽為:1.15.6 FROM nginx:1.15.6 #這里使用的是完整的鏡像地址,標簽為:v1.3.0 FROM XXXX:v1.3.0
5.2.2. COPY
復制指令,主要用於復制目錄和文件。
格式:
COPY [--chown=user:group] src dest
參數說明:
src : 待復制的文件或者目錄
dest : 復制文件的目的地
--chown=user:group : 可選參數,設置復制文件或者目錄的用戶組
例子:
#復制./site/目錄下面的所有文件,到/usr/share/nginx/html目錄,同時設置文件的用戶和組為nginx COPY --chown=nginx:nginx ./site/* /usr/share/nginx/html
5.2.3. RUN
用於執行命令
格式:
RUN command
只有一個參數command, 就是我們想要執行的命令。
5.2.4. CMD
用於設置容器默認執行的命令。
格式:
CMD ["executable","param1","param2"]
參數說明:
executable : 執行程序,可以包含完整路徑
param1, param2 ...param N : 執行程序的任意參數
5.2.5. ENV
設置容器環境變量
格式:
ENV = ...
例子:
#用空格分隔鍵值對 ENV myName="John Doe" myCat="fluffy"
5.2.6. USER
用於設置執行RUN, CMD 和 ENTRYPOINT命令時候的以什么用的身份運行。
格式:
USER user[:group]
參數說明:
user : 用戶名
group : 可選參數,用戶組
注意:如果設置的用戶不存在,則以root用戶身份運行。 例子:
#設置執行RUN、CMD、ENTRYPOINT的時候用戶和組為www
USER www:www
5.2.7. WORKDIR
用於為RUN、CMD、ENTRYPOINT、COPY命令設置工作目錄。如果設置的工作目錄不存在,則自動創建一個。
例子:
WORKDIR /path/to/workdir
5.3. .dockerignore文件
docker build命令在發送上下文目錄的文件到docker守護進程之前,可以通過.dockerignore配置文件,設置需要過濾的文件或者目錄。
.dockerignore文件保存在上下文目錄的根目錄。
.dockerignore例子:
.dockerignore配置語法類似.gitignore
# comment #忽略所有目錄下面的.log后綴的文件 **/*.log #忽略data目錄 /data #忽略所有的.md結尾的文件,除了README.md *.md !README.md
6. 鏡像管理
鏡像管理問題:
- 如果我們為不同的項目創建了多個鏡像,每個鏡像又有多個版本怎么管理它們?
- 在服務器集群中要根據不同的鏡像創建容器,去哪里找到鏡像呢?
為管理鏡像我們需要一個類似github的鏡像倉庫,Docker官方為我們提供了一個公開的鏡像倉庫。 官方倉庫地址:https://hub.docker.com/, 在上面可以找到各種各樣的鏡像,類似nginx、redis、mysql這些知名的開源軟件官方也到這里發布了自己的官方鏡像。
提示:知名開源軟件提供的官方鏡像會有Official Image標記, 這些鏡像會比較可靠,其他的鏡像都是網友上傳的。
下載鏡像不需要賬號密碼,如果要發布自己的鏡像就需要注冊帳號。
對於個人使用公開鏡像當然沒問題,但是對於公司就不行,對於公司要么自己搭建一個私有的鏡像倉庫,要么使用阿里雲這種雲平台提供的鏡像服務,創建一個私有倉庫。
無論是自己搭建的鏡像倉庫還是使用公開的鏡像倉庫用法是一樣的。
鏡像倉庫的用法類似git, 下面是鏡像倉庫的基本用法:
#登陸倉庫命令: #公開倉庫不需要登錄 $ docker login --username=帳號 倉庫地址 #例子: #登陸阿里雲容器服務倉庫 #回車后輸入密碼即可 docker login --username=123456@qq.com XXX #下載鏡像, 命令格式 docker pull 鏡像:[鏡像版本號] #例子1: #下載nginx最新鏡像 docker pull nginx #下載阿里雲鏡像倉庫的鏡像,鏡像版本號為:v1.0.0 docker pull XXX:v1.0.0 #上傳鏡像,命令格式 docker push 鏡像:[鏡像版本號] #例子: #推送鏡像,之前docker login命令登陸到什么倉庫,就推送到那里。 docker push XXX:v1.0.0
7. 容器數據管理
7.1. 鏡像分層技術
一個Docker鏡像是有多個layer(層)組成的;每一層都存儲一些文件數據,每一層都是在另一層的基礎上進行CRUD操作,每一層只是記錄相對前面幾層的差異部分的文件數據,Docker鏡像分層技術,很像git代碼管理,git記錄每一次代碼文件的變化歷史,每一次提交的版本都是上一個版本的差異部分。
注意:鏡像每一層的文件數據都是只讀的。
當創建一個容器的時候,會在頂層增加一層可寫的容器層,我們容器的數據都是寫在這上面。
容器層的數據不是持久化的,刪除容器數據就丟了。
例子:
Dockerfile定義如下
FROM ubuntu:15.04 COPY . /app RUN make /app CMD python /app/app.py
dockerfile由4條指令構成,創建鏡像后,分層示意圖如下:
從底層往上看,最底下的一層保存dockerfile的FROM指令運行后產生的數據,倒數第二層對應COPY指令復制文件產生的數據,以此類推。
大家可以看到每一層都一個ID,例如最底下一層的id為: d3a1f33e8a5a, 很像git的提交記錄吧。
當根據鏡像創建容器的時候會在頂層增加一層容器層(Container layer), 保存容器運行的數據。
提示:docker可以分層緩存數據,如果本地鏡像已經下載過對應層的數據,那么docker不會重復下載對應的層;所以有些鏡像看起來幾百M,但是瞬間就下載完成,原因是分層緩存對應的數據。
下圖是根據同一個鏡像創建多個容器的情況:
每一個容器都有自己單獨的容器層(Container layer),容器之間數據是獨立的;但是他們底層的鏡像數據是一樣的。
7.2. 容器文件數據管理
在Docker容器里面寫文件數據,這些數據不是持久化的,隨着容器被刪除數據將會丟失,下面是Docker提供的容器文件數據管理方式。
主要支持下面三種方式管理文件數據:
- Volumes
共享服務器上的某個目錄,但是這個共享目錄限制在/var/lib/docker/volumes/目錄下面,由docker維護。 - Bind mounts
共享服務器上的文件或者目錄,相對於Volumes區別就是不限制目錄,可以是系統任何位置。 - tmpfs
這種方式文件數據保存在服務器內存中。
提示:無論使用那種方式管理容器文件數據,對容器內部來說,看起來跟本地文件系統一樣。區別就是真實數據保存在什么地方。
容器管理文件數據示意圖:
7.2.1. Volumes方式
使用Volumes方式管理文件數據需要先創建volume數據卷,然后掛載到容器里面。
例子:
//創建數據卷 demo1-data $ docker volume create demo1-data //查看當前機器創建的數據卷 $ docker volume ls #輸出 DRIVER VOLUME NAME local demo1-data //查看數據卷詳情, 可以查看數據具體保存在什么地方 $ docker volume inspect demo1-data #輸出 [ { "CreatedAt": "2019-04-18T15:59:58+08:00", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/demo1-data/_data", "Name": "demo1-data", "Options": {}, "Scope": "local" } ] //刪除數據卷 $ docker volume rm demo1-data //將創建好的數據卷掛載到容器里面 $ docker run -d --name demo1 -v demo1-data:/mnt/data demo1:v1.0.0 參數說明: -d : 讓容器在后台運行. --name : 給容器起個名字. -v : 掛載數據卷, 格式: -v 數據卷名字:容器目錄 demo1:v1.0.0 : 鏡像的名字
7.2.2. Bind mounts方式
任意目錄或者文件掛載方式.
例子:
//一般我們本地開發啟動nginx,配置文件都是在本地,我們希望容器可以讀取本地的nginx配置。 //這個例子是把主機上的nginx配置文件掛載到nginx容器的配置文件里面, 這里掛載的是文件。 $ docker run --name my-custom-nginx-container -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx 參數說明: --name my-custom-nginx-container : 容器名字 -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro : -v 掛載參數,將主機上的/host/path/nginx.conf文件掛載到容器的/etc/nginx/nginx.conf文件,ro 參數只讀的意思。 //除了上面的掛載文件,我們也可以掛載目錄, 接着上面的例子 $ docker run --name my-custom-nginx-container -v /host/path/nginx/conf:/etc/nginx/:ro -d nginx 參數說明: -v /host/path/nginx/conf:/etc/nginx/:ro : 這里把主機的/host/path/nginx/conf目錄以只讀方式掛載到容器的/etc/nginx/目錄。
7.2.3. tmpfs方式
tmpfs主要是將數據存儲在主機內存中。
例子:
$ docker run -d --name demo1 --tmpfs /mnt/data demo1:v1.0.0 參數說明: --tmpfs /mnt/data : 掛載主機內存空間到容器的/mnt/data目錄
8. 容器互相通信
在實際應用的時候我們往往會啟動多個容器,分別運行不同應用,又或者為了負載均衡啟動大量的容器實例;那么容器之間怎么通信?
容器之間的通信方式跟服務器之間的通信方式差不多,除此之外在同一台主機的容器之間Docker支持直接互相連接的通信方式。
容器之間的通信方式大體上有兩種:
- 基於tcp/ip的網絡通信。
- 同一台主機上創建容器的時候,使用--link參數連接本地的其他容器。
例子1: 同一台主機上的容器之間通過link參數互聯.
//啟動一個redis server,容器名字叫: redis, redis其他參數配置默認 $ docker run -d --name redis //另外啟動一個容器,連接redis容器, 這樣在demo1容器內部可以使用redis名字當成redis服務器地址 $ docker run -d --name demo1 --link redis demo1:v1.0.0 參數說明: --link :連接容器, 格式: --link 容器名 , 連接多個容器,可以使用多個--link參數即可
例子2:主機怎么訪問容器里面的應用?
例如,本地開發測試,我們使用容器運行應用,那么容器外面的主機怎么訪問?
容器需要暴露容器內部的端口給主機,主機才能訪問容器內部的應用。
//例如,我們使用nginx容器部署了一個網站應用,nginx容器需要暴露80端口出來。 $ docker run --name myapp -p 8080:80 -d nginx 參數說明: -p : 綁定端口參數,格式 -p 主機端口:容器內部端口
主機端口可以任意選擇,只要沒被其他程序占用即可。
主機端口不一定要跟容器內部端口一致, 只是我們習慣用一樣的端口,便於理解。
9. Docker常用命令
//查看正在運行的容器 $ docker ps //啟動容器 - 容器創建之后不需要再次使用docker run命令創建容器, 這個時候只要啟動容器即可。 //啟動容器名為redis的容器 $ docker start redis //關閉容器 $ docker stop redis //刪除容器 $ docker rm redis //容器的啟動、關閉、刪除除了使用容器名字之外,還可以使用容器id //例如: $ docker stop f648f34beabb //執行容器里面的命令 $ docker exec -it redis bash 參數說明: -it : 打開一個交互的終端。 redis : 容器的名字叫做redis bash : 執行容器里面的bash命令 //上面這條命令的其實就是連接容器打開一個shell窗口,類似登錄服務器一樣。 // 命令格式: docker exec 容器名 要執行的命令 //刪除鏡像 docker image rm 鏡像名 //給鏡像打標簽 docker tag 鏡像id 標簽 例子: $ docker tag ImageId xxxx:v2.0.1 //清理未使用的鏡像 $ docker image prune
10. Docker實踐建議。
10.1. 盡量減小鏡像大小和layer。
不安裝跟應用無關的軟件,清理掉沒有用的文件; 例如我們在編譯安裝軟件的是最產生很多中間文件,安裝成功后可以刪除這些數據。
因為Docker鏡像的層級是有限的,在Dockfile配置文件中FROM、COPY、RUN這些指令都會產生layer, 用的最多的主要是RUN指令,推薦把連續的多條RUN指令合並成一條。
例子:
FROM centos COPY . /app WORKDIR /app #安裝依賴包 #下面每一條RUN指令都會產生一個layer, 光是RUN指令就產生了6個layer RUN yum -y install gcc automake autoconf libtool make gcc-c++ #編譯安裝 RUN ./configure RUN make RUN make install #清理安裝文件 RUN rm -rf /app #清理yum 緩存 RUN yum clean all
優化后的Dockfile
FROM centos COPY . /app WORKDIR /app #通過&&把多條命令連成一條命令,每行末尾的反斜杠是為了可讀性,一行書寫一條命令。 #這里只會產生一個layer RUN yum -y install gcc automake autoconf libtool make gcc-c++ \ && ./configure \ && make \ && make install \ && rm -rf /app \ && yum clean all
10.2. 避免在容器里面存儲數據
因為在實際生產環境中容器是經常被銷毀、重建的,可能是因為應用異常容器退出了,也可能是某台服務器負載太高,容器被調度到別的服務器運行; 當容器被銷毀的時候存儲在容器的數據會丟失。
生產環境中存儲持久化數據一般有以下方案:
- 通過數據卷存儲數據,一般可以選擇把數據存儲在主機服務器上、也可以存儲在分布式文件系統上。
- 通過外部數據庫、雲存儲、日志服務等可持久化的存儲引擎或者第三方雲服務存儲數據。
10.3. 一個容器只跑一個應用
要保證容器的“輕”的特性,我們一般不會把容器當成服務器來用,什么東西都往里面安裝。
例如:我們經常會在一台服務器上安裝nginx、php、mysql、redis,同時部署一堆應用系統。
如果在容器這么干,升級維護就變得復雜了,而且不容易擴展,如果要升級redis,或者升級某個應用系統就得大家一起更新,而且還沒法對里面的單個應用進行擴容。
一個容器跑一個應用,可以保證容器的輕量級特性,又相對獨立,便於單獨升級和擴容。
例如:一個PHP系統,需要nginx、 php fpm、 mysql、redis 那么鏡像和容器可以這么設計。
鏡像制作情況:
- nginx + php-fpm + PHP系統源碼 制作一個鏡像。
- redis 單獨一個鏡像。
- mysql 單獨一個鏡像。
容器情況:
根據上面的鏡像制作情況,至少需要同時運行三個容器,上面的三個鏡像分別創一個容器。
10.4. 為項目或者公司制作基礎鏡像
一個項目或者一個公司的基礎環境一般都是一樣的,沒有必要每次都重新制作基礎環境鏡像。
例如一個項目的基礎環境是: nginx + php-fpm + PHP擴展庫(curl、kafka、redis、swoole), 我們只要先根據環境需要制作一個基礎鏡像,項目只要繼承這個基礎鏡像,然后設置項目自己的參數即可。
10.5. 容器應用主進程不能切換到后台運行。
容器通過CMD設置應用的啟動命令,當容器啟動應用的主進程后,應用主進程的生命周期就跟容器捆綁了,主進程退出,容器就退出了。
這個地方跟我們一般服務器部署應用不一樣, 一般在服務器部署應用都是把進程切換到后台保持運行就可以,容器部署應用不能切換到后台運行,否則容器會任務應用結束了,容器自己就退出了。
創建容器的時候可以通過-d參數把容器切換到后台運行,例如: docker run -d --name redis redis