你是否遇到过这种问题,在一个开发环境下面运行完全正常的程序,到了生产环境就会出现种种问题。因为生产环境的种种配置、库与环境变量等依赖项都与开发环境不尽相同,所以部署到生产环境之前,必须提前将生产环境配置好,才能保证服务正常运行。

但这样十分麻烦,尤其是要迁移服务器的话,环境配置又要重新来一遍。最理想的状况是,部署到生产环境可以自带环境配置,把运行正常的环境直接复制到生产机器上。Docker 为这种需求提供了解决方案。

Docker 是什么

Docker 是一种 Linux 容器的封装, 由 Docker Inc. 开发,它把应用程序和依赖打包为一个文件,docker 基于这个文件生成一个虚拟容器,应用程序在这个虚拟容器里面运行,与在真实物理机上运行无异。它解决了环境配置问题。

Docker 目前提供了企业版 (Docker Enterprise Edition, Docker EE) 和社区版 (Docker Community Edition, Docker CE) 两种版本。

Docker 安装

Docker EE 是面向企业的,一般只需要使用 Docker CE 即可,我们选择安装 Docker CE 版本。在此以 Ubuntu18.04 为例:

卸载老版本的 Docker

如果安装过老版本的 docker (dockerordocker-engine),要先卸载掉

1
$ sudo apt-get remove docker docker-engine docker.io

安装 Docker

采用官方推荐的通过 Docker‘s repositories 方式来安装。

设置 Docker’s repository

  1. 先更新 apt 索引

    1
    $ sudo apt-get update
  2. 安装必要的包,这是为了 apt 能够通过 HTTPS 的方式访问 Docker repositories

    1
    2
    3
    4
    5
    $ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
  3. 添加 Docker 的官方 GPG key

    1
    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

    确认是否有带有 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 指纹的 key。

    1
    2
    3
    4
    5
    6
    $ sudo apt-key fingerprint 0EBFCD88

    pub 4096R/0EBFCD88 2017-02-22
    Key fingerprint = 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
    uid Docker Release (CE deb) <docker@docker.com>
    sub 4096R/F273FCD8 2017-02-22
  4. 安装 stable 版本的 repository。如果想安装 edgetest 版本,在 stable 后面添加 edgetest

    1
    2
    3
    4
    $ sudo add-apt-repository \
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable"

安装 Docker CE

  1. 先更新 apt 索引

    1
    $ sudo apt-get update
  2. 安装最新版的 Docker CE

    1
    $ sudo apt-get install docker-ce
    • 如果想安装特定版本 Docker CE,则可以列出 repo 中可用的版本,选择安装

    a. 列出 repo 中可用版本
    bash ​ $ apt-cache madison docker-ce ​ docker-ce | 18.03.0~ce-0~ubuntu | https://download.docker.com/linux/ubuntu xenial/stable amd64 Packages ​

    b. 安装特定版本

    1
    $ sudo apt-get install docker-ce=<VERSION>
  3. 确定是否安装成功,下载一个测试镜像并在容器中运行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $ sudo docker run hello-world
    Hello from Docker!
    This message shows that your installation appears to be working correctly.

    To generate this message, Docker took the following steps:
    1. The Docker client contacted the Docker daemon.
    2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
    3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
    4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

    To try something more ambitious, you can run an Ubuntu container with:
    $ docker run -it ubuntu bash

    Share images, automate workflows, and more with a free Docker ID:
    https://hub.docker.com/

    For more examples and ideas, visit:
    https://docs.docker.com/get-started/

    如果出现如上信息就安装成功了,如果是在 linux 下安装的还需要设置用户权限。

设置用户权限(linux 安装)

Docker daemon 进程与 Unix socket 绑定。默认状况下 Unix socket 权限由 root 用户拥有,其他用户必须用 sudo 才可以访问。Docker daemon 必须以 root 用户身份运行。想要省掉输入 sudo 就必须设置当前用户权限。

我们可以创建一个 docker 用户组并把当前用户加到这个组里面去,Docker daemon 启动时就创建一个可以被 docker 用户组访问的 Unix socket。

  1. 创建 docker 用户组

    1
    $ sudo groupadd docker
  2. 把当前用户加入 docker 用户组

    1
    $ sudo usermod -aG docker $USER
  3. 重新登陆当前用户来使用户组生效

  4. 此时就可以不加 sudo 运行 docker

    1
    $ docker run hello-world

