上一篇 下一篇 分享链接 返回 返回顶部

香港服务器KVM与Docker混合部署,网络和存储隔离如何落地?

发布人:Minchunlin 发布时间:2026-10-07 15:08 阅读量:7

香港服务器上落地 KVM 与 Docker 混合部署,建议采用“KVM 宿主机承载虚拟机,Docker 只运行在专用业务虚拟机内”的分层方案。宿主机负责虚拟化、管理网络和虚拟磁盘;业务虚拟机负责 Docker Engine、容器网络和应用数据。这样可以避免 Docker 修改宿主机转发规则,也避免业务容器直接接触宿主机文件系统。

以下方案以一台只有一个公网 IPv4 地址的香港服务器为例:宿主机使用公网地址,虚拟机接入 libvirt 私有 NAT 网络,宿主机通过 Nginx 反向代理将 80/443 请求转发到虚拟机。系统选用 Ubuntu Server 22.04 LTS,宿主机和业务虚拟机均采用同一大版本,Docker 使用 Compose 插件管理应用。示例地址均为文档保留地址或内网地址,实际部署时必须替换为服务商分配的参数。

A5数据提供香港物理服务器租用,覆盖入门建站、Xeon Gold与AMD EPYC等配置,并配备SSD或NVMe存储及CN2、国际带宽等线路方案,可为KVM宿主机、业务虚拟机和Docker应用提供独立的计算、网络与存储资源基础。针对数据库、接口服务、多容器并行运行等场景,也可结合不同内存、磁盘和带宽配置承载相应业务。

一、部署目标和隔离架构

1. 目标环境

本方案的上线目标如下:

  • 宿主机安装 KVM、QEMU 和 libvirt。
  • 创建一台专用业务虚拟机 app-vm。
  • Docker Engine 和 Compose 只安装在 app-vm 内。
  • 宿主机公网网卡不直接桥接给 Docker。
  • 虚拟机使用独立的私有网络地址 10.10.20.10。
  • Docker 使用独立的应用网络和数据库网络。
  • Docker 数据目录挂载到虚拟机独立数据盘 /srv/docker-data。
  • 宿主机只通过 Nginx 向虚拟机转发业务流量,不共享宿主机应用目录。

这种结构的边界如下:

一、部署目标和隔离架构配图

层级示例组件主要职责隔离方式
物理服务器香港机房服务器提供 CPU、内存、磁盘和公网带宽服务商硬件边界
KVM 宿主机Ubuntu Server 22.04管理虚拟机、虚拟网络和虚拟磁盘宿主机权限与 libvirt
业务虚拟机app-vm运行 Docker 和业务服务KVM 虚拟机边界
Docker 前端网络app_frontend暴露需要接收请求的容器Docker bridge 网络
Docker 后端网络app_backend连接应用和数据库internal 内部网络
虚拟机数据盘/srv/docker-data保存镜像层、容器卷和业务数据独立虚拟磁盘和文件系统

KVM 提供的是虚拟机级别的内核隔离,Docker 容器仍然共享业务虚拟机内核。因此,不建议把来源不明、需要强安全边界的容器直接混放在同一台业务虚拟机内。不同租户或不同信任级别的应用,应拆分到不同 KVM 虚拟机。

2. 网络地址规划

示例网络规划如下:

用途地址或接口说明
宿主机公网接口eno1物理网卡,不直接承载容器网络
宿主机公网桥br0宿主机公网地址绑定在此处
libvirt 私有网络virbr10宿主机与虚拟机之间的私有网段
私有网关10.10.20.1宿主机在私有网络中的地址
业务虚拟机10.10.20.10由 DHCP 保留地址分配
Docker 前端网络172.30.10.0/24业务入口容器使用
Docker 后端网络172.30.20.0/24应用和数据库内部通信

如果服务商额外提供多个公网 IP,并明确允许虚拟机使用多个 MAC 地址,也可以把虚拟机接入 br0。但这需要服务商侧放行二层地址或完成额外配置,不能仅在服务器上添加一个 bridge 就认为公网已经可用。

3. 存储隔离原则

