树莓派 Docker 化网页服务器(3):Nginx 容器化 —— 从 apt 到 Docker 的第一步
前言
前两篇我们完成了两件事:建立了容器化的认知框架,制作了带 Docker 底座的纯净系统镜像。现在你手上的树莓派,烧录即得 Docker 运行环境,随时可以跑容器。
这一篇,我们把 旧专栏第4篇 的核心成果—— Nginx 静态页服务——从 apt install nginx 迁移到 Docker 容器中运行。这是整个容器化改造的第一个应用组件,也是最基础的一步。 Nginx 容器化之后,后续的 ddns-go、acme.sh、frpc 都会遵循类似的模式:拉取镜像 → 挂载配置 → 端口映射 → 验证运行。
一、容器化原理
裸机 Nginx
在旧专栏里,Nginx 是这样运行的:
宿主机系统(Raspberry Pi OS)
↓
apt install nginx
↓
systemctl start nginx
↓
Nginx master 进程(以 root 或 www-data 用户运行)
↓
Nginx worker 进程(读取 /etc/nginx/ 下的配置)Nginx 直接跑在宿主机系统上,它的配置文件、日志文件、静态文件都散落在系统的各个目录中。如果换一台机器,你需要重新 apt install,重新复制配置,重新设置开机自启。
容器 Nginx
而在 Docker 里,Nginx 是这样运行的:
docker run arm64v8/nginx:stable
│
▼
┌─────────────────────────────────────────────┐
│ Docker 引擎 (dockerd) │
│ │
│ 1. 检查本地是否有 arm64v8/nginx:stable 镜像 │
│ ↓ 没有则从镜像源拉取 │
│ 2. 从镜像创建一层可写容器层(Copy-on-Write) │
│ ↓ │
│ 3. 分配容器网络(默认 bridge) │
│ ↓ │
│ 4. 挂载 volume(宿主机路径 → 容器内路径) │
│ ↓ │
│ 5. 启动容器进程(Nginx master 进程) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 容器内部(隔离环境) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Nginx │ │ Nginx │ │
│ │ Master │ → │ Worker │ → 处理 HTTP 请求│
│ │ Process │ │ Processes│ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 文件系统(来自镜像) │ │
│ │ /etc/nginx/nginx.conf │ │
│ │ /usr/share/nginx/html/ │ │
│ │ /var/log/nginx/ │ │
│ └──────────────────────────────────────┘ │
│ ↑ ↑ │
│ │ volume │ volume │
│ ┌────┴──────────────┴──────────────────┐ │
│ │ 宿主机 ~/docker/nginx/ │ │
│ │ conf/ → /etc/nginx/conf.d/ │ │
│ │ html/ → /usr/share/nginx/html/ │ │
│ │ logs/ → /var/log/nginx/ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 端口映射 │
│ 宿主机 :80 → 容器内 :80 │
│ 宿主机 :443 → 容器内 :443 │
│ (iptables NAT 规则,由 Docker 自动配置) │
└─────────────────────────────────────────────┘关键机制
1. 镜像层是只读的
arm64v8/nginx:stable 镜像里包含了 Nginx 二进制、默认配置、依赖库,所有这些都以只读层存在,不能被修改。无论你从这个镜像启动多少次容器,镜像本身都不会变化。
2. 容器层是可写的
Docker 在镜像层之上创建一层薄薄的可写层,Nginx 运行时产生的日志、临时文件都写在这里。但这层是临时的——容器删除后,这层也随之消失。
3. volume 是持久化通道
配置文件、静态文件、日志这些需要持久化的数据,通过 volume 挂载到宿主机目录。容器删了,volume 里的数据还在。这就是“容器无状态,数据有归宿”的核心思想。
4. 端口映射是 iptables 转发
iptables 是 Linux 内核的包过滤框架,Docker 用它自动完成端口转发,无需手动配置防火墙规则。-p 80:80 参数让 Docker 在宿主机上添加一条 iptables 规则,将发往宿主机 80 端口的流量转发到容器的 80 端口。IPv6 同理,通过 ip6tables 实现。
5. 进程隔离
容器内的 Nginx 看不到宿主机的其他进程,也看不到其他容器的进程。它以为自己运行在一台独立的机器上。
二、操作步骤
有了前面的原理铺垫,现在开始进行实际操作。
1. 拉取 Nginx 镜像
1.1 执行拉取命令
Nginx 官方提供了多架构镜像,树莓派 4B 是 arm64 架构,需要拉取对应的版本:
docker pull arm64v8/nginx:stable这条命令会从 Docker Hub 拉取 Nginx 的 arm64 稳定版镜像。拉取完成后可以验证架构:
docker inspect arm64v8/nginx:stable | grep Architecture输出应为 "Architecture": "arm64"。
1.2 速度异常的排查与解决
首次拉取时,我遇到了一个意料之外的问题:千兆下行宽带,拉取一个不到 200MB 的镜像,竟然花了 12 分钟。
这个速度显然不正常。原因在于 Docker 默认从 Docker Hub 官方源拉取镜像,而国内网络环境下,Docker Hub 的 IPv4 连接极不稳定,IPv6 更是经常超时。虽然第2篇我们已经配置了国内镜像源,但当时配置的是三个公共源(daocloud、1ms、xuanyuan),这些公共源对免费用户通常有速率限制,实际速度远达不到宽带上限。
解决方法是使用阿里云专属镜像加速器。阿里云为每个用户提供一个免费的专属加速地址,速度和稳定性远优于公共源。获取方式如下:
- 登录阿里云控制台,进入容器镜像服务
- 在左侧导航栏选择"镜像工具 → 镜像加速器"
- 页面中会显示一个专属地址,格式如
https://xxxx.mirror.aliyuncs.com
拿到地址后,编辑 Docker 配置文件:
sudo tee /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": [
"https://<你的ID>.mirror.aliyuncs.com",
"https://docker.m.daocloud.io",
"https://docker.1ms.run",
"https://docker.xuanyuan.me"
],
"ipv6": false,
"ip6tables": false
}
EOF注意:JSON 数组中每个地址之间必须用逗号分隔,漏写逗号会导致 Docker 解析失败,配置不生效。
将<你的ID>替换为阿里云控制台看到的前缀
重载配置并重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker验证配置是否生效:
docker info | grep -A5 "Registry Mirrors"确认阿里云专属地址出现在列表首位后,删除旧镜像并重新拉取:
docker stop rpi-nginx
docker rm rpi-nginx
docker rmi arm64v8/nginx:stable
docker pull arm64v8/nginx:stable这次拉取耗时从 12 分钟缩短到了 3 分钟,速度提升了 4 倍。如果你的宽带更好,阿里云专属加速器理论上可以将时间压缩到 1 分钟以内。
2. 创建目录结构
在宿主机上创建 Nginx 配置和数据目录,用于 volume 挂载:
sudo mkdir -p ~/docker/nginx/{conf,html,logs,ssl}各目录用途:
| 目录 | 用途 |
|---|---|
conf/ | Nginx 站点配置文件(替代旧专栏的 /etc/nginx/sites-enabled/) |
html/ | 静态网页文件 |
logs/ | 访问日志和错误日志 |
ssl/ | SSL 证书文件(后续 HTTPS 配置时使用) |
3. 准备静态文件
创建一个测试页面,用于验证容器化后的 Nginx 是否正常工作:
sudo nano ~/docker/nginx/html/index.html填入以下内容(可根据你的站点信息修改):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>jiejinx · 树莓派</title>
<style>
body{font-family:sans-serif;max-width:640px;margin:40px auto;padding:0 20px;line-height:1.6}
h1{color:#333}
.info{background:#f5f5f5;padding:16px;border-radius:8px;font-family:monospace;word-break:break-all}
</style>
</head>
<body>
<h1>🌲 jiejinx 的树莓派</h1>
<p>如果你看到这页,Nginx + IPv6 直连成功。</p>
<hr>
<h2>你的访问 IPv6</h2>
<div class="info" id="ip">正在获取…</div>
<script>
fetch('https://api6.ipify.org?format=json')
.then(r=>r.json())
.then(d=>ip.textContent='你的 IPv6:'+d.ip)
.catch(()=>ip.textContent='获取失败(可能通过 IPv4 访问)');
</script>
</body>
</html>保存退出(Ctrl+O→ Enter→ Ctrl+X)。
4. 编写 Nginx 配置文件
创建 Nginx 站点配置文件:
sudo nano ~/docker/nginx/conf/default.conf填入以下内容:
server {
listen 80;
listen [::]:80;
root /usr/share/nginx/html;
index index.html;
server_name _;
location / { try_files $uri $uri/ =404; }
}保存退出(Ctrl+O→ Enter→ Ctrl+X)。
关于 root 路径:这里的
root是容器内的路径,不是宿主机路径。宿主机上的~/docker/nginx/html通过 volume 挂载到了容器内的/usr/share/nginx/html,所以配置文件中必须写容器内的路径。如果写成~/docker/nginx/html,Nginx 在容器内部找不到这个目录,会返回Connection reset by peer错误。关于文件名后缀:Nginx 在
conf.d/目录下只加载.conf后缀的文件,文件名必须带.conf后缀才会被识别。如果命名为default不带后缀,Nginx 会静默忽略,容器启动后无法访问。
5. 运行 Nginx 容器
docker run -d \
--name rpi-nginx \
--restart always \
-p 80:80 \
-p 443:443 \
-v ~/docker/nginx/conf:/etc/nginx/conf.d:ro \
-v ~/docker/nginx/html:/usr/share/nginx/html:ro \
-v ~/docker/nginx/logs:/var/log/nginx \
-v ~/docker/nginx/ssl:/etc/nginx/ssl:ro \
arm64v8/nginx:stable这条命令的参数说明:
| 参数 | 说明 |
|---|---|
-d | 后台运行 |
--name rpi-nginx | 容器名称,后续管理用这个名字 |
--restart always | 容器退出或宿主机重启时自动启动 |
-p 80:80 -p 443:443 | 宿主机端口映射到容器端口,支持 IPv4 和 IPv6 双栈 |
-v | volume 挂载,宿主机路径:容器内路径:权限(ro 为只读) |
6. 验证运行
查看容器运行状态:
docker ps输出应显示 rpi-nginx 容器正在运行,端口映射包含 0.0.0.0:80->80/tcp, [::]:80->80/tcp,说明 IPv4 和 IPv6 均已监听。
在树莓派本地验证:
curl http://localhost应返回第3步准备的页面内容。
通过浏览器访问:
# 查看树莓派 IP 地址
hostname -I通过浏览器访问 http://树莓派IP,看到第3步准备的页面即表示 Nginx 容器化成功。
7. 用 Compose 文件管理容器启动
前面我们用一条很长的 docker run 命令启动了 Nginx 容器。这条命令虽然能工作,但有两个问题:
- 参数太多,容易输错
- 下次重装系统或迁移时,需要重新回忆这条命令
Docker Compose 可以解决这个问题。把容器的所有配置写在一个 docker-compose.yml 文件里,后续只需要一行命令就能启动。
在 ~/docker/nginx/ 目录下创建 docker-compose.yml:
sudo nano ~/docker/nginx/docker-compose.yml写入以下内容:
services:
nginx:
image: arm64v8/nginx:stable
container_name: rpi-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./conf:/etc/nginx/conf.d:ro
- ./html:/usr/share/nginx/html:ro
- ./logs:/var/log/nginx
- ./ssl:/etc/nginx/ssl:ro保存退出(Ctrl+O→ Enter→ Ctrl+X)。
保存退出后,有两种方式启动:
方式一:先切换到配置文件所在目录
cd ~/docker/nginx
docker compose up -d停止:
docker compose down方式二:直接用 -f 参数指定配置文件路径
docker compose -f ~/docker/nginx/docker-compose.yml up -d停止:
docker compose -f ~/docker/nginx/docker-compose.yml down两种方式效果完全一样。方式一更简洁,方式二更灵活(不需要切换目录)。后续第7篇将所有服务整合到一个 Compose 文件后,这两种方式依然适用。
为什么用 Compose 而不是 docker run? 因为后续的 ddns-go、acme.sh、frpc 都会用同样的方式管理,最终在第7篇我们会把所有服务整合到一个 Compose 文件中,实现一键部署。现在从 Nginx 开始养成习惯,后面就顺了。
三、意外之坑:树莓派频繁死机
3.1 现象
Nginx 容器正常运行后,我在验证环节遇到了一个意料之外的问题:树莓派在两天内频繁死机,绿灯不闪,SSH 断连,只能拔电重启。
一开始以为是偶然,但接连出现两三次后,确定不是巧合。
3.2 排查过程
重启后查看系统日志,没有发现 OOM(内存溢出)或进程崩溃的记录。内存充裕(3.5Gi 可用),Docker 和 Nginx 日志也都正常。
进一步查看内核日志,发现一条关键警告:
bridge: filtering via arp/ip/ip6tables is no longer available by default.
Update your scripts to load br_netfilter if you need this.Docker 创建 docker0 网桥时,需要内核 br_netfilter 模块支持 iptables 规则转发。这个模块默认没有加载,在某些网络条件下(特别是 Wi-Fi + Docker 虚拟网卡同时工作时),可能导致内核网络栈卡死,表现为系统假死、网络完全断开。
3.3 解决方案
方案一:加载 br_netfilter 内核模块
sudo modprobe br_netfilter
echo "br_netfilter" | sudo tee -a /etc/modules第一条命令立即加载模块,第二条命令确保开机自动加载。
方案二:更新树莓派固件和系统包
sudo rpi-eeprom-update
sudo apt update && sudo apt full-upgrade -y确保树莓派的固件和 Wi-Fi 驱动是最新版本,避免已知的驱动 bug。
3.4 验证
执行完方案一后,重启树莓派并确认模块已加载:
lsmod | grep br_netfilter如果输出中包含 br_netfilter,说明模块已正常加载。之后观察一段时间,死机问题应该不再出现。
总结
本篇完成了容器化改造的第一个应用组件迁移:
| 项目 | 旧专栏做法 | 容器化做法 |
|---|---|---|
| Nginx 安装 | apt install nginx | docker pull arm64v8/nginx |
| 配置文件位置 | /etc/nginx/sites-enabled/ | volume 挂载 ~/docker/nginx/conf/ |
| 静态文件位置 | /var/www/html/ | volume 挂载 ~/docker/nginx/html/ |
| 启动方式 | systemctl start nginx | docker compose up -d |
| 开机自启 | systemctl enable nginx | restart: always |
| 版本升级 | apt upgrade | 拉取新镜像,重建容器 |
Nginx 容器化之后,它的配置、日志、静态文件都与宿主机解耦,全部集中在 ~/docker/nginx/ 目录下。后续如果需要迁移到另一台机器,只需复制这个目录,再执行 docker compose up -d 即可。
此外,本篇还记录了一个意外收获:树莓派在 Docker 网络负载下可能触发 Wi-Fi 驱动的假死问题,解决方案是加载 br_netfilter 内核模块并更新固件。这个坑与 Docker 本身无关,但在树莓派上跑容器时很容易遇到,值得留意。
下一篇预告:将 ddns-go 从二进制 + systemd 迁移到 Docker 容器运行,实现域名解析的容器化管理。