如何瘦身docker镜像

容器化部署已经成为现在的主流,打包 docker 镜像已经是现代前端开发的必修课了。最近正好在做新项目的时候处理过相关事情,所以记录一下。

# docker 部署的需求

首先我们需要使用 docker 完成网站源代码的编译,并且还要自己使用 expressjs (opens new window) 搭建服务器。让网站可以运行,docker 跑起来之后只用负责端口转发即可。

大家都知道前端网站编译需要安装很多依赖,但这些依赖在网站运行的时候并不需要。那如何才能做到,编译之后丢弃这些不用的依赖呢? 我一开始尝试主动删除,但发现并没有减少 docker 的大小。

答案是Use multi-stage builds (opens new window)

# 编译网站

首先我们需要安装依赖并编译网站,dockerfile 内容如下:

FROM node:14.17.5-buster-slim as build
ARG GITHUB_PACKAGES_TOKEN
RUN apt-get update && apt-get install -y --no-install-recommends autoconf automake g++ libpng-dev make

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
COPY package.json yarn.lock /tmp/
RUN echo $GITHUB_PACKAGES_TOKEN
RUN cd /tmp && yarn install --forzen-lockfile --production=false
RUN mkdir -p /app && mv /tmp/node_modules /app
# From here we load our application's code in, therefore the previous docker
# "layer" thats been cached will be used if possible
WORKDIR /app
COPY . /app
ENV NODE_ENV production
RUN yarn build && yarn express:build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的命令编译了网站和 express 服务端代码,这里选择的 node 官方镜像并不是精简版,因为编译的时候需要很多依赖,精简版的话反而安装系统依赖更花时间。

# 安装依赖

网站运行并不需要 node_modules 但是 express 服务器是需要的,所以我们需要安装后端的依赖。

FROM node:14.17.5-alpine3.14 as deps

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
COPY server/package.json server/yarn.lock /tmp/
RUN cd /tmp && yarn install --forzen-lockfile --production=false
1
2
3
4
5
6

服务端的 package.json 文件我放在了 server 文件夹下,服务端依赖的东西很少,所以不太占用空间。

# 打包实际生成的文件

FROM node:14.17.5-alpine3.14
WORKDIR /app
COPY . /app
COPY --from=deps /tmp/node_modules ./node_modules/
COPY --from=build /app/dist ./dist/
COPY --from=build /app/server ./server/
1
2
3
4
5
6

接下来的步骤很简单了, 选择占用空间最小的 Alpine Linux 然后把编译好的文件复制进来就可以了。前两个步骤的所有东西都会被丢弃掉。所以实际生成的 docker 镜像会非常小。

# 总结

采用多阶段编译,可以有效的减少编译的依赖对空间的占用,做到最小化 docker 镜像。

# 参考

3 simple tricks for smaller Docker images (opens new window)

如何检测用户启动广告屏蔽插件

广告屏蔽插件让我们作为用户可以减少广告的打扰,但是作为开发者有时候因为用户开启了广告屏蔽导致网站收入减少或者影响了一部分功能的使用。我们不得不提示用户关闭广告插件,以正常使用网站。 但是针对不同的需求,广告的检测方式也不一样。我们接下来详细讲一讲。

# 网站本身投放广告

针对网站本身有投放广告的情况下,我们可以通过 JavaScript 来检查广告的 HTML 元素是否存在,以及 CSS 属性来判断。示例代码如下:

function isDisplayNone(node: Element | null): boolean {
  if (!node) {
    return false;
  }
  if (getComputedStyle(node).display === 'none') return true;
  return isDisplayNone(node.parentElement);
}