设置 Docker 开机启动

Ubuntu16.04 开始都用 systemd 来管理开机启动服务。如果 Ubuntu14.10 以下则使用 upstart

Ubuntu16.04 及以上 - systemd

1
2
$ sudo systemctl enable docker// 设置 docker 开机启动 
$ sudo systemctl disable docker// 取消 docker 开机启动

Ubuntu14.10 及以下 - upstart

Docker 被 upstart 自动配置为开机启动,取消开机启动则使用下面的命令

1
$ echo manual | sudo tee /etc/init/docker.override

Docker image

Docker 容器 (container) 是通过运行镜像 (image) 文件来生成的。image 是一个可执行的包,它包含了运行一个应用程序所需要的所有东西 — 运行时、库、环境变量和配置文件。一个 image 可以生成多个同时运行的 container。

Docker container

Docker 容器是 image 的一个运行时实例。image 被执行并被加载到内存就变成一个 container,也就是一个具有状态的 image 或用户进程。可以通过 docker ps 来查看正在运行的 container 的列表。

容器与虚拟机的区别

一个容器在 Linux 上 原生 运行,并于其他容器共享主机的内核。容器运行轻量级的独立的进程,占据资源相当少。而虚拟机则运行在一个客户操作系统 (Guest OS) 上,它通过虚拟机控制器技术来虚拟访问访问主机的资源,相对其他的进程,虚拟机要占据较多的机器资源。

Docker 启动服务

Docker 是 C/S 架构,运行 docker 命令时,相当于把传递指令给监听特定 Unix socket 的 Docker 服务,所以必须启动 Docker 服务。

1
2
3
4
5
# service 方式 
$ sudo service docker start

# systemctl 方式
$ sudo systemctl start docker

实战 - 构建一个 app 并创建 docker 镜像。

如果我们想要开发一个 python app,第一步就是安装一个 python 的 runtime 到开发机上,而这个配置必须完美契合 app 的配置要求,生产环境也必须这样。

使用 Docker,就可以把所需 Python runtime 打包为一个镜像 (image),无需安装。然后在 app 的代码包含这个 image 即可。我们就算把 app 的环境打包好了。

步骤 1: 通过 Dockerfile 来定义容器 (container)

Dockerfile 定义了 container 内的行为。在 container 环境中,所有对环境资源的访问,例如网络接口和磁盘驱动,都被虚拟化了,并与 container 外的操作系统隔离。因此需要定义到容器外的端口,并确定想要 “拷贝” 到这个环境的文件。做完这个以后,就可以确保在 Dockerfile 中运行的 app 在任何外部环境都能完全相同地运行。

创建 Dockerfile

1
2
3
cd ~/Workspace/dockerdemo
touch Dockerfile
vim Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ~/Workspace/dockerdemo/Dockerfile
# 用一个官方的 Python runtime 作为父 image
FROM python:2.7-slim

# 设置工作目录为 /app
WORKDIR /app

# 把当前的目录内容拷贝到 /app 中的容器
COPY . /app

# 安装 requirements.txt 定义的依赖包
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# 把 80 端口对容器外开放
EXPOSE 80

# 定义环境变量
ENV NAME World

# 容器启动时运行 app.py
CMD ["python", "app.py"]