存储隔离不等于简单地把所有目录放到一个大分区中。建议至少区分以下对象:

  • 宿主机系统盘:保存宿主机系统和管理工具。
  • 虚拟机系统盘:保存业务虚拟机操作系统。
  • 虚拟机数据盘:保存 Docker 数据和应用持久化数据。
  • 数据库目录:使用独立目录,避免和临时日志混放。
  • 宿主机镜像目录:每台虚拟机使用独立镜像文件,不让 Docker 直接绑定宿主机目录。

不要将宿主机的 /var/lib/docker、应用代码目录或数据库目录直接挂载到虚拟机中。此类共享会降低权限边界,也会让宿主机目录结构和虚拟机内部服务产生耦合。

二、准备条件

1. 硬件和服务商条件

建议准备以下资源:

  • 支持硬件虚拟化的物理服务器或裸金属实例。
  • 至少 4 个逻辑 CPU。
  • 至少 8 GB 内存;同时运行数据库和多个容器时建议从 16 GB 起步。
  • 系统盘之外,预留一块可分配给业务虚拟机的数据盘空间。
  • 服务商允许安装自定义操作系统或提供控制台重装能力。
  • 可以通过带外控制台、VNC 或串口控制台处理网络配置错误。
  • 记录公网 IPv4、子网掩码、默认网关、DNS、SSH 管理来源地址。
  • 确认服务商是否限制额外 MAC 地址、广播、ARP 或桥接流量。

香港服务器的网络交付方式并不完全一致。有的实例使用静态公网地址,有的使用 DHCP,有的要求网关采用特殊掩码。没有确认服务商参数前,不要直接套用下面的公网地址配置。

2. 安装系统和检查基础状态

宿主机使用 Ubuntu Server 22.04 LTS。完成系统安装并通过控制台确认 SSH 可用后,先检查硬件和磁盘:

sudo -i

cat /etc/os-release
uname -r
lscpu | egrep 'Architecture|CPU\(s\)|Virtualization'
free -h
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS
ip -br addr
ip route

如果 lscpu 中没有显示 VT-x 或 AMD-V 等虚拟化能力,继续安装 KVM 可能会失败。使用 libvirt 自带检查工具确认:

apt update
apt install -y cpu-checker

kvm-ok

预期结果类似:

INFO: /dev/kvm exists
KVM acceleration can be used

如果显示 /dev/kvm does not exist,先检查服务商是否提供裸金属环境,以及 BIOS 或实例规格是否开启虚拟化。云服务器的嵌套虚拟化未开启时,不能通过软件命令绕过。

3. 建立配置备份目录

修改网络、libvirt 或 Docker 配置前,先建立备份目录:

install -d -m 0700 /root/deploy-backup
cp -a /etc/netplan /root/deploy-backup/netplan-$(date +%F-%H%M%S)

如果服务器已有业务,不要直接覆盖原有防火墙、网络和容器配置。先记录现状:

systemctl --type=service --state=running
ss -lntup
ufw status verbose 2>/dev/null || true
nft list ruleset > /root/deploy-backup/nft-before.txt 2>/dev/null || true

后续涉及 netplan apply、ufw enable、mkfs、删除容器或重启 Docker 的操作,都应保留当前 SSH 会话,并准备使用服务商控制台回退。

三、安装 KVM、QEMU 和 libvirt

1. 安装宿主机依赖

在宿主机执行:

apt update
apt install -y \
  qemu-kvm \
  qemu-utils \
  libvirt-daemon-system \
  libvirt-clients \
  virtinst \
  bridge-utils \
  ovmf \
  cloud-image-utils

启动并设置 libvirt 开机启动:

systemctl enable --now libvirtd
systemctl status libvirtd --no-pager

确认 KVM 设备和 libvirt 服务:

ls -l /dev/kvm
virsh uri
virsh version
virt-host-validate qemu

virt-host-validate 出现 PASS 表示检查项通过。部分环境可能在 IOMMU、Secure Boot 或权限项显示 WARN,需要结合实际用途判断;如果 QEMU: Checking for hardware virtualization 直接失败,则先不要创建业务虚拟机。

2. 验证宿主机资源

创建虚拟机前,确认资源余量:

free -h
df -hT
df -ih
virsh list --all

