Skip to content

单页面应用

使用vite 快速创建一个 vue 项目

sh
pnpm create vite

Dockerfile

Dockerfile
FROM nginx:1.15.2-alpine
COPY /docs/.vitepress/dist /usr/share/nginx/html

COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
ENTRYPOINT ["nginx","-g","daemon off;"]
  • 把打包出来的 /docs/.vitepress/dist 文件复制到容器内的 /usr/share/nginx/html
  • 把 nginx 的配置文件nginx.conf复制到容器内的 /etc/nginx/nginx.conf

将命令通过以下几步翻译为一个 Dockerfile:

  1. 选择一个基础镜像。由于需要在容器中执行构建操作,我们需要 node 的运行环境,因此 FROM 选择 node。
  2. 将以上几个脚本命令放在 RUN 指令中。
  3. 启动服务命令放在 CMD 指令中。
dockerfile
FROM node:14-alpine

WORKDIR /code

ADD . /code
RUN yarn && npm run build

CMD npx serve -s build
EXPOSE 3000

构建完成。然而还可以针对以下两点进行优化。

  1. 构建镜像时间过长,优化构建时间
  2. 构建镜像文件过大,优化镜像体积

构建时间优化:构建缓存

我们注意到,一个前端项目的耗时时间主要集中在两个命令:

  1. pnpm i
  2. pnpm run build

在本地环境中,如果没有新的 npm package 需要下载,不需要重新 npm i。

那 Docker 中是不也可以做到这一点?

在 Dockerfile 中,对于 ADD 指令(官方文档)来讲,如果添加文件内容的 checksum 没有发生变化,则可以利用构建缓存Best practices for writing Dockerfiles)。

而对于前端项目而言,如果 package.json/pnpm-lock.yaml 文件内容没有变更,则无需再次 pnpm i

package.json/pnpm-lock.yaml 事先置于镜像中,安装依赖将可以获得缓存的优化,优化如下。

dockerfile
FROM node:16-alpine as builder

WORKDIR /app

# 单独分离 package.json,是为了安装依赖可最大限度利用缓存
ADD package.json pnpm-lock.yaml* /app/

# 此时,yarn 可以利用缓存,如果 yarn.lock 内容没有变化,则不会重新依赖安装
RUN yarn global add pnpm && pnpm i --frozen-lockfile;

ADD . /app
RUN pnpm run build

CMD npx serve -s build
EXPOSE 3000

进行构建时,若可利用缓存,可看到 CACHED 标记。

bash
$ docker-compose up --build
...
 => CACHED [builder 2/6] WORKDIR /code                                                                            0.0s
 => CACHED [builder 3/6] ADD package.json yarn.lock /code/                                                        0.0s
 => CACHED [builder 4/6] RUN yarn                                                                                 0.0s
...

构建体积优化:多阶段构建

我们的目标是提供静态服务(资源),完全需要依赖于 node.js 环境进行服务化。node.js 环境在完成构建后即完成了它的使命,它的继续存在会造成极大的资源浪费。

我们可以使用多阶段构建进行优化,最终使用 nginx 进行服务化。

  1. 第一阶段 Node 镜像:使用 node 镜像对单页应用进行构建,生成静态资源。
  2. 第二阶段 Nginx 镜像:使用 nginx 镜像对单页应用的静态资源进行服务化。
dockerfile
FROM node:16-alpine as builder

WORKDIR /app

# 单独分离 package.json,是为了安装依赖可最大限度利用缓存
ADD package.json pnpm-lock.yaml* /app/

# 此时,yarn 可以利用缓存,如果 yarn.lock 内容没有变化,则不会重新依赖安装
RUN yarn global add pnpm && pnpm i --frozen-lockfile;

ADD . /app
RUN pnpm run build

# 选择更小体积的基础镜像
FROM nginx:alpine
COPY --from=builder app/dist /usr/share/nginx/html

启动容器

我们将 Dockerfile 命名为 simple.Dockerfile

该 docker compose 配置位于 cra-deploy/simple.Dockerfile

bash
version: "3"
services:
  simple:
    build:
      context: .
      dockerfile: simple.Dockerfile
    ports:
      - 4000:80

使用 docker-compose up --build simple 启动容器。

创建镜像

sh
$ docker build --rm -f Dockerfile -t docker-react:latest .
[+] Building 0.4s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                   0.0s
 => => transferring dockerfile: 190B                                   0.0s
 => [internal] load .dockerignore                                      0.0s
 => => transferring context: 2B                                        0.0s
 => [internal] load metadata for docker.io/library/nginx:1.15.2-alpin  0.0s
 => [1/3] FROM docker.io/library/nginx:1.15.2-alpine                   0.0s
 => [internal] load build context                                      0.0s
 => => transferring context: 2.33kB                                    0.0s
 => [2/3] COPY ./index.html /var/www                                   0.1s
 => [3/3] COPY /docker/nginx/nginx.conf /etc/nginx/nginx.conf          0.1s
 => exporting to image                                                 0.0s
 => => exporting layers                                                0.0s
 => => writing image sha256:fab2f97f0c3f4adcf994cd7be5f9f8b566a584582  0.0s
 => => naming to docker.io/library/web

创建容器

sh
docker run --rm -d -p 80:80 docker-react:latest

目录结构如下

├── Dockerfile
└── docker
    └── nginx
        └── nginx.conf

配置 nginx

创建 nginx 配置文件 nginx.conf

nginx
# auto detects a good number of processes to run
worker_processes auto;

#Provides the configuration file context in which the directives that affect connection processing are specified.
events {
    # Sets the maximum number of simultaneous connections that can be opened by a worker process.
    worker_connections 8000;
    # Tells the worker to accept multiple connections at a time
    multi_accept on;
}

http {
    # what times to include
    include       /etc/nginx/mime.types;
    # what is the default one
    default_type  application/octet-stream;

    # Sets the path, format, and configuration for a buffered log write
    log_format compression '$remote_addr - $remote_user [$time_local] '
        '"$request" $status $upstream_addr '
        '"$http_referer" "$http_user_agent"';

    server {
        # listen on port 80
        listen 80;
        # save logs here
        access_log /var/log/nginx/access.log compression;

        # where the root here
        root /var/www;
        # what file to server as index
        index index.html index.htm;

        location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to redirecting to index.html
            try_files $uri $uri/ /index.html;
        }

        # Media: images, icons, video, audio, HTC
        location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
          expires 1M;
          access_log off;
          add_header Cache-Control "public";
        }

        # JavaScript and CSS files
        location ~* \.(?:css|js)$ {
            try_files $uri =404;
            expires 1y;
            access_log off;
            add_header Cache-Control "public";
        }

        # Any route containing a file extension (e.g. /devicesfile.js)
        location ~ ^.+\..+$ {
            try_files $uri =404;
        }
    }
}

nginx 镜像

sh
docker run -it --rm -p 3000:80 nginx:alpine

默认配置文件位于 /etc/nginx/nginx.conf

nginx
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

默认配置文件位于 /etc/nginx/conf.d/default.conf

nginx
server {
    listen       80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}