Dockerfile 被构建为为一个 image,COPY命令把requirements.txtapp.py拷贝到容器内,EXPOSE 命令让app.py` 的输出可以通过 HTTP 来访问。

步骤 2: 构建示例 demo

编写 demo app 代码

编写 requirements.txtapp.py,把它们放在 Dockerfile 的同一目录下。

requirements.txt

1
2
Flask
Redis

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

至此 app 就编写完毕了。我们可以看到 pip install -r requirements.txt 这个命令安装了 Flask 和 Redis,而 app 打印了环境变量 NAMEsocket.gethostname () 的输出。由于 Redis 安装之后并没有运行,所以预期 app 会运行失败,打印错误信息。

构建 app

查看当前目录的文件

1
2
$ ls
Dockerfile app.py requirements.txt

运行 build 命令,建立 Docker image,我们也可以用 -t 选项来为之创建一个标签。

1
docker build -t friendlyhello .

我们可以查看注册的 image

1
2
3
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
friendlyhello latest cfb6345425ed 41 seconds ago 132MB

运行 app

docker run 命令运行 app,-p 选项把机器上的 4000 端口绑定到 container 发布的 80 端口

1
2
3
4
5
6
7
docker run -p 4000:80 friendlyhello
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:80/ (Press CTRL+C to quit)

显示信息访问 http://0.0.0.0:80。但实际上,这条信息来自于容器内部,容器内部的 app 并不知道它正运行于容器内部,所以它在实体机上显示虚拟环境的地址 http://0.0.0.0:80,实际上我们已经把容器的 80 端口映射到了物理机的 4000 端口,所以真实地址是 http://localhost:4000

点击 CTRL+C 退出进程。

也可以添加 -d 选项在后台运行

1
2
docker run -d -p 4000:80 friendlyhello
6b514ac64d134b52a35394c82839da468dcb94b56123109bd33113e5fdb36d7b

输入该命令后,得到了一个 container ID,例如上面的 6b514ac64d134b52a35394c82839da468dcb94b56123109bd33113e5fdb36d7b,此时我们的 container 在后台运行。也可以通过 docker container ls 来查看 container ID 的缩写。

1
2
3
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b514ac64d13 friendlyhello "python app.py" 3 minutes ago Up 3 minutes 0.0.0.0:4000->80/tcp compassionate_shannon

运行 docker container stop 命令来停止 container 进程。

1
$ docker container stop 6b514ac64d13

步骤 3: 分享 image

我们想要我们的 image 可以随处运行。我们可以把自己的 image 推送到 Docker 仓库 (repository) 中。Docker 仓库类似于 Github 仓库,只是代码已经被构建好了。可以注册一个 Docker ID 来创建自己的 repository。Docker 命令行默认使用公有仓库。

登陆 Docker 公有仓库

我们需要去 Docker 官网 注册一个 Docker ID,在本机输入

1
$ docker login

接着输入 Docker ID 和密码即可登录。

为 image 添加标签

一个仓库中的 image 的格式是 username/repository:tag。这个 Tag 是可选的,但一般推荐添加上,因为仓库可以用 tag 来设定 Docker image 版本号。

1
$ docker tag image user/repository:tag

例如

1
$ docker tag helloworld maywzh/maywzhrepo:part2

通过 docker image ls 来查看新的添加了标签的 image:

1
2
3
4
5
6
7
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
friendlyhello latest cfb6345425ed 2 days ago 132MB
koa-demo 0.0.1 628b71e7fc7d 2 weeks ago 675MB
hello-world latest 4ab4c602aa5e 3 weeks ago 1.84kB
python 2.7-slim c9cde4658340 4 weeks ago 120MB
node 8.4 386940f92d24 13 months ago 673MB

发布 image

可以把标签过的 image 发布到仓库中:

1
$ docker push username/repository:tag

一旦完成,那么上传的东西就可以被可以被公开访问了,也可以上 Docker Hub 来查看 image 上传的 image。

从远端仓库拉取 image

经过之前的步骤,我们可以通过 docker run 在任何机器上来运行我们的 app

1
$ docker run -p 4000:80 username/repository:tag

如果 image 在本机不可用,就会把它拉回到本地:

1
$ docker run -p 4000:40 

总结

Docker 提供了一种虚拟化容器的方式来打包 app 以及它的运行环境,该容器 (container) 通过镜像 (image) 创建,而镜像通过编写 dockerfile 的方式来定义。我们也可以把为 docker 镜像添加标签,并推送到 docker 远程仓库中。这样就可以通过远程仓库直接运行镜像。

附录 所用的所有命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker build -t friendlyhello .  # 用 dockerfile 来构建镜像 
docker run -p 4000:80 friendlyhello # 运行 friendlyhello 把容器端口 80 导航到到运行机端口 4000
docker run -d -p 4000:80 friendlyhello # 后台运行模式
docker container ls # 列出所有的容器
docker container ls -a # 列出所有的容器包括为未运行的
docker container stop <hash> # 优雅的方式关闭容器
docker container kill <hash> # 强制杀掉容器进程
docker container rm <hash> # 删除容器
docker container rm $(docker container ls -a -q) # 删除所有容器
docker image ls -a # 列出所有的镜像
docker image rm <image id> # 删除特定镜像
docker image rm $(docker image ls -a -q) # 删除所有镜像
docker login # 登陆到 docker CLI session
docker tag <image> username/repository:tag # 为镜像添加标签来上传到公有库
docker push username/repository:tag # 上传镜像到公有库
docker run username/repository:tag # 从仓库运行镜像