function isHidden(node: Element) {
  if (getComputedStyle(node).visibility === 'hidden') return true;
  if (isDisplayNone(node)) return true;

  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

通过检测广告元素是否被删除或者隐藏可以非常可靠的提示用户关闭广告插件或者其他操作。

# 网站由于广告屏蔽导致功能不全

例如由于用户开启广告屏蔽插件,导致支付服务 stripe 无法正常使用。我们本身并没有在网站上发布广告。这时候可以使用网络请求的方式来检测广告屏蔽插件。示例代码如下:

  checkAdBlocker() {
   fetch('https://pagead2.googlesyndication.com/pagead/show_ads.js', {
     mode: 'no-cors'
   }).catch(() => {
     console.log('Please disable ad blockers');
   })
  }
1
2
3
4
5
6
7

原理很简单,广告屏蔽插件一定会阻止加载谷歌的 JavaScript 代码来屏蔽广告的执行,只要我们使用 fetch 方法来请求这个 JavaScript 文件,如果出错那就一定是使用了广告屏蔽插件。 所以同样的道理,如果因为有些图片的路径带上了 ad 之类的关键字导致被屏蔽,我们也可以使用这个方法来进行检测。记住这里需要开启 no-cors 模式,因为 JavaScript 的网络请求是受到同源策略的严格控制的。

# 总结

广告屏蔽插件的原理基本有三种,第一是有列表黑名单,出现在列表黑名单的资源都会直接被中断网络请求,例如无法加载图片,JavaScript 文件等等。第二是关键字匹配,如果资源文件里面有 ad 之类的相关广告关键字,那这个资源也会被中断请求。第三个是 DOM 检测,针对已经渲染好的 HTML 文件,通过 class 属性来检测是否有相关的广告关键字。如果符合结果,就删除相应的元素。

# 参考

FuckAdBlock (opens new window)

ELK收集Docker项目的日志

现在主流的项目部署都是采用 docker 部署了,一般项目使用 STDOUT 输出的日志都被 docker 来收集和保存。我们需要使用著名的 ELK (opens new window) 来分析日志,所以如何部署 ELK 并且把 docker 的日志转发给 ELK 呢? 这就是本篇文章我们需要解决的问题。

# 搭建 ELK

既然我们使用 docker 进行部署,那我们当然也要使用 docker 部署 ELK。docker-elk (opens new window) 是一个配置好的项目,我们可以基于这个项目迅速搭建起一个 ELK 项目。说实话, ELK 项目非常消耗内存,所以至少保证机器有 4GB 内存,否则可能会宕机。搭建好 ELK 之后,记得按照教程重新设置密码。

$ docker-compose exec -T elasticsearch bin/elasticsearch-setup-passwords auto --batch
1

初始化密码之后,并修改 kibanalogstash 的配置文件更新密码。最后再重启这两个服务:

$ docker-compose restart kibana logstash
1

# 使用 GELF 收集日志

GELF (opens new window) 是一个经典的日志格式并且 docker 原生支持这个格式,首先我们需要配置 logstash 采取这个格式来收集日志。

修改 logstash/pipeline/logstash.conf 文件的 input 部分:

input {
	beats {
		port => 5044
	}

	gelf {
		port => 5000
	}
}
1
2
3
4
5
6
7
8
9

这样收集 gelf 的端口就是 5000 默认使用 UDP 格式。修改配置之后别忘了重启 logstash

$ docker-compose restart logstash
1

接下来实际项目要把日志转发到 ELK 上,如果我们使用 docker-compose.yml 需要在当前的 service 上添加 logging 相关的配置:

logging:
  driver: gelf
  options:
    gelf-address: 'udp://0.0.0.0:5000'
1
2
3
4

如果你的 ELK 和 实际项目并不是在同一台主机上,记得修改 0.0.0.0 成相应的主机名或 IP 地址。这样配置之后就可以在 kibana 上查看收集到的日志了。 不过要记得配置 Index patterns 然后在 Discover 界面上就能看到日志记录了。

# 总结

ELK 收集 docker 日志并不是很复杂,但是摸索的时候很难调试,例如 ELK 配置了 gelf 方法收集日志,但却找不到好的方法来测试这个接口是否正常。 我只是通过 nmap 命令来检测 5000 端口是否开放,并不知道是否正常运行。

扫描当前主机的 UDP 端口:

nmap -sU 0.0.0.0
1

启动项目的 docker 的时候还出现了警告信息 warning: no logs are available with the 'gelf' log driver 为了这个问题我查了半天,但发现 这个信息即使出现了也可以正常收集。浪费了不少时间。最终的解决方案总是很简单,但查找的过程中难免绕弯路。

# 参考

https://ibm-cloud-architecture.github.io/b2m-nodejs/logging/ (opens new window)

Using Free Let’s Encrypt SSL/TLS Certificates with NGINX (opens new window)