示例分配可以是:

  • 宿主机保留 2 个逻辑 CPU和 2 GB 内存。
  • app-vm 分配 4 个 vCPU、8 GB 内存。
  • 虚拟机系统盘 60 GB。
  • 虚拟机 Docker 数据盘 100 GB。

不要把宿主机全部内存分配给虚拟机。宿主机还需要承担 QEMU、libvirt、Nginx、日志和文件缓存开销。

四、配置宿主机公网桥

1. 选择网卡名称

网卡名称不一定是 eno1,先确认实际名称:

ip -br link

下面假设物理网卡为 eno1,宿主机公网地址使用文档示例:

  • 地址:192.0.2.10/24
  • 网关:192.0.2.1
  • DNS:1.1.1.1、8.8.8.8

这些地址不能直接用于生产环境,必须替换为服务商提供的实际参数。

2. 编写 Netplan 配置

先查看现有文件:

ls -l /etc/netplan/
sed -n '1,200p' /etc/netplan/*.yaml

确认没有需要保留的现有配置后,创建桥接配置:

cat >/etc/netplan/01-br0.yaml <<'EOF'
network:
  version: 2
  renderer: networkd
  ethernets:
    eno1:
      dhcp4: false
      dhcp6: false
  bridges:
    br0:
      interfaces:
        - eno1
      addresses:
        - 192.0.2.10/24
      routes:
        - to: default
          via: 192.0.2.1
      nameservers:
        addresses:
          - 1.1.1.1
          - 8.8.8.8
      parameters:
        stp: false
        forward-delay: 0
EOF

如果服务商要求 DHCP,公网部分应改为:

  bridges:
    br0:
      interfaces:
        - eno1
      dhcp4: true
      dhcp6: false
      parameters:
        stp: false
        forward-delay: 0

不要同时在 eno1 和 br0 上配置公网地址。公网地址应绑定到 br0,物理网卡只作为桥成员。

3. 应用并验证网络

远程操作优先使用 netplan try,不要直接使用 netplan apply:

netplan generate
netplan try

netplan try 会在确认超时后自动恢复原配置。确认 SSH、路由和 DNS 均正常后,再执行:

netplan apply

ip -br addr show br0
ip route
ping -c 3 192.0.2.1
getent hosts archive.ubuntu.com

预期结果中,公网地址应出现在 br0,而不是 eno1。如果 SSH 中断,立即通过控制台登录并恢复备份:

rm -f /etc/netplan/01-br0.yaml
cp -a /root/deploy-backup/netplan-备份时间/* /etc/netplan/
netplan generate
netplan apply

备份目录名称需要替换为实际目录。公网桥配置错误是最容易造成远程失联的步骤,不能在没有控制台的情况下盲目操作。

五、创建 KVM 私有网络

1. 编写 libvirt 网络定义

创建只用于宿主机和业务虚拟机通信的私有 NAT 网络:

cat >/root/vm-private.xml <<'EOF'

  vm-private
  
  
  
    
      
      
    
  

EOF

定义并启动网络:

virsh net-define /root/vm-private.xml
virsh net-autostart vm-private
virsh net-start vm-private

验证网络状态:

virsh net-list --all
virsh net-info vm-private
ip -br addr show virbr10

预期状态:

Name         State   Autostart   Persistent
vm-private   active  yes         yes

如果提示网络名称已存在,不要直接删除。先查看原配置:

virsh net-dumpxml vm-private

确认原网络没有承载其他虚拟机后,再决定使用 virsh net-edit vm-private 调整配置。删除或重新定义已有网络可能导致其他虚拟机断网。

2. 验证 NAT 出口

私有网络启动后,宿主机应生成 virbr10。libvirt 默认会配置 NAT 规则,使虚拟机可以访问软件仓库,但外部网络不能主动访问虚拟机。

此时可以检查:

virsh net-dhcp-leases vm-private
ip route

如果还没有 DHCP 租约,属于正常情况,因为虚拟机尚未启动。

六、创建业务虚拟机并分离数据盘

1. 创建虚拟机

将 Ubuntu Server ISO 放到宿主机,例如:

/var/lib/libvirt/iso/ubuntu-22.04-live-server-amd64.iso

确认文件存在:

ls -lh /var/lib/libvirt/iso/

创建 60 GB 系统盘和业务虚拟机:

mkdir -p /var/lib/libvirt/images

virt-install \
  --name app-vm \
  --memory 8192 \
  --vcpus 4 \
  --cpu host-passthrough \
  --disk path=/var/lib/libvirt/images/app-vm.qcow2,size=60,format=qcow2,bus=virtio \
  --cdrom /var/lib/libvirt/iso/ubuntu-22.04-live-server-amd64.iso \
  --network network=vm-private,model=virtio,mac=52:54:00:10:20:10 \
  --os-variant ubuntu22.04 \
  --graphics none \
  --console pty,target_type=serial

通过控制台完成 Ubuntu 安装。安装时建议:

  • 创建普通管理用户。
  • 开启 OpenSSH Server。
  • 不在业务虚拟机中安装桌面环境。
  • 系统盘只用于操作系统和软件包。
  • 使用虚拟机内的 DHCP,不要手工配置与其他网段冲突的静态地址。

安装完成后退出安装器并重启:

virsh console app-vm

控制台中使用 Ctrl 加 ] 退出。确认虚拟机状态:

virsh list --all
virsh autostart app-vm

2. 安装虚拟机增强组件

进入 app-vm 后执行:

sudo apt update
sudo apt install -y qemu-guest-agent
sudo systemctl enable --now qemu-guest-agent

在宿主机检查虚拟机地址:

virsh domifaddr app-vm --source lease
virsh net-dhcp-leases vm-private

预期可以看到:

10.10.20.10

如果看不到地址,依次检查虚拟机网卡、DHCP 服务和网卡状态:

virsh domiflist app-vm
virsh net-dhcp-leases vm-private

在虚拟机内执行:

ip -br addr
ip route
ping -c 3 10.10.20.1
ping -c 3 1.1.1.1

虚拟机能够访问 10.10.20.1 但不能访问公网时,优先检查 libvirt NAT、宿主机出口和 DNS,不要先修改 Docker 配置。

3. 创建独立数据盘

停止虚拟机后创建新的数据盘。下面命令只适用于新建的空白镜像,qemu-img create 不会删除已有文件,但后续 mkfs 会清空目标设备:

virsh shutdown app-vm
virsh domstate app-vm

确认状态为 shut off 后创建数据盘:

qemu-img create -f qcow2 /var/lib/libvirt/images/app-vm-data.qcow2 100G

挂载为虚拟机的 vdb:

virsh attach-disk app-vm \
  /var/lib/libvirt/images/app-vm-data.qcow2 \
  vdb \
  --driver qemu \
  --subdriver qcow2 \
  --persistent

启动虚拟机并检查设备:

virsh start app-vm
virsh domblklist app-vm

进入虚拟机后确认新磁盘:

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS

如果确认 /dev/vdb 是刚创建的空白数据盘,再格式化:

六、创建业务虚拟机并分离数据盘配图

sudo mkfs.ext4 -L docker-data /dev/vdb
sudo mkdir -p /srv/docker-data
sudo mount /dev/vdb /srv/docker-data

mkfs.ext4 会清除 /dev/vdb 上的原有数据。如果设备名称不是 /dev/vdb,必须以 lsblk 的实际结果为准,不能照抄。

写入 UUID 挂载配置:

UUID=$(blkid -s UUID -o value /dev/vdb)
echo "UUID=${UUID} /srv/docker-data ext4 defaults,noatime 0 2" | sudo tee -a /etc/fstab
sudo mount -a
findmnt /srv/docker-data
df -hT /srv/docker-data

预期结果中,/srv/docker-data 的文件系统类型为 ext4,设备来源为数据盘 UUID。

七、在业务虚拟机中安装 Docker

1. 安装 Docker Engine

以下命令在 app-vm 内执行:

sudo apt update
sudo apt install -y ca-certificates curl gnupg openssl

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null

sudo apt update
sudo apt install -y \
  docker-ce \
  docker-ce-cli \
  containerd.io \
  docker-buildx-plugin \
  docker-compose-plugin

sudo systemctl enable --now docker

验证版本和服务:

sudo docker version
sudo docker compose version
sudo systemctl is-active docker

如果软件仓库无法访问,先检查虚拟机的默认路由和 DNS:

ip route
resolvectl status
curl -I https://download.docker.com

不要在 Docker 尚未安装完成时手工创建大量网络规则。网络出口问题应先在虚拟机层解决。

2. 将 Docker 数据根目录迁移到独立数据盘

如果这是新建虚拟机,Docker 尚未运行过,可以直接使用以下配置:

sudo mkdir -p /srv/docker-data/docker
sudo chmod 0711 /srv/docker-data

sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "data-root": "/srv/docker-data/docker",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  }
}
EOF

sudo systemctl restart docker

验证 Docker 数据根目录:

sudo docker info --format '{{.DockerRootDir}}'
df -hT /srv/docker-data

预期结果:

/srv/docker-data/docker

如果 Docker 已经运行并且 /var/lib/docker 中存在镜像、容器或卷,不能直接修改 data-root 后重启。应先停止 Docker,再迁移数据:

sudo systemctl stop docker
sudo rsync -aHAX --numeric-ids /var/lib/docker/ /srv/docker-data/docker/
sudo systemctl start docker
sudo docker info --format '{{.DockerRootDir}}'

确认容器和镜像可用后,才考虑清理旧目录。清理前必须完成备份和回滚验证,不要直接执行 rm -rf /var/lib/docker。

八、落地 Docker 网络隔离

1. 创建应用目录和权限

在虚拟机内创建业务目录:

sudo mkdir -p \
  /srv/docker-data/apps/site/html \
  /srv/docker-data/apps/postgres

sudo chown -R root:root /srv/docker-data/apps
sudo chmod 0755 /srv/docker-data/apps/site
sudo chmod 0755 /srv/docker-data/apps/site/html

写入测试页面:

echo 'kvm-docker-isolation-ok' | sudo tee /srv/docker-data/apps/site/html/index.html

数据库目录的最终属主应以镜像内数据库进程 UID 为准。以 PostgreSQL 常见镜像为例,可先使用容器启动后的实际 UID 调整,不要仅凭宿主机用户名判断。

2. 编写 Compose 配置

创建项目目录:

sudo mkdir -p /srv/docker-data/apps/stack
sudo chown "$USER":"$USER" /srv/docker-data/apps/stack
cd /srv/docker-data/apps/stack

生成随机数据库密码并限制配置文件权限:

DB_PASSWORD=$(openssl rand -hex 24)
printf 'POSTGRES_PASSWORD=%s\n' "$DB_PASSWORD" > .env
chmod 600 .env

创建 compose.yaml:

八、落地 Docker 网络隔离配图

services:
  web:
    image: nginx:1.27-alpine
    container_name: app-web
    restart: unless-stopped
    ports:
      - "10.10.20.10:8080:80"
    volumes:
      - /srv/docker-data/apps/site/html:/usr/share/nginx/html:ro
    networks:
      - frontend
      - backend
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    container_name: app-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - /srv/docker-data/apps/postgres:/var/lib/postgresql/data
    networks:
      - backend

networks:
  frontend:
    name: app_frontend
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.10.0/24

  backend:
    name: app_backend
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.30.20.0/24

这里有三层限制:

  1. web 只绑定到虚拟机私有地址的 8080 端口,没有绑定虚拟机公网地址。
  2. db 没有 ports 配置,不会在虚拟机上发布数据库端口。
  3. backend 设置为 internal: true,数据库只通过 Docker 内部网络提供服务。

生产环境应将镜像版本固定到经过验证的版本或摘要,并把数据库密码放入受控的密钥管理系统。示例密码仅用于部署演示。

3. 验证 Compose 配置并启动

先检查配置语法:

docker compose config --quiet

无输出且返回码为 0,表示 Compose 配置可以解析。然后启动:

docker compose up -d
docker compose ps

检查网络和端口:

docker network inspect app_frontend
docker network inspect app_backend
ss -lntp | grep 8080

预期监听地址应类似:

10.10.20.10:8080

如果看到 0.0.0.0:8080,说明 Compose 文件没有按预期绑定到私有地址,需要检查缩进和端口配置。

从业务虚拟机内测试:

curl -i http://10.10.20.10:8080

预期响应正文包含:

kvm-docker-isolation-ok

检查容器挂载来源:

docker inspect app-web --format '{{json .Mounts}}' | jq
docker inspect app-db --format '{{json .Mounts}}' | jq

挂载源应分别位于:

/srv/docker-data/apps/site/html
/srv/docker-data/apps/postgres

如果数据库容器反复重启,先查看日志:

docker compose logs --tail=100 db

常见原因包括数据目录中已有不完整初始化文件、目录权限不正确或数据盘未挂载。不要在未备份数据库数据的情况下执行 docker compose down -v,该命令可能删除 Compose 管理的卷。

九、配置宿主机反向代理和防火墙

1. 安装宿主机 Nginx

在 KVM 宿主机执行:

apt update
apt install -y nginx
systemctl enable --now nginx

创建反向代理配置:

cat >/etc/nginx/sites-available/app-vm.conf <<'EOF'
server {
    listen 80;
    listen [::]:80;

    server_name app.example.com;

    location / {
        proxy_pass http://10.10.20.10:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}
EOF

ln -s /etc/nginx/sites-available/app-vm.conf \
  /etc/nginx/sites-enabled/app-vm.conf

nginx -t
systemctl reload nginx

server_name 改为实际域名。生产环境应在宿主机 Nginx 处完成 HTTPS 配置,虚拟机内部只接收来自 10.10.20.1 的反向代理流量。

从宿主机测试到虚拟机的连通性:

curl -i http://10.10.20.10:8080
curl -i -H 'Host: app.example.com' http://127.0.0.1

如果第一条成功、第二条出现 502 Bad Gateway,说明虚拟机服务可用,但 Nginx 访问路径、配置或宿主机防火墙需要检查。

2. 配置宿主机入口防火墙

如果宿主机没有现有防火墙策略,可以使用 UFW。执行前必须确认管理来源地址,并保留服务商控制台:

apt install -y ufw

ufw status verbose

将 198.51.100.25 替换为实际办公出口或管理地址:

ufw allow from 198.51.100.25 to any port 22 proto tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw default deny incoming
ufw default allow outgoing
ufw --force enable

验证规则:

ufw status numbered
ss -lntp

这组规则的效果是:

  • 公网只允许 SSH 管理地址访问 22 端口。
  • 公网允许访问宿主机的 80/443。
  • 虚拟机不直接暴露公网地址。
  • 宿主机可以访问私有网络中的 10.10.20.10:8080。

如果 SSH 当前来源没有被允许,启用 UFW 后会立即失去远程管理能力。已经存在 nftables、云平台安全组或服务商防火墙时,不要重复套用默认拒绝规则,应先合并现有策略。

3. 检查 Docker 端口暴露边界

在业务虚拟机中执行:

ss -lntp
docker ps --format 'table {{.Names}}\t{{.Ports}}'

正确的结果应类似:

10.10.20.10:8080->80/tcp

数据库容器不应出现 0.0.0.0:5432->5432/tcp 或类似公网发布结果。如果数据库需要被其他服务访问,应让其他服务加入 app_backend 网络,而不是发布数据库端口。

十、逐层验证和常见异常处理

1. KVM 检查失败

检查命令:

virt-host-validate qemu
ls -l /dev/kvm
systemctl status libvirtd --no-pager

判断方式:

  • 没有 /dev/kvm:检查硬件虚拟化或服务商是否允许嵌套虚拟化。
  • libvirt 未运行:启动 libvirtd,再检查日志。
  • 虚拟机只能软件模拟:确认是否必须使用 KVM 加速;生产业务不建议忽略性能警告。
  • 虚拟机启动后立即退出:查看 journalctl -u libvirtd 和虚拟机 XML,重点检查磁盘路径、权限和 CPU 模式。

2. 虚拟机没有地址

在宿主机执行:

virsh domiflist app-vm
virsh net-info vm-private
virsh net-dhcp-leases vm-private

在虚拟机内执行:

ip -br link
ip -br addr
sudo systemctl status systemd-networkd --no-pager

如果 virsh domiflist 没有 vm-private,说明虚拟机网卡接错网络。可以关机后编辑虚拟机配置,不要在生产运行中随意删除网卡:

virsh edit app-vm

确认网卡的 MAC 地址与 DHCP 保留配置中的 52:54:00:10:20:10 一致。

3. Nginx 返回 502

按照由外到内的顺序检查:

十、逐层验证和常见异常处理配图

curl -i http://10.10.20.10:8080
docker compose ps
docker compose logs --tail=100 web
systemctl status nginx --no-pager
tail -n 100 /var/log/nginx/error.log

不同结果的含义:

  • 虚拟机内访问 10.10.20.10:8080 失败:先修复 Docker 服务。
  • 虚拟机内成功、宿主机访问失败:检查宿主机到 virbr10 的路由和防火墙。
  • 宿主机访问成功、域名访问失败:检查 DNS、宿主机 80/443 防火墙和服务商入口策略。
  • Nginx 错误日志显示连接拒绝:检查 Docker 发布地址、容器监听端口和虚拟机防火墙。
  • Nginx 错误日志显示超时:检查应用响应时间、容器资源和虚拟机负载。

4. 数据盘没有挂载

检查:

findmnt /srv/docker-data
lsblk -f
cat /etc/fstab
df -hT
df -ih

如果虚拟机重启后 Docker 使用了 /var/lib/docker,通常说明 /srv/docker-data 没有在 Docker 启动前挂载。应先停止 Docker,修复 /etc/fstab,再确认挂载成功后启动 Docker:

sudo systemctl stop docker
sudo mount -a
findmnt /srv/docker-data
sudo systemctl start docker
docker info --format '{{.DockerRootDir}}'

磁盘空间和 inode 都要检查。磁盘还有容量但 inode 用尽时,容器同样会创建文件失败。

5. 容器网络网段冲突

如果香港服务器所在内网、libvirt 网段或应用内部网段已经使用 172.30.10.0/24,Docker 可能出现路由冲突。检查:

ip route
docker network ls
docker network inspect app_frontend app_backend

确认网段未与宿主机、虚拟机和业务网络重叠后再启动应用。修改 Compose 网络网段会导致容器网络重建,生产操作前应记录容器配置并安排维护窗口。

十一、上线前结果检查

完成部署后,按以下顺序验收:

宿主机检查

virsh list --all
virsh net-list --all
virsh net-dhcp-leases vm-private
virsh domblklist app-vm
systemctl is-active libvirtd nginx
ip -br addr
ss -lntp

应满足:

  • app-vm 为 running。
  • vm-private 为 active 且设置了自启动。
  • 虚拟机获得 10.10.20.10。
  • 虚拟机系统盘和数据盘均存在。
  • 公网监听只出现在宿主机 Nginx。
  • 宿主机没有意外监听 Docker 管理端口。

虚拟机检查

findmnt /srv/docker-data
df -hT
docker info --format '{{.DockerRootDir}}'
docker compose -f /srv/docker-data/apps/stack/compose.yaml ps
ss -lntp

应满足:

  • /srv/docker-data 来源为独立虚拟数据盘。
  • Docker Root Dir 位于 /srv/docker-data/docker。
  • Web 容器正常运行。
  • 数据库容器没有发布到公网。
  • Web 仅绑定 10.10.20.10:8080。

网络隔离检查

curl -i http://10.10.20.10:8080
docker network inspect app_frontend
docker network inspect app_backend

重点确认:

  • 宿主机可以访问虚拟机业务端口。
  • 外部只能访问宿主机 Nginx 的 80/443。
  • 数据库没有宿主机端口映射。
  • 后端网络使用 internal: true。
  • 虚拟机没有直接接入公网桥,除非已经确认服务商允许该模式。

存储隔离检查

docker inspect app-web --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
docker inspect app-db --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
virsh domblklist app-vm

应用数据源路径应位于虚拟机的 /srv/docker-data 下,宿主机只看到虚拟磁盘文件。需要注意,这种方式主要是权限和目录边界隔离,并不等于加密。如果数据盘需要防止宿主机管理员直接读取,应进一步采用虚拟机内加密磁盘,并单独设计密钥托管和启动流程。

十二、回滚方案

1. 回滚 Docker 应用

只停止应用而保留数据:

cd /srv/docker-data/apps/stack
docker compose down

不要带 -v 参数,否则可能删除 Compose 管理的卷。恢复配置后重新启动:

docker compose up -d

如果只是回滚某个镜像版本,先修改 compose.yaml 并保存当前文件:

cp -a compose.yaml compose.yaml.before-rollback
docker compose pull
docker compose up -d

2. 回滚 Docker 数据根目录

如果迁移到独立数据盘后出现异常:

sudo systemctl stop docker
sudo cp -a /etc/docker/daemon.json /root/deploy-backup/daemon.json.after-move

将 data-root 改回原路径,或恢复原配置:

sudo rm -f /etc/docker/daemon.json
sudo systemctl start docker
docker info --format '{{.DockerRootDir}}'

原 /var/lib/docker 未确认可用前,不要删除 /srv/docker-data/docker。如果数据目录中已经产生新数据,回滚前需要先做数据库备份和应用文件备份。

3. 回滚虚拟机数据盘

需要拆除数据盘时,先停止 Docker 和虚拟机:

sudo systemctl stop docker

virsh shutdown app-vm

确认虚拟机已关机后再分离磁盘:

virsh detach-disk app-vm vdb --persistent

这只会从虚拟机配置中移除磁盘,不会自动删除宿主机上的 app-vm-data.qcow2。确认数据已经备份后,再决定是否保留或清理镜像文件。

4. 回滚私有网络

如果私有网络配置错误,先确认没有其他虚拟机使用:

virsh net-list
virsh net-dhcp-leases vm-private

停止业务虚拟机后,可以停止网络:

virsh net-destroy vm-private

net-destroy 会立即影响连接到该网络的虚拟机。若只是暂时停用,保留网络定义;不要在没有备份 XML 的情况下执行删除操作。

5. 回滚宿主机公网桥

如果 br0 配置导致公网失联,应通过服务商控制台操作:

cp -a /root/deploy-backup/netplan-备份时间/* /etc/netplan/
netplan generate
netplan apply

恢复后确认:

ip -br addr
ip route
systemctl status systemd-networkd --no-pager

公网桥和防火墙回滚必须优先于应用回滚。宿主机无法管理时,虚拟机和 Docker 内的任何操作都无法解决外部访问问题。

十三、最终验收与回滚检查项

上线前应逐项确认:

  • [ ] 已确认服务商允许安装 KVM,且 /dev/kvm 可用。
  • [ ] 已记录真实公网 IP、网关、DNS 和管理来源地址。
  • [ ] 宿主机公网地址绑定在 br0,没有重复绑定到物理网卡。
  • [ ] vm-private 网络已设置自启动。
  • [ ] app-vm 使用私有网络,没有未经确认的公网桥接。
  • [ ] 虚拟机系统盘和数据盘分离。
  • [ ] /srv/docker-data 能在重启后自动挂载。
  • [ ] Docker Root Dir 位于独立数据盘。
  • [ ] Web 容器仅发布到 10.10.20.10:8080。
  • [ ] 数据库容器没有发布宿主机端口。
  • [ ] Docker 前端网络与后端网络用途分开。
  • [ ] 宿主机 Nginx 可以访问虚拟机业务端口。
  • [ ] 宿主机防火墙只放行必要的 SSH、HTTP 和 HTTPS。
  • [ ] 已备份 Netplan、libvirt XML、Compose 文件和应用数据。
  • [ ] 已确认 docker compose down 不会误删生产数据。
  • [ ] 已通过控制台验证网络和防火墙失误后的恢复路径。
  • [ ] 已记录虚拟机磁盘、Docker 数据目录和数据库备份的位置。

这套结构的核心不是把 KVM 和 Docker 安装在同一台机器上,而是明确两者的边界:KVM 隔离虚拟机、虚拟网络和虚拟磁盘,Docker 只在业务虚拟机内部管理应用,宿主机通过受控入口转发流量。对于单公网 IP 的香港服务器,这种私有 NAT 加宿主机反向代理的方式更容易控制暴露面;当业务需要多个公网地址、独立租户或更强安全边界时,应继续拆分虚拟机,而不是只增加 Docker 容器数量。