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"]
简介
我必须得说, docker 我用得并不多,所以下面的介绍,只是我的理解,可能和正确说法有偏差 =.=
先说容器化这个概念
举个例子,当不使用容器化时,比如你要在服务器上部署一个 node 项目,那么服务器上首先得安装一个 node 吧。如果还需要部署别的啥项目,也可能会需要配置相应的环境。
而当你使用了容器化后,我们代码需要的运行环境也包含在容器中了,所以只用在服务器上启动这个容器就行了。
这样你可以在任意支持运行容器的环境去运行你构建的容器。同时容器和容器之间也获得了隔离,比如你在某个容器里面执行
rm -rf /
应该并不会让服务器挂掉了?(别试,我只是猜猜,如果你有自己随便玩的云服务器你就试吧)docker 是一个用来实现容器化的工具
我们把代码和需要的运行环境构建为一个镜像(docker build),然后推送到公共/私有镜像仓库(docker push),然后在服务器上拉取这个镜像(docker pull),然后运行这个镜像(docker run)。
服务器上只要安装了 docker ,那么就可以运行镜像了,不需要再考虑不同代码对运行环境的不同要求,因为运行环境已经包含在镜像中了。这样省掉了部署应用时对环境的考虑,使部署工作比较统一和简单。这个特性也很天然地利于横向扩展。
再说一下会很常见的镜像(image)和容器(container)这两个概念。
有点像 JS 中的类和实例。运行一个镜像其实也就是启动了一个镜像的实例,这个实例我们叫做容器。基于一个镜像可以启动多个容器,启动时也允许不同的参数。
现在再来看 docker 的这个图标,是不是感觉很形象了。
鲸鱼就是镜像仓库,一个个集装箱就是镜像。我们做好镜像,放到仓库,需要用的时候从仓库拿出来。集装箱中已经包含了镜像的运行环境,所以我们拿到镜像后,直接运行就行了。
直接使用官方镜像
docker 本身有提供很多官方镜像在 Docker Hub 上,有些场景我们直接使用官方镜像就行了。有些场景我们需要构建一个自定义镜像时,也一般是基于一个官方镜像来的。
举例一:使用 ubuntu 镜像来学习 linux 命令
比如当你想学习/测试一些 linux 命令时,你可以先
docker pull ubuntu
来拉取一个 ubuntu 镜像,然后docker run -ti --rm ubuntu bash
来启动一个 ubuntu 镜像的实例。-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
来做一个端口映射,我们才能访问到页面。停止和移除容器的命令分别是
docker stop xxx
和docker 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 缓存给清理掉来进一步减小镜像体积。这里注意一下,为什么要先
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 对应的阶段才会生成镜像。目的就是为了尽量减小镜像体积。我们在前面的阶段进行安装依赖、打包代码等,然后在最后一个阶段,只把必要的代码拷贝进来。
使用自定义的 nginx 镜像来学习 HTTP 缓存
HTTP 缓存是通过设置 HTTP header 来实现的,我们需要在 nginx 镜像基础上安装 nginx-extras ,然后就可以通过 more_set_headers 语法来设置 header 了。
所以这里我们的 Dockerfile 很简单,如下:
然后执行
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
来启动。另外,当你需要启动多个服务时,也可以配置在
docker-compose.yml
中,这样启动时会启动配置的所有服务。也可以很方便停止和销毁所有服务。通过docker compose -h
查看所有命令。其它一些资料