Docker是什麼
Docker是一種虛擬化技術,類似虛擬機器,這使得安裝在其中的程式能夠只依賴虛擬機器的環境,而不受外部作業系統環境的影響。同虛擬機器不同的是,Docker的虛擬容器佔用空間更小,使得它比虛擬機器更容易分發和多例項安裝。
Docker容器化技術的整個開發使用方式非常類似java應用開發,這裡同java應用開發做一個類比,幫助有過java開發經驗的同學快速掌握其中的核心概念
Dockerfile
相當於Java應用開發中的Maven配置檔案pom.xml或則gradle的build.gradle檔案。java開發中的pom.xml和build.gradle是用來宣告java應用依賴的jar包,和應用的構建方式。而Dockerfile是用來宣告一個程式依賴的環境和構建執行方式。比如redis的Dockerfile如下:
# 第一部分,宣告redis程式依賴系統環境,是使用的debian
FROM debian:stretch-slim
# 第二部分,配置系統許可權,新增新的組和使用者,專供redis使用
RUN groupadd -r redis && useradd -r -g redis redis
# 第三部分,是安裝系統更新,環境變數配置,以及下載redis並安裝
ENV GOSU_VERSION 1.10
RUN set -ex; \
\
fetchDeps=" \
ca-certificates \
dirmngr \
gnupg \
wget \
"; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu nobody true; \
\
apt-get purge -y --auto-remove $fetchDeps
ENV REDIS_VERSION 5.0.4
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd
# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
\
buildDeps=' \
ca-certificates \
wget \
\
gcc \
libc6-dev \
make \
'; \
apt-get update; \
apt-get install -y $buildDeps --no-install-recommends; \
rm -rf /var/lib/apt/lists/*; \
\
wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
mkdir -p /usr/src/redis; \
tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
rm redis.tar.gz; \
\
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
\
make -C /usr/src/redis -j "$(nproc)"; \
make -C /usr/src/redis install; \
\
rm -r /usr/src/redis; \
\
apt-get purge -y --auto-remove $buildDeps
# 第四部分,設定redis後續命令的工作目錄
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data
#第五部分,啟動redis服務,並配置向外暴露的埠
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD ["redis-server"]
複製程式碼
可能每個不同的Docker程式,其Dockerfile略有不同,但大致都可以總結為這麼幾步
- 宣告執行系統環境
- 安裝系統更新,安裝程式
- 配置環境變數
- 設定向外暴露的埠,並啟動程式
image
相當於java應用開發中的jar包。java中基於pom.xml或build.gradle build而成jar。而docker中,基於Dockerfile build出的是image。它可以像jar包一樣,提交到Docker的中央倉庫,並被下發指其它機器使用。一個使用Dockerfile構建image的demo如下:
-
先用python開發一個簡單的web服務,名為app.py
from flask import Flask from redis import Redis, RedisError import os import socket # Connect to Redis redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2) app = Flask(__name__) @app.route("/") def hello(): try: visits = redis.incr("counter") except RedisError: visits = "<i>cannot connect to Redis, counter disabled</i>" html = "<h3>Hello {name}!</h3>" \ "<b>Hostname:</b> {hostname}<br/>" \ "<b>Visits:</b> {visits}" return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits) if __name__ == "__main__": app.run(host='0.0.0.0', port=80) 複製程式碼
-
再編寫Dockerfile
# 從程式程式碼中,我們知道使用的python,需要依賴python的環境。python環境的image在docker公共倉庫中,可以直接使用,在這個image基礎上,新增我們的應用,構建另一個image FROM python:2.7-slim # 把容器看做一個小型作業系統的話,這一步設定後續命令在這個容器作業系統內的路徑。名字可以任意。相當於普通linux中的cd命令。路徑不存在應該可以直接建立。Dockerfile後續的所有命令,都是在這個資料夾下執行的 WORKDIR /app1 # 將宿主機的當前路徑內容拷貝到app1下 COPY . /app1 # 從app.py程式程式碼中,可以看到其依賴FLask庫環境和Redis,這裡通過pip安裝,這一步是在python image的內部執行的,不是外部環境。相當於再給python的image系統映象安裝東西 RUN pip install --trusted-host pypi.python.org Flask RUN pip install --trusted-host pypi.python.org Redis # 將容器的80埠暴露出來。 EXPOSE 80 # 在容器內設定一個環境遍歷,key為NAME, value為world。就像linux中設定環境變數一樣。只不過這裡是在容器這個作業系統內設定環境變數,相應的容器中的程式可以讀取這個環境變數 ENV NAME World # 這一步放在最後,前面的所有命令基本上把程式要求的環境都初始化好了,這裡直接執行命令,CMD的第一個引數是程式命令,後面的是引數。這裡就是通過python來run app.py。 由於當前路徑是/app1(前面WORKDIR設定的),並且其中包含app.py,所以在該路徑下執行python app.apy當然找得到程式檔案 CMD ["python", "app.py"] 複製程式碼
-
構建image 在宿主機上建立一個資料夾,名字任意,將Dockerfile和app.py 都放置其中(因為Dockerfile中有一個命令COPY . /app1,所以要確保程式跟Dockerfile在同一的路徑下,才可以拷貝進去。當然你可以不在一個路徑下,那就需要修改Dockerfile命令,將具體app.py的路徑寫全),然後在該路徑下執行構建命令構建image,並將其取名為hellworld
docker build --tag=helloworld .
-
釋出image 你可以像釋出jar一樣,將image釋出到docker中央倉庫,或公司的私有倉庫,具體方式這裡就不展開討論了。
container
類似於java應用中的jar執行。我們基於image執行後,會建立一個執行的例項,即為container,容器。比如我們可以使用以下命令,通過前面build的image,建立一個container
docker run -p 4000:80 helloworld
複製程式碼
network
container需要對外進行通訊,可能需要網路服務。有5種網路驅動可供docker配置,用來配置docker的聯網行為。
-
bridge 橋接模式,通過鏈路層裝置連結host網路,它同host使用不同的ip,一般在單節點的host使用這種方式,預設是這種方式
-
host 模式,container直接跟host公用一個ip,這也意味這container暴露什麼埠,通過host的ip可直接訪問,不推薦這種方式
-
overlay docker叢集的網路連線驅動方式
-
Macvlan 對docker配置mac地址,通過實體地址進行網路通訊
-
none 使docker沒有任何網路連線
data volumes
container中的程式執行時,可能會產生一些資料,或者需要使用一些資料,甚至希望同其它container共享資料。那麼實現這些的方式就是data volumes,它對應docker的儲存概念,後續會詳細講解。
docker daemon
類似於Java虛擬機器。它負責image構建,分發,獲取,執行,以及container、volumes、network等上述核心元件的管理,遮蔽底層作業系統的細節,使得基於docker構建的服務能夠跨平臺。我們一般通過docker CLI也即docker命令列來向docker deamon傳送命令執行上述管理。
Docker的基本使用方式
作為普通使用者大多數時候,我們只是從中央倉庫中獲取別人製作好的image,在本地建立container來提供服務,比如獲取mysql的image,在本地建立一個mysql的servers。所以下面主要介紹對container的一些核心操作命令。
獲取image
使用如下命令去遠端倉庫中拉取,image檔案
docker pull IMAGE[:TAG]
複製程式碼
比如我們想要獲取redis的image,在中央倉庫中我們可以看到有很多redis的image,他們用不同的tag區分
我們可以通過指定tag來拉取特定的image,比如我們拉取tag為5.0.4-alpine的image。docker pull redis:5.0.4-alpine
如何建立一個container
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
複製程式碼
其命令主幹是docker run IMAGE
。每一次run,都會建立一個新的container
我們基於前面拉到的redis image啟動一個containerdocker run redis:5.0.4-alpine
可以在建立的時候指定許多引數,比如建立container時,指定名字docker run --name test-redis redis:5.0.4-alpine
將容器中的程式以後臺形式執行docker run --name some-redis -d redis
檢視docker相關元件
我們可以使用ls命令,來檢視docker中container,image,network,volume等元件的id和名字,就像linux中的ls命令一樣。
docker container ls #檢視正在執行的container
docker container ls --all #檢視包括已停止和執行中的所有container
docker image ls #檢視本地擁有的image
docker network ls#檢視當前系統具有的網路驅動
docker volume ls#檢視當前系統具有的volume儲存
複製程式碼
如何停止一個container
docker container stop CONTAINER_ID|CONTAINER_NAME
複製程式碼
可以使用container的id或name來將處在run中的container停止
如何啟動一個start
通過上述的ls命令,獲取到container的名字或id,然後通過命令docker container start CONTAINER_ID|CONTAINER_NAME
來啟動容器,舉例docker container start 48b24d849908
如何檢視container中的程式執行日誌
我們可以將container當做一個小的linux系統。那啟動後如何登入?有兩種方式,第一種是attach命令到指定的容器,比如
sudo docker container attach 48b24d849908
複製程式碼
但這個命令是隻將當前的host終端attach到指定的container中正在執行的程式。並顯示其輸出。但並不能任意的瀏覽container的其他系統目錄。如果僅僅是為了看當前container中的執行程式日誌,大可不必用上述方法,直接用logs命令輸出即可(當然這種方式的能看到日誌的前提是,container中的程式將日誌輸出到了STDOUT或STDERR中才行)比如:
sudo docker container logs 48b24d849908
複製程式碼
如果嫌輸出的日誌太多,也可以管道加less慢慢看
sudo docker container logs 48b24d849908 | less
複製程式碼
想要正真的直接登入container去瀏覽其系統檔案,需要使用一下命令
sudo docker container exec -it 48b24d849908 /bin/bash
複製程式碼
當然這個要容器裡確實有bash程式才行。exec還可以run程式中的其他命令
如何啟動一個一次性的container
基於image建立一個container後,如果不主動刪除,那麼該container會一直存在,若以希望container被停止後,自動刪除。那麼可以在建立命令run中加引數--rm
。例如:
docker run --rm --name some-redis -d redis
複製程式碼
如何讓容器自動重啟
有時我們希望宿主機在重啟後,或docker deamon重啟後,相應的container能自動重啟。那麼在建立container時,使用引數--restart
來控制重啟行為。重啟策略主要有以下幾種
-
no 預設選項,不會自動重啟container
-
on-failure 當container非正常退出時,自動重啟
-
always 無論什麼情況都自動重啟。但手動停止容器後,需要docker daemon程式重啟時,才會重啟container,也即宿主機重啟時,會重啟container
-
unless-stopped 同always類似,但是手動停止的container不會在自動重啟。
舉例docker run -dit --restart unless-stopped redis
如何做埠對映
程式執行在container中。container又被docker deamon管理。所以需要將container中的程式暴露的埠,對映到宿主機自己的指定埠,否則外部程式無法直接同container通訊。可以在建立時指定引數-p
來指定。例如:docker run -p 6379:6379/udp -p6379:6379 redis:5.0.4-alpine
其中冒號左邊為宿主機的埠,右邊為container中程式暴露的埠。斜槓後面指定暴露的埠型別是UDP還是TCP,如果是TCP可以不寫。
如何對映檔案系統
container中程式可能需要讀或寫一些資料,要使得這些資料能夠被宿主機可見,需要像埠對映一樣,將container中的檔案路徑對映到外部檔案系統中。這些外部的檔案系統可以是宿主機的檔案系統,也可是docker管理的volume。這裡以宿主機的檔案系統為例
docker run -v /home/v2ray_proxy:/etc/v2ray -p 1081:1081 v2ray/official v2ray -config=/etc/v2ray/config.json
複製程式碼
將宿主機路徑/home/v2ray_proxy對映到container的/etc/v2ray路徑,這樣宿主機在/home/v2ray_proxy中修改的內容,container可以通過其/etc/v2ray路徑獲取到。反之亦然。
如何清理所有不使用的container、image、volume、network
可以使用rm命令,刪除指定id或name的相關元件。比如:
docker container rm CONTAINER_ID
docker image rm IMAGE_ID
docker volume rm VOLUME_ID
docker network rm NETWOKR_ID
複製程式碼
可能上述手動挨個刪太麻煩,你可以使用prune
命令,直接將符合需求的元件全部刪除。比如:
docker image prune#刪除未被任何容器使用的image
docker container prune#刪除所有未啟動的container
docker volume prune#刪除所有未被使用的volume
docker network prune#刪除所有未被使用的網路
docker system prune#刪除所有未被使用的container,image ,volume, network。docker 1.7以上需要顯示執行`--volumes`引數,才能一併將volume也刪除,之所以這麼做是害怕一不小心把資料給刪了。多加引數增加了誤刪資料的門檻
複製程式碼
以上所有的刪除prune命令,都可以基於過濾條件來刪除。加引數--filter
即可,比如刪除過去24小時未啟動的容器
docker container prune --filter "until=24h"
複製程式碼
如何檢視container的資源使用情況
使用命令docker stats
Layer
一個Dockerfile最終會被構建成image,一個image被run後會生成一個container。為了最大化共享儲存檔案,減少儲存空間的浪費,docker引入了層的概念layer. Docerkfile中RUN, COPY, ADD三個命令會產生layer
一個dockerfile中從上下到下的命令,反應到image上是由下到上的層,每一層都是基於上一層進行構建的。layer又分為image layer和container layer,前者是image構建時,每句dockerfile命令對應生成的layer,後者是通過image 生成一個新的container 時,container所獨有的read writer 層。
container的read writer layer是container的程式讀寫檔案時,檔案的儲存的層,它會隨著container的銷燬而銷燬。通常來說,container執行生成或修改檔案內容最好不要放到其read write layer,因為不方便cotnainer間共享,又容易影響container本身的讀寫效能,所以一般通過volume或bind mount的方式,將container讀寫的檔案內容對映掛載到外部。
比如,Dockerfile
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
複製程式碼
- 第一句是基礎層,表示基於ubuntu15的image構建
- 第二句在ubuntu15的基礎上,將宿主機當前路徑的內容拷貝到image的/app路徑做為新的layer
- 第三句,使用make命令,將/app中的檔案進行編譯,生成的內容為新的layer
- 第四句,使用python命令執行上一步build的可執行檔案app.py,其對應container中的R/W layer
其對應的image層的示例為:
多個container公用image layer的示例:檔案系統
Docker中的任何資料的產生,預設都是儲存在了container的write layer,這帶來了以下一些問題:
- 不方便備份和訪問,因為資料在容器裡面
- 資料易丟失,當容器被刪除後,資料也跟著被刪除
- 不方便程式更新,容器跟資料繫結了,這個時候你想通過更新的image,建立新的容器來達到升級程式的目的變得很難,因為你要丟資料
為了解決這些問題,Docker提出資料更容器分離的理念。以掛載的路徑來區分,有以下三種掛載方式
- volume mount 受docker deamon管理的檔案系統
- bind mount 當前宿主機的檔案系統
- tmpfs mount 記憶體
Volume mount
建立volume的幾種方式
-
直接用volume命令建立例如
docker volume create my-vol
-
在建立一個container時或service時,通過引數
-v
或者--mount
掛載volume時,volume不存在,也會自動建立。舉例如下://volume名為myvol2,掛載到container的指定目錄為/app $ docker run -d \ --name devtest \ -v myvol2:/app \ nginx:latest //建立四個nginx container組成的service $ docker service create -d \ --replicas=4 \ --name devtest-service \ --mount source=myvol2,target=/app \ nginx:latest 複製程式碼
-v
和--mount
這兩個都能指定掛載的volume(如果不存在,都會建立),建立service時,只能使用mount命令。-v
引數後面直接指定所有的配置value不直觀,--mount
的配置,則是以key=value的形式體現,能夠清楚的知道指定配置項意義。能通過他們配置的資訊有:
- source container外的宿主檔案系統(bind mount時,source就是宿主的檔案路徑)或volume
- destination path: container 內的指定路徑
- 讀寫模式:對掛載的宿主檔案路徑或volume是否有讀寫的權利
- driver: 如果掛載到container的是volume時,配置該volume的驅動型別。volume的驅動型別預設是local,也即宿主機所在檔案系統。但有些volume對應的儲存可能是aws,所以其驅動就不是local.
使用-v
引數的大概形式為:
-v <source>:<destination>
//其中source可以忽略,忽略時,預設建立一個匿名的volume
複製程式碼
使用--mount
引數的大概形式為:
--mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>'
//其中src可以寫為source
//dst 可以寫為destination或target
複製程式碼
像volume中填充內容 如果一個空的volume掛載到指定的container目錄,並且該目錄下已經有內容,那麼這些內容會自動被複制到volume下。舉例如下;
$ docker run -d \
--name=nginxtest \
--mount source=nginx-vol,destination=/usr/share/nginx/html \
nginx:latest
//名為nginx-vol的volume,裡面會被拷貝進/usr/share/nginx/html檔案
複製程式碼
bind mount
volume是掛載一個由docker 守護程式管理的檔案系統到container。而bind mount是直接掛載宿主機的任意檔案路徑到container。這樣宿主機其他程式該掛載路徑下的檔案內容,container也會感受到,反之亦然。其掛載命令跟volume差不讀,不再贅述,只是其mount的type為bind。
簡單總結來看,希望容器間相互共享內容,使用volume掛載到container 希望容器和宿主機之間相互共享內容,使用bind mount
tmpfs mount
tmpfs是將容器指定路徑對映到記憶體,這樣當容器對指定路徑寫資料時,不會寫到容器自己的write layer。並且tmpfs不能被容器共享,即A容器mount 的tmpfs,不能被B容器讀到,這就使得tmpfs非常適合儲存一些易失的,且容器獨有的私密資訊。
tmpfs只能在linux的docker中使用
tmpfs的掛載也有兩種引數方式,一是--tmpfs
,二是--mount
,前者不能指定任何引數,後者則可以,後者的功能和工作範圍都比較廣。
volume和bind在掛載時,需要指定一個source,而tmpfs的掛載不需要,只用指定掛載到對應contaienr的路徑即可。
後話
容器化使得部署應用變得簡單方便。docker還提供了swarm,使得服務以叢集化形式編排和部署同樣變得簡單。這裡不再詳述。
使用容器化提供服務時,需要遵循微服務化的原則,保持服務的原子性,即一個container只提供一種服務。這樣更加方便後期管理和程式擴充套件。