findxc / blog

88 stars 5 forks source link

docker 入门 #56

Open findxc opened 3 years ago

findxc commented 3 years ago

简介

我必须得说, docker 我用得并不多,所以下面的介绍,只是我的理解,可能和正确说法有偏差 =.=

先说容器化这个概念

容器化 是指将软件代码和所需的所有组件(例如库、框架和其他依赖项)打包在一起,让它们隔离在自己的“容器”中。

举个例子,当不使用容器化时,比如你要在服务器上部署一个 node 项目,那么服务器上首先得安装一个 node 吧。如果还需要部署别的啥项目,也可能会需要配置相应的环境。

而当你使用了容器化后,我们代码需要的运行环境也包含在容器中了,所以只用在服务器上启动这个容器就行了。

这样你可以在任意支持运行容器的环境去运行你构建的容器。同时容器和容器之间也获得了隔离,比如你在某个容器里面执行 rm -rf / 应该并不会让服务器挂掉了?(别试,我只是猜猜,如果你有自己随便玩的云服务器你就试吧)

docker 是一个用来实现容器化的工具

我们把代码和需要的运行环境构建为一个镜像(docker build),然后推送到公共/私有镜像仓库(docker push),然后在服务器上拉取这个镜像(docker pull),然后运行这个镜像(docker run)。

服务器上只要安装了 docker ,那么就可以运行镜像了,不需要再考虑不同代码对运行环境的不同要求,因为运行环境已经包含在镜像中了。这样省掉了部署应用时对环境的考虑,使部署工作比较统一和简单。这个特性也很天然地利于横向扩展。

再说一下会很常见的镜像(image)和容器(container)这两个概念。

An image typically contains a union of layered filesystems stacked on top of each other. An image does not have state and it never changes.

A container is a runtime instance of a docker image.

有点像 JS 中的类和实例。运行一个镜像其实也就是启动了一个镜像的实例,这个实例我们叫做容器。基于一个镜像可以启动多个容器,启动时也允许不同的参数。

现在再来看 docker 的这个图标,是不是感觉很形象了。

73F1746F-D683-49CF-9339-59D96F6FEBC5

鲸鱼就是镜像仓库,一个个集装箱就是镜像。我们做好镜像,放到仓库,需要用的时候从仓库拿出来。集装箱中已经包含了镜像的运行环境,所以我们拿到镜像后,直接运行就行了。

直接使用官方镜像

docker 本身有提供很多官方镜像在 Docker Hub 上,有些场景我们直接使用官方镜像就行了。有些场景我们需要构建一个自定义镜像时,也一般是基于一个官方镜像来的。

举例一:使用 ubuntu 镜像来学习 linux 命令

比如当你想学习/测试一些 linux 命令时,你可以先 docker pull ubuntu 来拉取一个 ubuntu 镜像,然后 docker run -ti --rm ubuntu bash 来启动一个 ubuntu 镜像的实例。

DADEB571-B5D7-48E3-9BE0-7E1F11F462BF

-i 是指启动容器后还可以继续交互, -t 是指分配一个终端,比如这里命令末尾我们指定了终端是 bash

--rm 是指当你在容器里面 exit 来退出容器时,这个容器会自动停止和移除掉。

docker ps 是查看当前运行的容器。 docker ps -a 是查看所有容器,包括因为异常停止的容器。

举例二:使用 nginx 镜像来学习 nginx

nginx 镜像 介绍文档里有一些使用示例。

使用 nginx 时,一般会涉及到配置两个东西,一个是 nginx 自身的配置文件,比如监听什么端口,某个路径代理到哪里去等待,再一个就是前端代码路径。

docker run 提供了 -v absolute_local_path:absolute_container_path 参数来把本地路径挂载到容器中去,比如当启动一个数据库容器时,数据库的数据就需要存在本地,这样当销毁容器时,数据也仍然保留在服务器上。

这里我们还是先 docker pull nginx ,然后 docker run -d --name my-nginx -p 8080:80 nginx 来启动一个 nginx 容器。启动后浏览器访问 http://localhost:8080 能看到一个 nginx 的默认页面。

-d 表示让容器在后台运行。 nginx 容器默认监听的端口是 80 ,这里需要加上 -p 8080:80 来做一个端口映射,我们才能访问到页面。

227F1DDD-E97C-45AC-8A8F-091F6F234DC9

停止和移除容器的命令分别是 docker stop xxxdocker rm xxx

当启动了一个容器,想要进入这个容器里面时,命令是 docker exec -it my-nginx bash 。有些容器里面没有 bash ,就把 bash 替换为 sh 就好了。

我们把刚才启动的 nginx 先停止并移除掉,然后用挂载前端代码目录的方式来启动: docker run -d --name my-nginx -p 8080:80 -v /Users/xc/project/tmp/nginx-hello/static:/usr/share/nginx/html nginx 。这时候访问 http://localhost:8080 看到的就是 static 中 html 对应的界面了,如果修改了 static 中代码,刷新下页面就行。

-v 配置的路径需要用绝对路径,可以通过 pwd 来查看当前路径。 /usr/share/nginx/html 这个路径是 nginx 配置默认使用的路径。

自定义构建一个镜像

实际使用时,更多的场景是我们需要自定义构建一个镜像。我们会根据项目编写一个 Dockerfile ,然后通过 docker build 来进行构建镜像。

给一个 node 项目写 Dockerfile

docker 文档上有写 --> Build your Node image | Docker Documentation

