Docker简介和使用Docker快速搭建开发环境

这篇文章不是一个面面俱到的docker文档,只很浅显的介绍了docker的一些基本概念和如何用docker快速搭建开发环境来节省时间,并且在后面的测试和发布中保持环境的高度一致。

Docker 概念

Docker是开发人员和系统管理员使用容器开发,部署和运行应用程序的平台。 使用Linux容器来部署应用程序称为集装箱化。

镜像(Image)

如我们所知,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu 16.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

容器(Container)

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。

容器和虚拟机的区别

容器在Linux上原生的运行,并与其他容器共享主机的内核。 它运行一个独立的进程,相比其他可执行文件没有占用更多的内存,使其轻量级。

相比之下,虚拟机(VM)运行一个完整的“客户”操作系统,通过管理程序虚拟访问主机资源。 一般来说,虚拟机提供的环境比大多数应用程序所需的资源更多。

镜像的定制

docker commit

docker commit可以定制镜像,但是并不推荐使用。更多的是用来理解、docker 镜像的分层存储。

  • 拉取 nginx 镜像

我们先使用docker pull拉取一个nginx镜像:

docker pull nginx

然后我们使用docker images列出镜像可以看到我们刚pull的nginx镜像

  • 使用这个镜像创建一个容器

使用docker run创建容器:

docker run --name webserver -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。

  • 修改容器内容

假设我们要换掉这个欢迎页面,我们可以使用 docker exec 命令进入容器,修改其内容。

docker exec -it webserver bash
echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exit

再刷新浏览器,发现内容已经改变:

我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动。

# docker diff webserver

C /root
A /root/.bash_history
C /run
A /run/nginx.pid
C /usr/share/nginx/html/index.html
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
  • 保存成新的镜像

现在我们定制好了变化,需要将其保存下来形成镜像。

当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

# docker commit \
    --author "tunan" \
    --message "edited web page" \
    webserver \
    nginx:v2
sha256:c8792e8d71576f2a16602ecb0707c294709163ab4905b8e8ae37a1bacd08ed76

这时再列出镜像就可以看到我们刚刚通过docker commit 创建的镜像了。

  • 慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

首先,如果仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。

此外,使用 docker commit意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然 docker diff 或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。

  • 编写Dockerfile

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile:

FROM nginx
RUN echo '<h1>Hello, World!</h1>' > /usr/share/nginx/html/index.html
  • 构建镜像

执行命令docker build -t nginx:v3 .:

docker build -t nginx:v3 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 3f8a4339aadd
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in c32537314da3
Removing intermediate container c32537314da3
 ---> 887ce4889404
Successfully built 887ce4889404
Successfully tagged nginx:v3

再次列出所有镜像,发现新镜像已经构建完成:

COPY和ADD的区别

刚使用Dockerfile的时候看到有人在Dockerfile中用ADD,有人用COPY,难免会产生疑惑,这两个区别在哪里?

ADD是带有高级功能的COPY,如需要自动解压文件等,如果不需要使用这些高级功能,我们尽量使用COPY

RUN vs CMD vs ENTRYPOINT

RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。

CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。

ENTRYPOINT 配置容器启动时运行的命令。

Compose

我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

例子

Nodejs+Express+MongoDB+PM2

这个项目是靶场的后端API,使用Nodejs+Express提供服务并连接MongoDB数据库,通过PM2运行项目,项目结构大致是这样:

如果我们使用传统的开发环境搭建方法,我们一般这样做:

  1. 安装nodejs
  2. 安装npm
  3. 安装MongoDB
  4. 配置MongoDB
  5. 启动mongod
  6. 安装pm2

这还是在对node和mongo极其熟悉且不走弯路的前提下。

那么换用docker做开发环境,我们在搭建基本项目目录后基本只需要做两步:

  • 编写Dockerfile文件

因为默认的nodejs镜像没有PM2,我们使用Dockerfile定制镜像:

FROM node
# Install PM2
RUN npm install -g pm2
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install package for project first
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app
# expose 4000 port
EXPOSE 4000
CMD pm2 start --no-daemon  server.js
  • 编写docker-compose.yml文件

定制完镜像,我们使用docker-compose将node服务和MongoDB连接起来

app:
  build: .
  volumes:
    - .:/usr/src/app/
  links:
    - mongo
  ports:
    - 4000:4000

mongo:
  image: mongo
  volumes:
      - ./data:/data/db
  ports:
      - "127.0.0.1:27017:27017"

然后直接在项目目录中运行docker-compose up,就可以完美运行本项目:

Python+Flask+Mongo

这个例子是个todo list,展示如何快速搭建Python+Flask+Mongo的开发环境

  • Dockerfile:
FROM python:2.7
COPY . /todo
WORKDIR /todo
RUN pip install -r requirements.txt
  • docker-compose:
web:
  build: .
  command: python -u app.py
  ports:
    - "5000:5000"
  volumes:
    - .:/todo
  links:
    - mongo
mongo:
  image: mongo
  volumes:
    - ./data:/data/db
  • 运行docker-compose up

使用docker开发的一些建议

容器应该是短暂的

通过 Dockerfile 构建的镜像所启动的容器应该尽可能短暂(生命周期短)。「短暂」意味着可以停止和销毁容器,并且创建一个新容器并部署好所需的设置和配置工作量应该是极小的。

使用 .dockerignore 文件

使用 Dockerfile 构建镜像时最好是将 Dockerfile 放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。为了提高构建镜像的效率,你可以在目录下新建一个 .dockerignore 文件来指定要忽略的文件和目录。.dockerignore 文件的排除模式语法和 Git 的 .gitignore 文件相似。

一个容器只运行一个进程(单一职责)

应该保证在一个容器中只运行一个进程。将多个应用解耦到不同容器中,保证了容器的横向扩展和复用。

如果容器互相依赖,建议使用docker-compose

尽量避免容器内发生频繁的文件读写

强烈建议使用 VOLUME 来管理镜像中的可变部分和用户可以改变的部分,如数据库文件,存储的图片文件,频繁修改的代码等。

Comments
Write a Comment