systemd-nspawn容器实战【镜像精简到3MB,容器使用独立网络】




前言

以前我的树莓派服务是放docker容器中的,但是后来docker访问受限,于是就用systemd-nspawn容器替代。systemd-nspawn容器的功能相对docker而言没那么丰富,但是胜在简单、轻量、便利。
本博文是作者“进取有乐”原创,仅发布于博客园,转载请注明出处。


需求

容器大小:我不喜欢太大的容器,所以容器要精简,经过我精简后的镜像只有几MB大小。我的服务有花生壳、小米球、vlmcsd(KMS服务器)
容器网络:我希望不使用宿主机网络空间,使用容器独立的网络。可以使用桥接、macvlan、ipvlan等,桥接网络麻烦点,所以这里详细介绍桥接网络。其他如macvlan、ipvlan、network-zone、veth等操作比较简单,在文末介绍。


精简容器体积

为了精简容器,开始时,我想要用alphine,但是后来发现我的某个服务在alpine下面无法运行,所以我使用了debian的initrd.gz,initrd是linux系统启动过程中的一个最小化内存盘系统。
!!注意: 下面操作容器的usr var etc等目录的时候,不要在路径前面加/, 否则会损坏宿主机的系统!!

cd /tmp; wget https://mirrors.tuna.tsinghua.edu.cn/debian/dists/bookworm/main/installer-arm64/current/images/cdrom/initrd.gz #下载适用于arm64架构的debian12的initrd.gz。最近稳定版是debian13 https://mirrors.tuna.tsinghua.edu.cn/debian/dists/stable/main/installer-arm64/current/images/cdrom/initrd.gz
mkdir -p /opt/unsafe/base; cd /opt/unsafe/base; # 创建一个目录,用于后续操作
gzip -dc /tmp/initrd.gz |cpio -idm; # 解包
rm -v bin sbin lib; mv -v usr/{bin,sbin,lib} ./; rm -rfv initrd usr/* var/* # 删掉根下的bin sbin lib三个软连接,把usr/下的bin、sbin、lib三个目录移动到根下。删掉initrd/目录,删掉usr/ var/下的所有数据(而不删除目录本身)
cd bin; ls -l |grep '^-' |awk '{print $NF}' # 查看bin/下有很多软连接和常规文件,除了busybox这个常规文件外(busybox保证基本shell),其他常规文件用处不大,下面一条命令会删除它们。
rm -v $(ls -l |grep '^-' |awk '!/busybox/{print $NF}' |xargs) # bin/下除了busybox,其他的常规文件都删除
cd ../sbin; rm -v $(ls -l |grep '^-' |awk '{print $NF}') # sbin/下所有常规文件删掉,保留软连接
ldd /tmp/phtunnel; ldd /tmp/xiaomiqiu; ldd /tmp/vlmcsd-armv7el-uclibc-static # 查看我的花生壳、小米球、vlmcsd需要依赖什么动态库。执行后发现,后两者是静态可执行文件,花生壳(phtunnel)依赖的动态库如下

       linux-vdso.so.1 
       libpthread.so.0
       libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 
       /lib/ld-linux-aarch64.so.1 
       libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6   

cd ../lib; find . -regextype egrep -regex ".*(linux-vdso|libpthread|libc|ld-linux-aarch64|libm)\.so.*" # 在lib/下找一下这几个文件
find . -type f -regextype egrep ! -regex ".*(linux-vdso|libpthread|libc|ld-linux-aarch64|libm)\.so.*" -delete # find命令增加取反"!"操作,删除lib/下除了上面依赖库之外的所有文件
cd ../etc; find . -regextype egrep ! -regex ".*(passwd|group|fstab)" -delete # 删除etc/下除了passwd、group、fstab之外的所有文件或目录
cp -v /etc/os-release ./ # 把宿主机的os-release 复制到容器etc目录,没有这个文件,容器启动不了。
cd ../; while :; do x="$(find . -mindepth 2 -type d -empty |xargs)"; if [ -n "$x" ]; then rm -rfv $x; else break; fi; done # 回到容器的根,删除深度至少是2层的空目录,因为有些子目录删除后会导致父目录也变成空目录,所以要循环删除
du -sh # 查看容器的根,发现占用空间是3M。这个体积已经满意了。但是精简后也要缺点,就是容器启动的时候不能使用-b参数(boot),一些网络配置文件无法使用,只能容器启动后动态执行网络配置命令。


创建目录结构

useradd -d /opt/unsafe unsafe # 创建unsafe用户,家目录是/opt/unsafe
cd /var/lib/machines; ln -s /opt/unsafe/base hsk; ln -s /opt/unsafe/base xmq; ln -s /opt/unsafe/base vlmcsd # 在/var/lib/machines目录下创建hsk、xmq、vlmcsd三个软连接,三个软连接都指向/opt/unsafe/base的容器文件根系统。
mkdir /opt/unsafe/base/app # 在容器根下面创建一个app目录用于绑定三个服务的二进制路径
mkdir /opt/unsafe/{hsk,xmq,vlmcsd} # 在/opt/unsafe目录下创建hsk、xmq、vlmcsd目录用来存放这三个服务的二进制文件和配置文件。
chown -R unsafe:unsafe /opt/unsafe; chmod 755 /opt/unsafe # 递归修改unsafe目录属主。修改家目录权限


测试容器是否正常启动

systemd-nspawn --machine hsk --bind=/opt/unsafe/hsk:/app --read-only --private-users=2048:65535 # 启动容器,这里的--machine hsk 也可以写成--directory /var/lib/machines/hsk, 因为/var/lib/machines下有了hsk目录,所以这里才可以写成--machine hsk, 否则必须指明容器根目录路径。 --bind表示把宿主机的目录绑定到容器的/app路径下, --private-users 2048:65535表示容器是以这个范围内的uid来启动,宿主机执行ps auxf就能看到进程的uid是大于等于2048的。执行了这个命令后,就进入了容器的shell界面。
/app/phtunnel -c /app/phtunnel.json # 在容器的shell命令下启动花生壳,发现正常。同样的步骤,小米球和vlmcsd测试都正常。(我是直接测试,建议先在宿主机测试好,调好配置文件,然后再在容器内测试)。敲Ctrl+]组合键3次退出容器


创建并测试容器的独立网络

我的树莓派连接的网关是192.168.1.1;宿主系统的网络是用NetworkManager管理网络的,NetworkManager执行命令后会在/etc/NetworkManager/system-connections/形成配置文件。现在我的树莓派是使用有线连接的(如果是wifi连接,需要特殊处理,见后文)。这里使用桥接网络给容器使用。
nmcli connection add type bridge con-name br1 ifname br1 ipv4.method manual ipv4.addresses "192.168.1.101/24" ipv4.gateway "192.168.1.1" ipv4.dns "223.5.5.5,119.29.29.29" # 创建一个桥接网络接口br1
nmcli connection add type bridge-slave con-name br1-slave-eth0 ifname eth0 master br1 # 把eth0接口加入br1
nmcli connection show # 看看eth0对应的连接的NAME字段是什么,一般是"Wired connection 1",有那么就删掉它 nmcli connection delete "Wired connection 1",防止该连接和br1的slave网口冲突
nmcli connection up br1 # 使br1处于up状态。此时nmcli connection show br1发现它是自动连接的。
systemd-nspawn --machine hsk --bind=/opt/unsafe/hsk:/app --read-only --private-users=2048:65535 --network-bridge=br1 # 启动hsk容器,进入容器shell,容器的host0网口和宿主机的br1是桥接在一起的
ip link set host0 up; ip a a 192.168.1.102/24 dev host0 #在容器内的shell中启用host0网口,并配上ip地址。大概在配好ip后30多秒才能ping通网关192.168.1.1
ip route add default via 192.168.1.1 dev host0 # 添加默认路由,ping baidu.com通了。


以systemd服务来管理

上面的测试通过后,形成服务文件,服务文件的形式有两种,一种是常规的"服务名.service"文件,一种是systemd-nspwan容器的"服务名.nspawn"文件。注意在/etc/systemd/nspawn/目录下的 .nspawn配置文件是被信任的,/var/lib/machines/下的.nspawn文件是不被信任的(不信任场景下,执行systemd-nspawn -M name时,会忽略一些系统级命令)。.nspawn服务是使用类似"systemctl start systemd-nspawn@服务名.service"来管理服务的。我使用的是常规的.service文件。
以花生壳的.service服务文件为例: cat /lib/systemd/system/hsk.service

    [Unit]
    Description=hsk(phtunnel) service
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    # 如果想要服务启动过程及运行日志,请执行journalctl -feu hsk ,下面启动过程使用循环ping检查网关连通性,通了之后再启动服务。
    ExecStart=systemd-nspawn --bind=/opt/unsafe/hsk:/app --read-only --private-users=2048:65535\
                             --machine hsk --network-bridge=br1 /bin/sh -c 'ip link set host0 up; ip addr add 192.168.1.111/24 dev host0;\
                             ip route add default via 192.168.1.1 dev host0; echo "等待网络..."; \
                             while :; do ping -c1 -w1 -W1 192.168.1.1 >/dev/null 2>&1 && break; done;\
                             /app/phtunnel -c /app/phtunnel.json;'
    
    Restart=always
    RestartSec=30s
    ProtectSystem=yes
    
    [Install]
    WantedBy=multi-user.target

执行systemctl start hsk 启动花生壳服务的容器,执行systemctl enable hsk 使花生壳服务开机启动。小米球、vlmcsd服务也用类似步骤完成。只是容器ip地址和命令行不同:
/app/xiaomiqiu -log stdout -log-level error -config /app/xiaomiqiu.conf
/app/vlmcsd-armv7el-uclibc-static -D -l syslog


通过wifi连接网关的容器配置

  • 如果使用wifi连接网关(我的wlan0的ip地址是192.168.1.X),由于wifi网络接口无法加入桥接网口,所以网络配置方法也和有线网连的不同。
    nmcli connection add type bridge con-name br0 ifname br0 ipv4.method manual ipv4.addresses "192.168.100.1/24" # 创建一个桥接网络接口br0
    nmcli connection up br0 # 使br0处于up状态。
    修改/etc/sysctl.conf文件,使net.ipv4.ip_forward=1 然后执行sysctl -p立即生效。我的树莓派是Debian12,如果是Debian 13则默认不再读取 /etc/sysctl.conf,需将配置分散到 /etc/sysctl.d/ 目录下的独立文件。
    增加nftables的NAT:让容器中的花生壳、小米球和能经由wlan0访问公网(masqurade),也让外部网络经由wlan0访问容器中vlmcsd提供的KMS服务(DNAT)。下面是nftables.conf内容:cat /etc/nftables.conf

      flush ruleset
      
      table ip tb_pi {
              chain ch_fwd {
                      type filter hook forward priority filter; policy accept;
                      iifname "br0" ip saddr 192.168.100.0/24 accept
                      ct state established,related accept
              }
      
              chain ch_snat {
                      type nat hook postrouting priority srcnat; policy accept;
                      oifname "wlan0" ip saddr 192.168.100.0/24  masquerade
              }
      
              chain ch_dnat {
                      type nat hook prerouting priority 100; policy accept;
                      tcp dport 1688 dnat to 192.168.100.13
              }
      }
    

    上面的nftables配置文件要配合nftables服务生效,确保nftables服务是开机启动的,确保nftables.service文件读取的配置文件路径是/etc/nftables.conf。没问题后,执行systemctl restart nftables 重新载入配置文件。
    花生壳的.service文件和有线网连接的也不同,比如ip地址,默认路由、测试ping的地址等。服务配置文件内容如下,cat /lib/systemd/system/hsk.service

      [Unit]
      Description=hsk(phtunnel) service
      After=network-online.target
      Wants=network-online.target
      
      [Service]
      # 使用while ping检查宿主机br0网口,通了之后再启动服务。
      ExecStart=systemd-nspawn --bind=/opt/unsafe/hsk:/app --read-only --private-users=2048:65535\
                               --directory /var/lib/machines/hsk --network-bridge=br0 \
                               /bin/sh -c 'ip link set host0 up; ip addr add 192.168.100.11/24 dev host0;\
                               ip route add default via 192.168.100.1 dev host0; echo "等待网络..."; \
                               while :; do ping -c1 -w1 -W1 192.168.100.1 >/dev/null 2>&1 && break; done;\
                               /app/phtunnel -c /app/phtunnel.json;'
      
      Restart=always
      RestartSec=30s
      ProtectSystem=yes
      
      [Install]
      WantedBy=multi-user.target
    



关于容器网络的更多内容

  • 由于我是精简镜像,所以很多非常便利的功能不能实现。比如宿主机如果使用的是systemd-networkd服务管理网络,则systemd-networkd.service默认包含 /usr/lib/systemd/network/80-container-ve.network , 此文件匹配所有通过该选项创建的虚拟以太网连接的宿主端接口, 此文件不但为这些接口启用了DHCP功能,而且还为这些接口设置了通向宿主机外部网络的路由(从而可以连通外网)。 该服务还默认包含 /usr/lib/systemd/network/80-container-host0.network , 此文件匹配所有通过该选项创建的虚拟以太网连接的容器端接口,并且为这些接口启用了DHCP功能。 如果在宿主与容器内同时运行了systemd-networkd服务, 那么无须额外的配置,即可自动实现在容器与宿主之间进行IP通信,并且可以连接到外部网络。

  • DHCP功能。如果宿主机开启dnsmasq服务,并且把dhcp服务提供给指定网口(比如br0等),容器使用-b启动,并且网口配置成dhcp自动获取ip,则容器启动后,即可获取一个宿主机DHCP服务提供的IP地址

  • 容器的其他网络选项
    --network-zone XX # 则宿主机产生一个叫"vz-XX"的网口,容器启动后,生成一个host0网口。--network-zone XX 的形式穿件的网络会自动在宿主机创建网络接口(第一个容器启动),自动销毁(最后一个容器退出)。可以方便的将一组相关的本地容器,添加到基于虚拟以太网的同一个广播域(也就是同一子网)之中。 这样的广播域就被称为"区域"(zone)。如果结合前面的systemd-networkd的自动分配ip地址、联通外网功能,会很方便。
    --network-veth-extra XX:YY # 容器启动后,宿主机生产一个名字类似于XX@if2的虚拟网络接口,这里的@if2表示宿主机的XX虚拟接口关联的物理接口是序号为2的接口(ip link show命令查看发现序号2的接口是eth0)。使用ip address命令修改ip地址的时候,使用的接口名是XX,XX@if2后面的@if2要去掉。容器中的接口是YY@if39,这里的@if39表示容器的YY接口与宿主机的序号第39的接口关联,在宿主机执行ip link show发现39号网络接口的名字是XX。 当然参数也可以不用冒号和冒号后面的名字,表示宿主机和容器产生的网络接口都是XX。
    --network-veth # 该选项不用带参数,容器启动后,宿主机会产生一个叫"ve-容器名"的虚拟网络接口,虚拟机的网络接口是host0。--network-veth 是使用 systemd-nspawn@容器名.service 模版配置文件是的默认选项。

    --network-macvlan 宿主机网口 # 该选项隐含--private-network。容器与宿主机的mac地址不同。例如--network-macvlan=eth0 进入容器后,我们会发现容器中mv-eth0的mac地址和宿主机eth0的mac地址不同。容器的mv-eth0需要设置up,并分配ip地址才可以使用(与宿主机的eth0同一个网段)。我测试发现不可以ping通宿主机ip,但是可以ping通同个局域网的其他主机。
    --network-ipvlan 宿主机网口 # 该选项隐含--private-network。容器与宿主机的mac地址相同。例如--network-ipvlan=eth0 进入容器后,我们会发现容器中iv-eth0的mac地址和宿主机eth0的mac地址相同。容器的iv-eth0需要设置up,并分配ip地址才可以使用(与宿主机的eth0同一个网段)。我测试发现不可以ping通宿主机ip,但是可以ping通同个局域网的其他主机。

  • 不管是哪种虚拟网络,容器和宿主机的网口都要处于up状态,想要通信就要分配ip地址。

posted on 2025-09-24 16:09  进取有乐  阅读(71)  评论(1)    收藏  举报

导航