Dockerfile 内容如下。这个只是一个能用的 Dockerfile ,并不是一个最优的,比如在 RUN npm install --production 这一步还可以把 npm 缓存给清理掉来进一步减小镜像体积。

# 基于 node:12.18.1 来构建我们自己的镜像
FROM node:12.18.1
# 设置 NODE_ENV=production 是因为有些 npm 包在生产模式下性能会好点
ENV NODE_ENV=production

# 镜像中工作路径是 /app ,下面的 COPY 就会拷贝到这个路径下
WORKDIR /app

# 把当前路径下的 package.json 和 package-lock.json* 拷贝到镜像的 ./ 下
COPY ["package.json", "package-lock.json*", "./"]

# 安装依赖,这里只安装了生产需要的依赖,是为了减少镜像体积
RUN npm install --production

# 把当前路径下所有文件拷贝到镜像的 . 下, . 也就是 ./
# 这里如果有 .dockerignore ,拷贝时会忽略 ignore 中的文件
COPY . .

# 当 docker run 这个镜像时会执行的命令
CMD [ "node", "server.js" ]

这里注意一下,为什么要先 COPY ["package.json", "package-lock.json*", "./"] 然后安装依赖,然后再 COPY . . 呢,和先 COPY . . 然后再安装依赖有什么区别?

因为镜像是一层一层的,如果缓存中已经存在这一层就可以直接使用缓存。如果某一层变化了,那么接下来的层都会重新构建。

由于项目的依赖一般不会变化,所以 COPY ["package.json", "package-lock.json*", "./"]RUN npm install --production 这两层一般会可以走缓存。

而如果是先 COPY . . 再安装依赖,那么业务代码每次变更,都会需要重新安装依赖,缓存的命中率会大大降低。

给一个 Next.js 项目写 Dockerfile

Next.js 文档上有写 --> Deployment | Next.js

小技巧:当你想给某个项目写 Dockerfile 时,可以先看看官方文档上有没有,有就可以借鉴一下。

Dockerfile 内容如下。这里面用到了 多阶段构建 ,简单理解就是写了多个 FROM ,前面的 FROM 都只是准备工作,最后一个 FROM 对应的阶段才会生成镜像。目的就是为了尽量减小镜像体积。我们在前面的阶段进行安装依赖、打包代码等,然后在最后一个阶段,只把必要的代码拷贝进来。

# 这里是基于 node:alpine 镜像来制作自己的镜像,镜像的 alpine 版本是最精简的
FROM node:alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Rebuild the source code only when needed
FROM node:alpine AS builder
WORKDIR /app
COPY . .
# 这里 --from=deps 就是从上一个阶段拷贝需要的东西过来
COPY --from=deps /app/node_modules ./node_modules
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline

# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

USER nextjs

EXPOSE 3000

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
# ENV NEXT_TELEMETRY_DISABLED 1

CMD ["yarn", "start"]

使用自定义的 nginx 镜像来学习 HTTP 缓存

HTTP 缓存是通过设置 HTTP header 来实现的,我们需要在 nginx 镜像基础上安装 nginx-extras ,然后就可以通过 more_set_headers 语法来设置 header 了。

所以这里我们的 Dockerfile 很简单,如下:

FROM nginx
RUN apt-get update && apt-get install -y nginx-extras

然后执行 docker build -t nginx-with-extras:v1 . 来构建镜像。默认会使用当前路径下的 Dockerfile 进行构建,也可以通过 -f 来指定其它的文件。命令末尾的 . 是指一个构建的 context ,当需要拷贝文件到镜像中时就主要注意这个路径了。 -t 是指镜像的名字和 tag 。

然后我们先 docker run -d --name my-nginx nginx 来启动一个 nginx 容器,然后 docker cp my-nginx:/etc/nginx/conf.d ./ 把容器中默认的配置拷贝出来,这时候就可以在 conf.d/default.conf 中加上比如 more_set_headers 'Cache-Control: max-age=60'; 来设置缓存有效期为 60s 。

然后再用挂载配置的方式启动一个基于 nginx-with-extras 镜像的容器: docker run -d --name my-nginx -p 8080:80 -v /Users/xc/project/tmp/nginx-hello/static:/usr/share/nginx/html -v /Users/xc/project/tmp/nginx-hello/conf.d:/etc/nginx/conf.d nginx-with-extras:v1

这时候再访问 http://localhost:8080 ,应该从控制台就能看到请求的资源的 Response Headers 中有一行 Cache-Control: max-age=60 了。(测试 HTTP 缓存用 .js 或者 .css 来测试会更符合预期, html 文件浏览器可能会自行加额外 header ,比如谷歌请求 html 时 Request Headers 中始终会有 Cache-Control: max-age=0

后面如果有修改 conf.d ,需要 docker restart my-nginx 才会生效。

完整的代码示例见 GitHub - findxc/http-cache-example: use nginx to learn how http cache works

docker compose

上面自定义的 nginx 镜像,启动命令很长一串,因为需要映射端口,需要挂载文件,对于这种参数比较多的,我们可以把命令写在 docker-compose.yml 中,然后通过 docker compose up -d 来启动。

services:
  web:
    container_name: my-nginx
    build: .
    ports:
    - 8080:80
    volumes:
    - ./static:/usr/share/nginx/html
    - ./conf.d:/etc/nginx/conf.d

另外,当你需要启动多个服务时,也可以配置在 docker-compose.yml 中,这样启动时会启动配置的所有服务。也可以很方便停止和销毁所有服务。通过 docker compose -h 查看所有命令。

其它一些资料