Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker 入门 #56

Open
findxc opened this issue Oct 11, 2021 · 0 comments
Open

docker 入门 #56

findxc opened this issue Oct 11, 2021 · 0 comments
Labels

Comments

@findxc
Copy link
Owner

findxc commented Oct 11, 2021

简介

我必须得说, 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 查看所有命令。

其它一些资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant