2024-08-31 13:14:00
最近准备把部署在 Cloudflare, Vercel, Netlify 上的项目迁移到自己的 VPS 通过 Docker 运行,就复习了一下 Docker 镜像打包。 但是一个很小的项目打包出来就是 1.05GB, 这显然是不能接受的。所以研究了一下 Node.JS 项目 Docker 镜像最小化打包方案, 将镜像大小从 1.06GB 缩小到了 135 MB。
示例项目是一个 Astro 项目, 使用 Vite 作为构建工具, SSR 模式运行。
主要思路是使用最小化系统镜像,选用 Alpine Linux 镜像。
按照 Astro 官方文档服务端渲染模式(SSR), 将基础镜像替换为 node:lts-alpine, NPM 替换为 PNPM, 打包出来的体积是 1.06 GB。 也就是最差的状态。
FROM node:lts-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY . .
RUN pnpm install --frozen-lockfile
RUN export $(cat .env.example) && pnpm run build
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
docker build -t v0 .
[+] Building 113.8s (11/11) FINISHED docker:orbstack
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 346B 0.0s
=> [internal] load metadata for docker.io/library/node:lts-alpine 1.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 89B 0.0s
=> [1/6] FROM docker.io/library/node:lts-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 240.11kB 0.2s
=> CACHED [2/6] RUN corepack enable 0.0s
=> CACHED [3/6] WORKDIR /app 0.0s
=> [4/6] COPY . . 2.0s
=> [5/6] RUN pnpm install --frozen-lockfile 85.7s
=> [6/6] RUN export $(cat .env.example) && pnpm run build 11.1s
=> exporting to image 13.4s
=> => exporting layers 13.4s
=> => writing image sha256:653236defcbb8d99d83dc550f1deb55e48b49d7925a295049806ebac8c104d4a 0.0s
=> => naming to docker.io/library/v0
主要思路是先安装生产环境依赖,产生第一层。 再安装全量依赖,打包生成 JavaScript 产物,产生第二层。 最后将生产环境依赖和 JavaScript 产物复制到运行环境。
按照 多层构建(使用 SSR) 的方案, 将镜像大小缩小到了 306MB,缩小不小,但是这个方案有个缺点,需要明确的制定生产依赖,如果少指定了生产依赖,运行时会报错。
FROM node:lts-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM build-deps AS build
COPY . .
RUN export $(cat .env.example) && pnpm run build
FROM base AS runtime
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
docker build -t v1 .
[+] Building 85.5s (15/15) FINISHED docker:orbstack
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 680B 0.0s
=> [internal] load metadata for docker.io/library/node:lts-alpine 1.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 89B 0.0s
=> [base 1/4] FROM docker.io/library/node:lts-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 0.0s
=> [internal] load build context 0.3s
=> => transferring context: 240.44kB 0.2s
=> CACHED [base 2/4] RUN corepack enable 0.0s
=> CACHED [base 3/4] WORKDIR /app 0.0s
=> [base 4/4] COPY package.json pnpm-lock.yaml ./ 0.2s
=> [prod-deps 1/1] RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile 35.1s
=> [build-deps 1/1] RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 65.5s
=> [runtime 1/2] COPY --from=prod-deps /app/node_modules ./node_modules 5.9s
=> [build 1/2] COPY . . 0.8s
=> [build 2/2] RUN export $(cat .env.example) && pnpm run build 7.5s
=> [runtime 2/2] COPY --from=build /app/dist ./dist 0.1s
=> exporting to image 4.2s
=> => exporting layers 4.1s
=> => writing image sha256:8ae6b2bddf0a7ac5f8ad45e6abb7d36a633e384cf476e45fb9132bdf70ed0c5f 0.0s
=> => naming to docker.io/library/v1
主要思路是将 node_modules 内联进 JavaScript 文件,最终只复制 JavaScript 文件到运行环境。
之前看 Next.JS 的时候,记得可以将 node_modules 内联进 JavaScript 文件,这样就不需要 node_modules 了。 所以就研究了一下,发现 Vite SSR 也是支持的,所以判断 Docker 环境就使用内联的方式,不需要复制 node_modules ,只复制最终的 dist 产物,将镜像大小缩小到 135MB 了。
打包脚本改动:
vite: {
ssr: {
noExternal: process.env.DOCKER ? !!process.env.DOCKER : undefined;
}
}
最终的 Dockerfile 如下:
FROM node:lts-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# FROM base AS prod-deps
# RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM build-deps AS build
COPY . .
RUN export $(cat .env.example) && export DOCKER=true && pnpm run build
FROM base AS runtime
# COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
docker build -t v2 .
[+] Building 24.9s (13/13) FINISHED docker:orbstack
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 708B 0.0s
=> [internal] load metadata for docker.io/library/node:lts-alpine 1.7s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 89B 0.0s
=> [base 1/4] FROM docker.io/library/node:lts-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 0.0s
=> [internal] load build context 0.3s
=> => transferring context: 240.47kB 0.2s
=> CACHED [base 2/4] RUN corepack enable 0.0s
=> CACHED [base 3/4] WORKDIR /app 0.0s
=> CACHED [base 4/4] COPY package.json pnpm-lock.yaml ./ 0.0s
=> CACHED [build-deps 1/1] RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 0.0s
=> [build 1/2] COPY . . 1.5s
=> [build 2/2] RUN export $(cat .env.example) && export DOCKER=true && pnpm run build 15.0s
=> [runtime 1/1] COPY --from=build /app/dist ./dist 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:0ed5c10162d1faf4208f5ea999fbcd133374acc0e682404c8b05220b38fd1eaf 0.0s
=> => naming to docker.io/library/v2
最终对比,体积从 1.06GB 降低到 135MB, 构建时间从 113.8s 降低到 24.9s
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
v2 latest 0ed5c10162d1 5 minutes ago 135MB
v1 latest 8ae6b2bddf0a 6 minutes ago 306MB
v0 latest 653236defcbb 11 minutes ago 1.06GB
示例项目是开源的,可以在 GitHub 查看。
2024-08-11 17:18:12
之前一直在 X 分享一些有趣的工具,而且也会同步到 Telegram Channel 上。看到 Austin 说准备建立一个网站,把分享内容都收录进去。 刚好想到最近看到的一个模板 Sepia,就想到把 Telegram Channel 转成微博客。
难度不大,主功能一个周末就搞完了。过程中做到了浏览器端 0 JS, 分享一些有趣的技术点:
防剧透模式和移动端搜索框隐藏展示,使用的 CSS ":checked 伪类" 和 "+ 紧邻兄弟组合器" 实现的,参考
过渡动画使用的 CSS View Transitions, 参考
图片灯箱用的 HTML 的 popover 属性,参考
返回顶部的展示和隐藏使用 CSS animation-timeline 实现 Chrome 115 以上版本专属,参考
多图瀑布流布局,使用 grid 布局实现,参考
访问统计使用一个 1px 的透明图片做 LOGO 背景,上古技术了,现在几乎没有访问统计软件支持了
禁止浏览器端 JS 运行,使用的 Content-Security-Policy 的 script-src 'none' 参考
搞完以后,就直接开源了,很意外有那么多人喜欢,一周就收获了 800+ 的 star 了。
如果你也有兴趣,可以去 GitHub 上看看。
2024-07-29 21:39:55
最近在准备给家用网络升级 2.5G, 在咸鱼收了一台阿里云 AP8220 来折腾。 但是这个机器的刷机资料太少了。折腾了2天才成功刷机。所以写一篇文档记录一下。
非必要,不刷机。
刷机有风险,请做好刷砖准备。
任何刷机问题与我无关,我只分享我的刷机过程。
使用 USB 转 Console 线连接到设备 Console 口, 网线连接到设备 LAN 口。
Putty 设置链接 COM3(取决于你电脑设备USB口,自己去设备管理看下), 波特率设置 115200
。
设备通电,立即按键盘的 shift+@
中断启动,即可进入 Uboot, 不行就多试几次。
电脑设置静态IP: 192.168.10.1
下载文件 mibib.bin 文件和 tftp32.exe 放入同目录,打开 tftp32 后,切换到 192.168.10.1
。
在 Putty 开始敲命令:
tftpboot mibib.bin
nand device 1
nand erase 0x50000 0x10000
nand write $fileaddr 0x50000 $filesize
刷完以后,断电。
设备重新通电,进入 Uboot。
将固件以 factory.bin 结尾的文件命名为 ap8220.bin
放入 tftp32.exe 同目录。
在 Putty 开始敲命令刷写固件:
tftpboot ap8220.bin
flash rootfs
set boot3 "set mtdparts mtdparts=nand0:0x8000000@0x0(fs)"
set boot4 "ubi part fs && ubi read 42000000 kernel"
set setup1 "partname=1 && setenv bootargs ubi.mtd=rootfs ${args_common}"
set setup2 "partname=2 && setenv bootargs ubi.mtd=rootfs ${args_common}"
saveenv
刷完以后,断电。
重新通电等待后可正常启动。
电脑改回 DHCP 获取 IP 地址, 访问 http://192.168.1.1 进入 Web 界面。
在 Web 界面升级刷入 sysupgrade.bin 文件。
刷机完成。
这个机器的刷机资料很少,我想建一个微信群交流。有兴趣的加我微信 ccbikai
。
2024-07-07 22:48:53
之前做 L(O*62).ONG 的第一版时,使用的服务端跳转,上线第二天就被 Google 警告了安全风险,只能改成本地跳转提醒后跳转再去申诉。
对于这种场景最好的做法是使用 Google Safe Browsing 来做跳转,但是 Safe Browsing 有使用限制,每天只能调用 10000 次,而且不支持自定义名单。由于我只想依赖 Cloudflare 一个平台就没有使用 Google Safe Browsing。
前段时间和一个网友交流的时候,突然脑洞大开,想到使用带成人和非法网站过滤的安全 DNS 服务器来做域名安全性的检查。 于是使用 家庭版 1.1.1.1 做了一下尝试发现是可行的。 但是 1.1.1.1 不支持自定义名单,想到之前在 HomeLab 用过 Cloudflare Zero Trust Gateway 就去研究了一下,发现了这个几乎完美的方案。
Cloudflare Zero Trust 提供的 Gateway 自带 DNS(DoH) 服务器,而且可以配置防火墙规则,支持按域名风险等级、内容分类、自定义名单等多种规则屏蔽解析。而且它收集的数据源来自 Cloudflare 专有数据、30 多个开放情报源、机器学习模型、社区反馈等,可以说是相当齐全了。 更多的细节可以在官方文档查看。
我屏蔽了高风险分类、成人、赌博、政府、少儿不宜、新注册等高风险的域名,并且手动维护了一些黑名单和白名单。
配置完成以后就可以得到一个 DoH 地址:
我们使用下面的代码接入到项目中:
async function isSafeUrl(
url,
DoH = "https://family.cloudflare-dns.com/dns-query"
) {
let safe = false;
try {
const { hostname } = new URL(url);
const res = await fetch(`${DoH}?type=A&name=${hostname}`, {
headers: {
accept: "application/dns-json",
},
cf: {
cacheEverything: true,
cacheTtlByStatus: { "200-299": 86400 },
},
});
const dnsResult = await res.json();
if (dnsResult && Array.isArray(dnsResult.Answer)) {
const isBlock = dnsResult.Answer.some(
answer => answer.data === "0.0.0.0"
);
safe = !isBlock;
}
} catch (e) {
console.warn("isSafeUrl fail: ", url, e);
}
return safe;
}
Cloudflare Zero Trust 管理面板也提供了一个可视化的界面,方便查屏蔽情况。可以看到一些成人网站和新注册域名都被屏蔽了。
如果域名被封了,还可以在日志查看原因。
2024-07-07 21:28:51
最近在学习 AI 相关的前端知识,看到 Transformers.js 的一个实例很感兴趣就把它做成了一个工具。
通过在 WebWorker 中使用 Transformers.js 调用 WebGPU 运行 RMBG-1.4 模型,可以在浏览器本地使用 AI 移除图片背景。在 M1 PRO 上处理一张 4K 图片只需要 500ms。
工具地址:https://html.zone/background-remover
相关的源码在 https://github.com/xenova/transformers.js/tree/main/examples/remove-background-client ,如果想自己实现,可以参考这个仓库。提醒一下向调用 WebGPU,需要切换到 Transformers.js V3 版本。