Skip to content

树莓派 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 官方提供了多架构镜像,树莓派 4Barm64 架构,需要拉取对应的版本:

bash
docker pull arm64v8/nginx:stable

这条命令会从 Docker Hub 拉取 Nginxarm64 稳定版镜像。拉取完成后可以验证架构:

bash
docker inspect arm64v8/nginx:stable | grep Architecture

输出应为 "Architecture": "arm64"

1.2 速度异常的排查与解决

首次拉取时,我遇到了一个意料之外的问题:千兆下行宽带,拉取一个不到 200MB 的镜像,竟然花了 12 分钟。

这个速度显然不正常。原因在于 Docker 默认从 Docker Hub 官方源拉取镜像,而国内网络环境下,Docker HubIPv4 连接极不稳定,IPv6 更是经常超时。虽然第2篇我们已经配置了国内镜像源,但当时配置的是三个公共源(daocloud、1ms、xuanyuan),这些公共源对免费用户通常有速率限制,实际速度远达不到宽带上限。

解决方法是使用阿里云专属镜像加速器。阿里云为每个用户提供一个免费的专属加速地址,速度和稳定性远优于公共源。获取方式如下:

  1. 登录阿里云控制台,进入容器镜像服务
  2. 在左侧导航栏选择"镜像工具 → 镜像加速器"
  3. 页面中会显示一个专属地址,格式如 https://xxxx.mirror.aliyuncs.com

拿到地址后,编辑 Docker 配置文件:

bash
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:

bash
sudo systemctl daemon-reload
sudo systemctl restart docker

验证配置是否生效:

bash
docker info | grep -A5 "Registry Mirrors"

确认阿里云专属地址出现在列表首位后,删除旧镜像并重新拉取:

bash
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 挂载:

bash
sudo mkdir -p ~/docker/nginx/{conf,html,logs,ssl}

各目录用途:

目录用途
conf/Nginx 站点配置文件(替代旧专栏的 /etc/nginx/sites-enabled/
html/静态网页文件
logs/访问日志和错误日志
ssl/SSL 证书文件(后续 HTTPS 配置时使用)

3. 准备静态文件

创建一个测试页面,用于验证容器化后的 Nginx 是否正常工作:

bash
sudo nano ~/docker/nginx/html/index.html

填入以下内容(可根据你的站点信息修改):

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 站点配置文件:

bash
sudo nano ~/docker/nginx/conf/default.conf

填入以下内容:

nginx
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 容器

bash
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 双栈
-vvolume 挂载,宿主机路径:容器内路径:权限(ro 为只读)

6. 验证运行

查看容器运行状态:

bash
docker ps

输出应显示 rpi-nginx 容器正在运行,端口映射包含 0.0.0.0:80->80/tcp, [::]:80->80/tcp,说明 IPv4 和 IPv6 均已监听。

在树莓派本地验证:

bash
curl http://localhost

应返回第3步准备的页面内容。

通过浏览器访问:

bash
# 查看树莓派 IP 地址
hostname -I

通过浏览器访问 http://树莓派IP,看到第3步准备的页面即表示 Nginx 容器化成功。

7. 用 Compose 文件管理容器启动

前面我们用一条很长的 docker run 命令启动了 Nginx 容器。这条命令虽然能工作,但有两个问题:

  • 参数太多,容易输错
  • 下次重装系统或迁移时,需要重新回忆这条命令

Docker Compose 可以解决这个问题。把容器的所有配置写在一个 docker-compose.yml 文件里,后续只需要一行命令就能启动。

~/docker/nginx/ 目录下创建 docker-compose.yml

bash
sudo nano ~/docker/nginx/docker-compose.yml

写入以下内容:

yaml
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)。
保存退出后,有两种方式启动:

方式一:先切换到配置文件所在目录

bash
cd ~/docker/nginx
docker compose up -d

停止:

bash
docker compose down

方式二:直接用 -f 参数指定配置文件路径

bash
docker compose -f ~/docker/nginx/docker-compose.yml up -d

停止:

bash
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 内核模块

bash
sudo modprobe br_netfilter
echo "br_netfilter" | sudo tee -a /etc/modules

第一条命令立即加载模块,第二条命令确保开机自动加载。

方案二:更新树莓派固件和系统包

bash
sudo rpi-eeprom-update
sudo apt update && sudo apt full-upgrade -y

确保树莓派的固件和 Wi-Fi 驱动是最新版本,避免已知的驱动 bug。

3.4 验证

执行完方案一后,重启树莓派并确认模块已加载:

bash
lsmod | grep br_netfilter

如果输出中包含 br_netfilter,说明模块已正常加载。之后观察一段时间,死机问题应该不再出现。

总结

本篇完成了容器化改造的第一个应用组件迁移:

项目旧专栏做法容器化做法
Nginx 安装apt install nginxdocker pull arm64v8/nginx
配置文件位置/etc/nginx/sites-enabled/volume 挂载 ~/docker/nginx/conf/
静态文件位置/var/www/html/volume 挂载 ~/docker/nginx/html/
启动方式systemctl start nginxdocker compose up -d
开机自启systemctl enable nginxrestart: always
版本升级apt upgrade拉取新镜像,重建容器

Nginx 容器化之后,它的配置、日志、静态文件都与宿主机解耦,全部集中在 ~/docker/nginx/ 目录下。后续如果需要迁移到另一台机器,只需复制这个目录,再执行 docker compose up -d 即可。

此外,本篇还记录了一个意外收获:树莓派在 Docker 网络负载下可能触发 Wi-Fi 驱动的假死问题,解决方案是加载 br_netfilter 内核模块并更新固件。这个坑与 Docker 本身无关,但在树莓派上跑容器时很容易遇到,值得留意。

下一篇预告:将 ddns-go 从二进制 + systemd 迁移到 Docker 容器运行,实现域名解析的容器化管理。