用 Podman 部署 Node.js 应用

记录一下把 Node.js 应用容器化部署到服务器的完整流程。

为什么选 Podman

Podman 是无守护进程(daemonless)的容器运行时,根用户也无需运行后台服务。对单机部署来说更轻量、更安全。

Podman vs Docker 主要差异

维度 Docker Podman
守护进程 需要 dockerd 无
rootless 实验性 一等公民
CLI 兼容 - 大部分兼容
systemd 需额外配置 原生集成

部署架构

我们这个博客采用如下结构:

Nginx (反代 :80 / :443)
   ↓
blog-app 容器 (:3000)
   ↓
posts/ 目录 (bind mount,只读)

关键设计

  • 绑定挂载 posts/:这样改 Markdown 文件不需要重建镜像
  • 只读挂载:容器不能修改文章,保证安全
  • 独立网络:博客与现有 factcheck 应用互不干扰

Dockerfile 解析

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
COPY lib ./lib
COPY views ./views
COPY public ./public
EXPOSE 3000
CMD ["node", "server.js"]

各层含义

  1. 基础镜像:node:20-alpine 体积小(~50MB),足够支撑生产
  2. 先复制依赖文件:利用 Docker 层缓存,改代码不重装依赖
  3. npm ci --omit=dev:生产模式安装,跳过 devDependencies
  4. 复制源码:最后才复制,这样改代码只重建最后一层
  5. EXPOSE 3000:声明端口,只是文档作用,真正发布靠 podman run -p

启动命令

podman run -d \
  --name blog-app \
  -p 3000:3000 \
  -v /home/kay/blog/posts:/app/posts:ro \
  --restart=unless-stopped \
  localhost/blog:latest

参数说明

  • -d:后台运行
  • --name blog-app:容器命名,方便后续管理
  • -p 3000:3000:端口映射,宿主 3000 → 容器 3000
  • -v ... :ro:bind mount,只读,改 Markdown 文件立即生效
  • --restart=unless-stopped:除非手动 stop,否则自动重启

常见问题

容器启动后访问不到

检查端口是否真的监听在 0.0.0.0 而非 127.0.0.1。Node.js 启动必须用 0.0.0.0。

Markdown 修改后没生效

如果是 bind mount 部署,改文件立即生效(无需重启);如果是 COPY 部署,需要 podman build 重建镜像。

内存占用过高

Node.js 默认 RSS 上限较高,可加 --max-old-space-size=512 限制堆内存。

总结

Podman + bind mount 是个人/小团队部署 Node.js 应用的极佳组合。轻量、安全、迭代快。