用 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"]
各层含义
- 基础镜像:
node:20-alpine体积小(~50MB),足够支撑生产 - 先复制依赖文件:利用 Docker 层缓存,改代码不重装依赖
- npm ci --omit=dev:生产模式安装,跳过 devDependencies
- 复制源码:最后才复制,这样改代码只重建最后一层
- 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 应用的极佳组合。轻量、安全、迭代快。