LXC容器简明教程

LXC(LinuX Containers)是一种操作系统层的虚拟化技术,是Linux内核容器功能的一个用户空间接口。与Docker/Podman不同,LXC注重的是容器化的操作系统而不是应用服务。使用LXC就如同在裸机或虚拟机上运行了一个完整的Linux操作系统,这些容器一般基于一个干净的发布镜像并且会长时间运行。

安装

1
2
3
4
5
6
7
8
# Debian
apt install lxc lxcfs lxc-templates [dnsmasq-base]

# RHEL
yum install lxc lxcfs lxc-templates [dnsmasq]

# Alpine
apk add lxc lxc-templates lxc-download xz

检查可用的容器模板文件:ls /usr/share/lxc/templates

配置

守护进程配置

LXC的守护进程配置文件位于/etc/defaults/lxc(Debian)或/etc/sysconfig/lxc(RHEL),内容如下:

1
2
3
4
5
6
7
8
9
10
# 是否启用开机启动,这一选项在systemd下没有意义
LXC_AUTO="true"
# 自动启动的容器组,默认为onboot组
BOOTGROUPS="onboot,"
# 等待容器关机的时间,默认5s
SHUTDOWNDELAY=5
# 开机服务选项,如果要在开机时自动启动所有容器,设为"-a -A"
OPTIONS=
# 停止服务选项,如果要直接杀死所有容器,添加-k
STOPOPTS="-a -A -s"

设置完成后,启动主服务,它掌管容器的Autoboot:

1
systemctl enable --now lxc

网络守护进程配置

LXC的网络守护进程配置文件位于/etc/defaults/lxc-net(Debian)或/etc/sysconfig/lxc-net(RHEL),内容如下:

1
2
3
4
# 启用自动生成的lxcbr0网桥,这需要dnsmasq包
USE_LXC_BRIDGE="true"
# LXC的DHCP配置文件,可以设为/etc/dnsmasq.conf以应用主机配置
#LXC_DHCP_CONFILE=/etc/dnsmasq.conf

设置完成后,启动网络服务,它掌管网桥配置:

1
systemctl enable --now lxc-net

LXC的默认网桥为lxcbr0,这是一个三层交换机,宿主机侧的Veth会桥接在该网桥上,LXC会自动设置NFTables防火墙进行NAT,使得容器能够连通外网。

在旧版本的RHEL中,默认不创建此网桥,让用户使用自己创建的网桥。若要启用此网桥,先安装dnsmasq包(但是不必启动dnsmasq服务),然后修改/etc/sysconfig/lxc,将USE_LXC_BRIDGE=改为"true",然后重启LXC网络服务systemctl restart lxc-net

Alpine没有自动配置的网桥,用户只能自己配置。

最后,启动lxcfs服务,它为容器提供了主机伪文件系统虚拟化的接口:

1
systemctl enable --now lxcfs

容器配置

LXC自带的默认配置文件保存在/usr/share/lxc/config/,它们会在通过模板生成容器时按需在容器配置文件中自动添加引用。具体默认配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 以下内容来自/usr/share/lxc/config/common.conf,这是通用的默认配置
# tty设备映射目录为/dev/lxc
lxc.tty.dir = lxc
# 最多1024个伪终端
lxc.pty.max = 1024
# 最多4个tty
lxc.tty.max = 4
# 放弃一些不安全的Capability
lxc.cap.drop = mac_admin mac_override sys_time sys_module sys_rawio
# 绑定克隆后自动生成主机名的脚本
lxc.hook.clone = /usr/share/lxc/hooks/clonehostname
# 配置cgroup设备权限
lxc.cgroup.devices.deny = a
lxc.cgroup.devices.allow = ...
lxc.cgroup2.devices.deny = a
lxc.cgroup2.devices.allow = ...
# 配置一些默认目录映射
lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
lxc.mount.entry = /sys/fs/fuse/connections sys/fs/fuse/connections none bind,optional 0 0
# 启用默认SECCOMP配置
lxc.seccomp.profile = /usr/share/lxc/config/common.seccomp

# 以下内容来自/usr/share/lxc/config/userns.conf,这是对支持非特权模式的容器的优化配置,可以覆盖在通用配置之上
# 清空所有的cgroup设备权限设置,交给用户设置
lxc.cgroup.devices.deny =
lxc.cgroup.devices.allow =
lxc.cgroup2.devices.deny =
lxc.cgroup2.devices.allow =
# 清空所有Capability设置,交给用户设置
lxc.cap.drop =
lxc.cap.keep =
# 清空tty设置
lxc.tty.dir =
# 微调默认目录映射
lxc.mount.auto = sys:rw

# 以下内容来自/usr/share/lxc/config/common.seccomp,这是默认的SECCOMP设置
# 版本2
2
# 黑名单
denylist
# 禁用一切umount -f操作
reject_force_umount
# 所有架构
[all]
# 阻止指定的系统调用并返回指定的代码
kexec_load errno 1
open_by_handle_at errno 1
init_module errno 1
finit_module errno 1
delete_module errno 1

# 以下内容来自/usr/share/lxc/config/oci.conf,这是对OCI容器的优化配置
# 给OCI容器启用DHCP
lxc.hook.start-host = /usr/share/lxc/hooks/dhclient
lxc.hook.stop = /usr/share/lxc/hooks/dhclient

系统级容器配置文件位于/etc/lxc/default.conf(默认通用)和/var/lib/lxc/容器名/config(独立配置)。

用户级容器配置文件位于~/.config/lxc/default.conf(默认通用)和~/.config/lxc/容器名/config(独立配置)。

以下配置建议写入通用配置文件内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
lxc.include = /path
导入其他位置的配置文件。

lxc.net.0.type = veth
第 0 号网卡的类型,如果是第二块网卡,则为 lxc.net.1.type 可选类型如下:
none:完全拷贝主机的网络状态。注意对于非特权容器来说,必须挂载sysfs才能使用该选项。
empty:不创建网卡,容器仅有回环网卡。
veth:创建一个对等网卡,该网卡的一端分配给容器,另一端与 lxc.net.0.link 指定的网桥桥接。
macvlan:创建一个 MACVlan 接口,该接口和由 lxc.net.0.link 指定的网桥相连接。
vlan:创建一个由 lxc.net.0.link 指定的虚拟局域网接口分配给容器,VLan的标识符可由 lxc.net.0.vlan.id 指定。
phys:将 lxc.net.0.link 指定的物理网卡分配给容器,此时主机将不能使用该网卡。

lxc.net.0.name = 0
指定虚拟网卡接口的名称,默认是eth开头。

lxc.net.0.link = lxcbr0
指定虚拟网卡连接到的网桥。

lxc.net.0.ipv4[|ipv6].address = 10.0.3.102
容器中网卡的IP。默认情况下,使用DHCP,IPv6对RA报文的处理方法和主机一致。

lxc.net.0.ipv4[|ipv6].gateway = 10.0.3.1
容器中网卡的网关。

lxc.net.0.flags = up
指定网卡的状态,up激活接口,down关闭接口。

lxc.net.0.hwaddr = 00:16:3e:bc:27:d1
指定虚拟网卡的MAC地址,默认情况该值会自动分配。

lxc.net.0.mtu = 1500
指定虚拟网卡的MTU大小,默认情况该值和主机一致。

lxc.arch = amd64
指定容器架构,可选x86, i686, x86_64, amd64。

lxc.autodev = 1
在启动容器时自动生成最小化的/dev目录,默认启用。

lxc.autodev.tmpfs.size = 500K
容器的/dev目录的临时分配空间,默认为500K。

lxc.init.cmd = /sbin/init
启动容器时的init程序。

lxc.apparmor.profile = generated|unchanged|unconfined
容器内的AppArmor规则。
unchanged:继承宿主机规则。
unconfined:不受限制的空白规则。
generated:使用临时生成的规则。

lxc.apparmor.allow_incomplete = 0|1
是否允许AppArmor不完整。

lxc.apparmor.allow_nesting = 0|1
是否允许嵌套容器。

lxc.selinux.context = system_u:system_r:lxc_t:s0|unconfined_t
手动设置容器的SELinux标签。一般用不到这个功能。
unconfined_t表示关闭容器根文件系统的SELinux标签。

lxc.tty.max = 2
容器中可用的最大终端数。

lxc.idmap = u 0 1000 25565
映射宿主机用户,从左到右依次为:
映射用户或用户组,可选u, g。
容器中的用户UID|GID。
宿主机中的用户UID|GID。
映射的用户数量,用于其他用户,必须确保用户有子UID|GID。

lxc.seccomp.profile = /etc/lxc/rules.seccomp
Seccomp安全模块的规则路径。

lxc.seccomp.allow_nesting = 1
无论是否加载了SECCOMP,都加载过滤规则,这对于处于嵌套环境中的容器特别有用。
默认值为0。

lxc.no_new_privs = 1
禁止容器通过execve调用获得任何新权限,这包括SetUID,SetGID和Capabilities。
当启用此选项时,很多发行版会无法正常工作,因为通用发行版一般都需要带有SetUID或SetGID位的可执行文件。
除非有高安全需求,否则不建议启用此选项。

lxc.cgroup.relative = 1
确保LXC不会脱离cgroup,这在使用systemd服务控制LXC容器时有用。

lxc.log.level = 2
日志等级。

lxc.log.file = /var/log/lxc.log
日志文件路径。

lxc.log.syslog = local7
LXC登记的syslog日志设施,可选local[1-7]。

以下内容建议写入容器特定的配置文件内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
lxc.uts.name = my-container
容器的主机名。

lxc.environment = TERM=xterm-256color
在容器启动时设置新的环境变量。在某些情况下可能有用。

lxc.tty.dir = lxc
保存容器tty设备文件的目录,会显示在/dev目录下。

lxc.sysctl.内核参数项 = 值
设置容器中的内核参数项。

lxc.cgroup.dir = lxc.slice
将容器的监视CGroup(保存监视进程及其子进程)和载荷CGroup(保存容器Init进程及其子进程)放到一个父CGroup下。
值是相对于根CGroup的子目录,会显示在宿主机的/sys/fs/cgroup目录下。
默认情况下不创建父CGroup,容器的监视CGroup和载荷CGroup直接放在根CGroup下。

lxc.cgroup.dir.container = lxc.payload.容器名称
手动指定容器的载荷CGroup。
和lxc.cgroup.dir互斥。

lxc.cgroup.dir.monitor = lxc.monitor.容器名称
手动指定容器的监视CGroup。
和lxc.cgroup.dir互斥。

lxc.cgroup.dir.monitor.pivot = CGroup
容器停止时,将其监视进程移动到指定的CGroup。

lxc.cgroup.dir.container.inner = CGroup
在容器的载荷CGroup内再创建一个CGroup。

lxc.group = onboot
将容器加入指定的自动启动组,onboot是内置组,如果什么组都不设置,那么会加入一个隐藏的NULL组。

lxc.start.auto = 1
对容器启用自动启动。

lxc.start.delay = 10
该容器自动启动的延迟。

lxc.monitor.unshare = 1
在容器启动时,将挂载命名空间与宿主机隔离,也就是说容器在启动后**无法再挂载任何新的设备**。
这需要root权限,也就是说仅系统级容器可用,实际上对用户来说也没什么意义——用户本来就不能挂载。

lxc.rootfs.path =
设置容器根文件系统。可以是目录,镜像文件或块设备,默认为目录dir,格式如下:
[dir:]/path 将指定目录作为容器的根文件系统。
overlayfs:/lower:/upper 使用将upper:rw目录覆盖在lower:ro目录之上的overlayfs作为容器的根文件系统。
loop:/file 使用loopback设备作为容器的根文件系统。

lxc.rootfs.options = noacl
容器根文件系统的额外挂载选项,比如:
noacl 禁用ACL。
nosuid 禁用SetUID。
lazytime 惰性更新文件访问记录。
noatime 完全不更新文件访问记录。

lxc.rootfs.managed = 0
LXC是否管理该容器的根文件系统,设为0则LXC无权修改该容器的存储内容。
默认值为1。

lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
设置映射哪些宿主机上的内核文件系统,这可能在某些情况下相当有用。包括以下几个可选项:
proc 等价于proc:mixed。
proc:mixed 读写映射/proc,但是/proc/sys和/proc/sysrq-trigger是只读的。这是默认配置中的操作。
proc:rw 完全读写映射/proc。这不安全。
sys 等价于sys:mixed。
sys:mixed 只读映射/sys,但是/sys/devices/virtual/net是可写的。这是默认配置中的操作。
sys:ro 完全只读映射/sys。
sys:rw 完全读写映射/sys。
cgroup 如果容器有特权,那么等价于cgroup:rw,否则等价于cgroup:mixed。
cgroup:mixed[:force] 在/sys/fs/cgroup中创建容器自己的子目录 lxc.monitor.容器名 和 lxc.payload.容器名,然后将主机的/sys/fs/cgroup下的控制器只读映射到这个子目录中,再将这些子目录读写映射到容器内的控制器目录中。容器只能写入自己的cgroup配置,但是不能写入主机特有的cgroup配置。:force表示强制进行映射。这是默认配置中的操作。
cgroup:ro[:force] 在/sys/fs/cgroup中创建容器自己的子目录 lxc.monitor.容器名 和 lxc.payload.容器名,然后将主机的/sys/fs/cgroup下的控制器只读映射到容器中,再将这些子目录只读映射到容器内的控制器目录中。:force表示强制进行映射。
cgroup:rw[:force] 在/sys/fs/cgroup中创建容器自己的子目录 lxc.monitor.容器名 和 lxc.payload.容器名,然后将主机的/sys/fs/cgroup下的控制器读写映射到容器中,再将这些子目录读写映射到容器内的控制器目录中。:force表示强制进行映射。这不安全。
cgroup-full:mixed[:force] 将/sys/fs/cgroup目录直接只读映射到容器中,但是只有容器自己的控制器目录是可写的。:force表示强制进行映射。
cgroup-full:ro[:force] 将/sys/fs/cgroup目录直接只读映射到容器中。:force表示强制进行映射。
cgroup-full:rw[:force] 将/sys/fs/cgroup目录直接读写映射到容器中。:force表示强制进行映射。
**注意,对于cgroup v2来说,因为cgroup namespace的存在,cgroup的挂载选项就变得没有意义了,会被LXC无视**。

lxc.mount.fstab = /xxx
指定一个fstab配置文件,格式如下:
去掉/的容器目录 去掉/的容器目录 文件系统 挂载选项 0 0
它的主要作用是从镜像挂载容器目录。
如果是内核文件系统或者镜像文件,那么需要按照类型指定文件系统,如cgroup,sysfs,proc,dev等等。
挂载选项往往带有nodev,noexec,nosuid。

lxc.mount.entry = /mnt mnt none bind 0 0
映射宿主机的目录,格式如下:
主机目录 去掉/的容器目录 文件系统 bind,其它挂载选项 0 0
文件系统必须为none。
挂载选项必须有bind,默认情况下是rw权限,可以设置ro挂载选项。
挂载选项create=dir|file会在挂载时自动创建挂载目录|文件。
挂载选项optional会使挂载源不存在时也不会失败。
如果使用无特权容器,注意权限,确保主机上的目录或挂载点的UID和GID是无特权容器用户在外部的UID。

lxc.cgroup2.devices.allow = a rw
**如果使用cgroup v1,那么cgroup2需要改为cgroup**。
允许容器访问宿主机物理设备。它往往需要和设备的mount.entry映射条目一起出现。
默认情况下,LXC禁止容器访问所有物理设备。
也可以手动指定,语法左到右依次为:
设备类型,可选a(全部设备),c(字符设备|输入输出设备),b(块设备|存储设备),可以执行ls -l /dev,然后查看第一个字母获得。
设备位置,可以执行ls -l /dev,然后查看第五列内容获得,然后使用:连接节点,如1, 2即为1:2,可以使用通配符,比如*:*表示同类型的所有设备。
访问权限,可选r(访问),w(写入),m(创建节点)。

lxc.cgroup2.devices.deny = c 226:0
**如果使用cgroup v1,那么cgroup2需要改为cgroup**。
禁止容器访问宿主机物理设备。
语法左到右依次为:
设备类型,可选a(全部设备),c(字符设备|输入输出设备),b(块设备|存储设备),可以执行ls -l /dev,然后查看第一个字母获得。
设备位置,可以执行ls -l /dev,然后查看第五列内容获得,然后使用:连接节点,如1, 2即为1:2,可以使用通配符,比如*:*表示同类型的所有设备。

lxc.cgroup2.cpuset.cpus = 0,1,2,3,4,5,6,7
**如果使用cgroup v1,那么cgroup2需要改为cgroup**。
容器可用的CPU核心,这里表示八核的所有核心。

lxc.cgroup2.memory.max = 2G
**如果使用cgroup v1,那么cgroup2需要改为cgroup**。
容器最大内存大小。

lxc.cgroup2.memory.swap.max = 2G
**如果使用cgroup v1,那么cgroup2需要改为cgroup**。
容器最大交换空间大小。

lxc.cap.drop =
用于无特权容器,只放弃指定的Capability。

lxc.cap.keep =
用于无特权容器,只保留指定的Capability。

一个常用的最小配置内容为:

1
2
3
4
5
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.name = eth0
lxc.apparmor.profile = unconfined

一个常用的安全增强配置为:

1
2
3
4
5
6
7
8
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.name = eth0
lxc.monitor.unshare = 1
lxc.apparmor.profile = generated
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

容器性能限制

使用CGroup v2实现容器的性能限制,编辑配置文件添加:lxc.cgroup2.配置组.配置项 = 大小

例如:

1
2
3
4
5
lxc.cgroup2.cpu.share = 1024    # CPU占用比例
lxc.cgroup2.cpuset.cpus = 0,1 # CPU可用核心
lxc.cgroup2.memory.max = 2G # 最大可用内存大小
lxc.cgroup2.memory.swap.max = 2G # 最大可用交换空间大小
lxc.cgroup2.devices.allow = a # 允许访问所有设备

注:如果LXC使用目录作为根文件系统,要限制容器根分区大小,请使用配额或子卷。

嵌套容器

要在容器中进行嵌套,确保在配置文件中添加了以下内容:

1
2
3
lxc.apparmor.allow_nesting = 1
# 如果使用的不是cgroup v2
#lxc.mount.auto = cgroup

目录映射

映射目录的方法是添加lxc.mount.entry配置项,例如:

1
lxc.mount.entry = /mnt mnt none bind 0 0

需要注意的有三点:

  1. 容器中的目标目录的开头没有/,它是一个相对路径。
  2. 文件系统总是为none,挂载选项总是为bind
  3. 后两位数字总是为0

设备映射

映射设备的方法和目录几乎一致,只是配置项略有不同:

1
2
lxc.mount.entry = /dev/sdc dev/sdc none bind,optional,create=file 0 0
lxc.cgroup2.devices.allow = b 10:0 rwm
  • optional表示可选挂载,在源设备不存在时,不会导致挂载失败。
  • create=file|dir表示自动创建,在目标挂载点不存在时,容器内会自动创建。
  • 另外,容器还需要通过lxc.cgroup2.devices.allow配置项配置访问设备的权限。

例如,映射主机显卡:

1
2
3
4
# /dev/dri/card0的设备号
lxc.cgroup2.devices.allow = c 226:0 rwm
# /dev/dri是目录
lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir

映射FUSE设备以实现挂载FUSE文件系统:

1
2
3
4
# /dev/fuse的设备号
lxc.cgroup2.devices.allow = c 10:229 rwm
# /dev/fuse是文件
lxc.mount.entry = /dev/fuse dev/fuse none bind,optional,create=file

特别地,LXC容器内可以直接挂载块设备,只需要对配置项做出一些修改:

1
lxc.mount.entry = /dev/sdc mnt ext4 defaults 0 0

注意,此时文件系统是块设备中的真实文件系统。

CGroup问题

关闭CGroup v1

在4.0.2版本以前,用户必须手动关闭CGroup v1,编辑配置文件,添加以下内容:

1
2
lxc.cgroup.devices.allow =
lxc.cgroup.devices.deny =

cgroup.relative

lxc.cgroup.relative = 1这一配置项与systemd的Delegate=yes配置项相匹配,它会导致LXC容器的CGroup目录创建并保存在用户当前的会话CGroup(session-$SESSION_ID.scope)下,而不是根CGroup下(只影响非特权容器)。

  • 需要注意的是,这会导致用户的该会话CGroup直到LXC容器停止前都无法被移除。因为会话CGroup下的LXC容器载荷CGroup目录已经委派给LXC了。

使用 lxc@.service 服务单元文件进行管理的容器,和非Root用户通过systemd-run --user --scope -r -p "Delegate=yes" lxc-start -n 容器名 [-F]启动的容器必须启用这一配置项。

非systemd的系统上的问题

Hybrid CGroup

目前,所有使用systemd的Linux发行版已经切换到了纯CGroup v2,也就是unified cgroup,但是仍存在部分非systemd发行版使用的还是兼容的hybrid cgroup,使用以下命令检查:

1
[ "$(stat -fc %T /sys/fs/cgroup/)" = "cgroup2fs" ] && echo "unified" || ( [ -e /sys/fs/cgroup/unified/ ] && echo "hybrid" || echo "legacy")

LXC默认使用和宿主机一样的CGroup模式,如果宿主机操作系统使用hybrid cgroup,那么容器会启动失败,显示:

1
2
Failed to mount cgroup at /sys/fs/cgroup/systemd: Operation not permitted
[!!!!!!] Failed to mount API filesystems.

添加以下配置项,启用CGroup v2可以解决问题:

1
lxc.init.cmd = /sbin/init systemd.unified_cgroup_hierarchy=1

CGroup目录

在非systemd的系统上,用户可能需要手动挂载CGroup目录,用户可以检查系统是否有自动挂载:

1
ls -l /sys/fs/cgroup

如果没有,那么用户可以在/etc/fstab中添加:

1
cgroup  /sys/fs/cgroup  cgroup  defaults  0  0

端口映射

在默认的NAT模式下,可以通过端口映射使得容器可以在外部被访问。当然,这种方法并不是最好的。并不推荐这样做。

iptables

添加规则:

1
iptables -t nat -A PREROUTING -i 公网网卡 -p tcp|udp --dport 主机端口 -j DNAT --to-destination 容器IP:端口

删除规则:

1
iptables -t nat -D PREROUTING -i 公网网卡 -p tcp|udp --dport 主机端口 -j DNAT --to-destination 容器IP:端口

nftables

先创建一个nat表:

1
nft add table ip nat

nat表中创建prerouting链:

1
nft add chain ip nat prerouting {type nat hook prerouting priority dstnat \; policy accept \;}

最后在prerouting链中添加映射规则:

1
nft add rule ip nat prerouting meta iifname 公网网卡 tcp|udp dport 主机端口 dnat to 容器IP:端口

删除规则:

1
nft delete rule ip nat prerouting meta iifname 公网网卡 tcp|udp dport 主机端口 dnat to 容器IP:端口

使用物理网卡

将物理网卡分配给容器而不影响主机使用的一种方法是创建子网卡。
首先创建子网卡:

1
ip addr add 192.168.8.222/24 label enp?s?:? dev enp?s?

然后编辑容器配置:

1
2
lxc.net.1.type = phys
lxc.net.1.link = enp?s?:?

使用自建网桥

用户自建网桥后,可以将主机方的Veth桥接到自己创建的虚拟网桥上,对于自建的网桥,LXC不会进行NAT,用户可以将主机网卡也桥接到网桥上,实现二层交换

首先编辑/etc/defaults/lxc-net(Debian)或/etc/sysconfig/lxc-net(RHEL),禁用默认的网桥:

1
USE_LXC_BRIDGE="false"

创建虚拟网桥:
brctl addbr br?
brctl addif br? enp?s?

ip link add br? type bridge
ip link set enp?s? master br?

给网桥分配IP:
ip addr add 192.168.?.?/24 dev br?
当然,使用自动化的网络管理工具创建网桥也是可以的。

然后编辑容器配置:

1
2
3
lxc.net.0.type = veth
lxc.net.0.link = br?
lxc.net.0.flags = up

SECCOMP

SECCOMP是Linux的系统调用安全模块,使用它可以阻止用户认为不安全的系统调用。

默认情况下,LXC使用/usr/share/lxc/config/common.seccomp配置文件对系统调用进行限制,用户可以设置lxc.seccomp.profile配置项指定另外一个SECCOMP规则文件,规则文件的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 版本号
2
# 黑名单模式
denylist
# 禁止umount -f
reject_force_umount
# 指定架构
[all]
# 阻止指定的系统调用并返回指定的代码
kexec_load errno 1
open_by_handle_at errno 1
init_module errno 1
finit_module errno 1
delete_module errno 1

设置无特权容器

无特权容器不能直接挂载主机上的网络存储和FUSE

对于存储的挂载,用户应当先在宿主机上进行挂载,设定好权限后再设置目录映射。

首先给用户添加子UID和子GID:
usermod 用户名[root] -v|--add-subuids UID段[100000-165535] 添加子UID
usermod 用户名[root] -V|--del-subuids UID段[100000-165535] 删除子UID
usermod 用户名[root] -w|--add-subgids GID段[100000-165535] 添加子GID
usermod 用户名[root] -W|--del-subgids GID段[100000-165535] 删除子GID

或者,直接创建/etc/subuid/etc/subgid,内容均如下:

1
root:1000000:65536

如果只是普通使用,那么子ID可以选择三段,后面的配置项中的ID随子ID的增加而增加:
100000-165535
200000-265535
300000-365535

但是,如果希望在容器内嵌套地创建无特权容器,那么需要设置165536个子ID:

1
root:1000000:165536

如果使用普通用户创建容器,还需要确保内核参数:kernel.unprivileged_userns_clone = 1,写入/etc/sysctl.conf,然后执行sysctl -p即可。

确保权限正确:

如果使用系统级容器:

1
chmod 755 /var/lib/lxc

如果使用用户级容器:

  • setfacl -m u:100000:x /home/$USER
  • setfacl -m u:100000:x /home/.local
  • setfacl -m u:100000:x /home/.local/share

如果是用户级容器,还需要在/etc/lxc/lxc-usernet写入以下内容:

1
用户名 veth lxcbr0 10

然后编辑容器设置,给容器配置用户映射:

1
2
3
# 从左到右依次为:用户或组,容器内ID起点,宿主机ID起点,映射数量
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

还需要设置AppArmor规则,以防权限问题:

1
lxc.apparmor.profile = unconfined

如果是系统级容器,执行lxc-create创建容器,然后执行:

1
lxc-attach -n 容器名 passwd`(需要先启动

或者:

1
chown -R 0:0 /容器根文件系统 && chroot /容器根文件系统 passwd && chown -R 100000:100000 /容器根文件系统

修改密码,然后就可以使用lxc-start启动容器。

用户级容器需要进行额外配置,编辑容器配置文件,添加配置项:

1
lxc.cgroup.relative = 1

然后,执行systemd-run --user --scope [-r] -p "Delegate=yes" lxc-start -n 容器名 [-F]启动容器。

由于非特权容器的特殊性,对于某些目录的挂载是必然失败的,这其中的典型包括sys-kernel-debug.mountsys-kernel-config.mount,对于这些服务,可以考虑删除/etc/fstab中的挂载点,或者直接systemctl mask掉。

登录时报错

在某些使用SysV init的发行版容器上,登录时可能会报错:

1
cannot set terminal process group (-1): Inappropriate ioctl for device

这是因为无特权容器中的进程无权设置终端模式(无权执行ioctl(ftty, TIOCSCTTY, 1)),调用被内核拒绝,导致TTY设备本身状态不正常,之后bash也无法执行ioctl操作。将容器启用特权模式可以避免这个错误,不过意义不大。

创建容器

语法格式为:lxc-create [-B best|dir|lvm|loop|btrfs|zfs|rbd] -t 模板 -n 容器名
默认情况下使用dir即目录保存容器文件系统,用户也可以指定其他的存储方式,包括以下几种:

  • best,自动检测并尝试使用最佳的存储后端。
  • dir,使用普通的目录进行存储,这意味着容器的文件系统与宿主机的文件系统一致,而且大小没有限制。优点是大小是弹性的。
  • loop,回环文件系统。会创建一个镜像文件rootdev进行存储,可以通过--fstype指定文件系统(默认情况下为ext4),--fssize指定镜像最大大小(默认情况下为1G)。缺点是大小是立刻全部分配,而且不能再改变的。
  • lvm,逻辑卷。默认情况下会创建卷组lxc,精简池lxc,1G的容器同名LV,ext4文件系统进行存储。可以通过--vgname指定VG名,--thinpool指定精简池名,--lvname指定LV名,--fstype指定文件系统,--fssize指定卷大小。
  • btrfs,Btrfs子卷。会创建容器的同名子卷进行存储。
  • rbd,远程块设备。不太常用。

创建的容器的根文件系统位于/var/lib/lxc/容器名/rootfs(系统容器)或~/.local/share/lxc/容器名/rootfs(用户容器),要修改容器初始root密码,执行lxc-attach -n 容器名 passwd(需要先启动)或者chroot /容器根文件系统 passwd

创建一个基于Busybox的最小化容器

这是最简单的方法

1
lxc-create [-B best|dir|lvm|loop|btrfs|zfs|rbd] -t busybox -n 容器名 [-- --busybox-path Busybox二进制文件路径]

从网络下载镜像创建容器

这是最常用的方法

1
lxc-create [-B best|dir|lvm|loop|btrfs|zfs|rbd] -t download -n 容器名 -- --server 镜像源 [--dist 发行版] [--release 版本号] [--arch 架构]

使用--server指定镜像源,--表示结束lxc-create的选项,将后面的选项传送给下载脚本。

如果没有指定选项,那么下载时会交互式选择系统,版本和架构。

可用的镜像源包括:

  • 清华大学:mirrors.tuna.tsinghua.edu.cn/lxc-images
  • 北京外国语大学:mirrors.bfsu.edu.cn/lxc-images
  • 腾讯云:mirrors.cloud.tencent.com/lxc-images

执行lxc-create -t download -n clean -- --flush-cache清理本地缓存。

从本地镜像创建容器

一个完整的镜像包括两部分:

  • 元信息包meta.tar.xz
  • 根文件系统包rootfs.tar.xz

lxc-create [-B best|dir|lvm|loop|btrfs|zfs|rbd] -t local -n 容器名 -- --meta 元信息包路径 --fstree 根文件系统包路径

创建OCI容器

LXC还支持直接运行符合OCI标准的容器,只需要执行:

1
lxc-create [-B best|dir|lvm|loop|btrfs|zfs|rbd] -t oci -n 容器名 -- --url 容器URL [--username 仓库用户名 --password 仓库密码]

进入容器

通过一个伪终端对,一头在宿主机的命名空间,一头在容器的命名空间并连接到容器中的指定终端上,实现和容器终端交互。

1
lxc-console -n 容器名 [-t TTY号]
  • 因为容器空间中的伪终端总是pts/0,所以在宿主机上显示的容器中的进程的控制终端总是pts/0
  • 在宿主机视角下,/dev/pts/目录中会按顺序创建两个新的伪终端。
  • 当TTY号为0时,连接到/dev/console。当没有指定TTY号时,LXC会自动选择一个可用的TTY使用。

原理:

  • 在容器中创建一个PTS,容器内对应终端上的进程的标准输入输出连接到PTS,宿主机上的lxc-attach进程的标准输入输出连接到PTMX。

如果容器内使用systemd

systemd在容器环境中自动启动的console-getty.service服务会在/dev/console上初始化getty,因此用户可以直接通过-t 0进入容器进行登录。

但是,因为容器中不存在/dev/tty0设备,所以systemd默认不会在/dev/ttyX上初始化getty(getty@.service服务需要这个条件),因此,直接通过-t X进入容器是不会出现登录提示符的。

用户需要先执行lxc-attach -n 容器名 systemctl start container-getty@X.service,在容器中初始化对应的TTY后,才可以执行lxc-console -n 容器名 -t X进入容器进行登录。

如果容器内不使用systemd

在不使用systemd的容器上,默认不会初始化/dev/console。用户需要确保容器在启动时会初始化/dev/console/dev/ttyX,才能通过-t X进入容器。

对于使用Runit的容器来说,就是确保agetty-ttyXagetty-console)服务存在并且会在开机时自动启动。

对于使用Openrc-init的容器来说,就是确保agetty.ttyXagetty.console)服务存在并且会在开机时自动启动。

对于使用System V init的容器来说,就是确保在/etc/inittab中配置了对应的TTY上初始化getty的行为。

操作

检查配置情况

1
lxc-checkconfig

注:目前,cgroup已经基本全部迁移到v2,所有关于cgroup v1的警告都不必在意。

列出容器

1
lxc-ls [-f|--fancy] [容器名]
  • -1:单列输出。
  • -f|--fancy:显示详细信息。
  • -F|--fancy-format:和-f结合使用,指定详细信息中包含的内容,可选项包括:NAME, STATE, PID, RAM, SWAP, AUTOSTART, GROUPS, INTERFACE, IPV4, IPV6, UNPRIVILEGED
  • --running:仅列出运行中的容器。
  • --stopped:仅列出停止的容器。
  • --frozen:仅列出冻结的容器。

查看容器配置

1
lxc-config -l

查看所有可用配置项。

查看容器信息

1
lxc-info -n 容器名 [-i] [-p] [-S] [-s]
  • -i:显示容器IP。
  • -p:显示容器PID。
  • -S:显示容器占用。
  • -s:显示容器状态。

查看容器内进程

1
lxc-ps -n 容器名

启动容器

1
lxc-start [-F] [-d] -n 容器名
  • -F:启动时立刻进入前台/dev/console,否则默认为-d,即后台启动。

停止容器

1
lxc-stop [-W] [-t] [-k] [-r] -n 容器名
  • -W:立刻返回,不等待容器停止。
  • -t:设置超时,超时后强制杀死容器,单位为秒。
  • -k:直接强制杀死容器。
  • -r:重启容器。

冻结容器

1
lxc-freeze -n 容器名

被冻结的容器会暂停一切进程的运行。

解冻容器

1
lxc-unfreeze -n 容器名

在容器中执行命令

1
lxc-attach [-u] [-g] -n 容器名 [命令]

常用:

  • lxc-attach -n 容器名 -- /bin/bash --login
  • lxc-attach -n 容器名 -- /bin/login -p -f root

若没有命令,默认在容器中以当前用户身份启动Shell。

  • -u:指定容器中执行命令的用户UID,默认为root。
  • -g:指定容器中执行命令的用户GID,默认为root。
  • --keep-env:执行命令时保留所有当前的环境变量,这是默认行为。
  • --clear-env:执行命令时清除所有当前的环境变量。
  • --keep-var:在--clear-env的情况下,保留一个指定的环境变量。
  • -v '变量名=值':手动设定一个环境变量。

原理:

  • 在容器中创建一个伪终端对,容器内创建的进程的标准输入输出连接到从设备,宿主机上的lxc-attach进程的标准输入输出连接到主设备。

创建容器并在容器中执行命令

1
lxc-execute -n 容器名 [命令]

这个命令不常用。

脱离容器
按下Ctrl + A,然后再按Q脱离容器终端。

删除容器

1
lxc-destroy -n 容器名 [-f] [-s]
  • -s:连同删除所有快照。
  • -f:强制删除。

拷贝容器

1
lxc-copy [-M] [-R] -n 容器名 -N 新容器名
  • -R:交换容器名和新容器名。
  • -M:保留容器网卡的MAC地址。

制作快照
容器必须先停止才能创建快照。

1
lxc-snapshot -n 容器名

列出快照

1
lxc-snapshot -n 容器名 -L|--list

恢复快照
容器必须先停止才能恢复快照。

1
lxc-snapshot -n 容器名 -r 快照名 [-N 新容器名]

分配物理设备

1
lxc-device add /dev/设备名 -n 容器名
1
lxc-device del /dev/设备名 -n 容器名

监控所有容器

1
lxc-top [-d X] [-b] [-s n|c|b|m|s|k -r]
  • -d:指定刷新间隔。
  • -b:以软件形式输出。
  • -s:指定排序方式。
  • -r:反向排序。

监控容器状态

1
lxc-monitor -n 容器名 [-Q]
  • -Q:让监控器退出。

等待容器

1
lxc-wait -n 容器名 -t 状态 [-o 超时]

等待容器到达某一状态,可选状态包括:STOPPED, STARTING ,RUNNING, STOPPING, ABORTING。

批量启动

1
lxc-autostart -g 组名[onboot] [-a -A] [-s|-k|-r|-t 5]
  • -g:指定启动的组,如果什么组也没有指定,那么会选择隐藏的NULL组。
  • -a:启动所有组中所有自动启动的容器。
  • -A:和-a搭配使用,无视自动启动设置,强制启动所有容器。
  • -s:关闭容器,而不是启动。
  • -k:杀死容器。
  • -r:重启容器。
  • -t 5:操作的时限,超时会杀死容器。

LXD

LXD是LXC的升级版,它在本质上是使用Go语言编写的LXC的REST API守护进程,以及一套配套的命令行工具。通过REST API,用户可以简化对LXC的管理与进一步开发,而且最重要的是,可以方便有效地实现分布式集群构建

LXD依赖Snap包管理器和Debian系操作系统,这里暂时就不细讲了。

在2023年,LXD被Canonical全面接管,原作者创建了Incus分支维护非Canonical的LXD,值得关注。

Libvirt-LXC

Libvirt也可以用于管理LXC,这是一套独立的引擎,并不需要LXC套件本身被安装,只需要安装libvirtlibvirt-daemon-driver-lxc

这个功能基本上已经过时并且在新版本被Libvirt废弃了,在这里不谈。

lxctl

仿照vzctl语法,使用Perl语言封装的LXC控制脚本,在Debian上可以通过apt install lxctl安装。

配置

配置文件位于/etc/lxctl/lxctl.yaml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
paths:
YAML_CONFIG_PATH: '/etc/lxctl' # 配置文件目录
LXC_CONF_DIR: '/var/lib/lxc' # LXC配置目录
ROOT_MOUNT_PATH: '/var/lib/lxctl/root' # 容器根文件系统保存目录
TEMPLATE_PATH: '/var/lib/lxctl/templates' # 模板保存目录
LXC_LOG_PATH: '/var/log/lxc.log' # 日志目录
LXC_LOG_LEVEL: 'DEBUG' # 日志等级
check:
skip_kernel_config_check: 1 # 跳过内核检查
list: # list默认列出的内容
COLUMNS: 'name,disk_free_mb,status,ip,hostname'
os:
OS_TEMPLATE: ubuntu-10.04-amd64 # 默认容器模板
root:
ROOT_SIZE: 30G # 容器根大小
ROOT_TYPE: file # 容器根类型
fs:
FS: ext4 # 容器文件系统
FS_MOUNT_OPTS: defaults,barrier=0 # 容器文件系统挂载属性
FS_OPTS: '-b 4096' # 容器文件系统挂载选项
lvm: # 如果使用LVM的话,配置使用的VG
VG: vg0
rsync: # 远程连接时的选项
RSYNC_OPTS: "-aH --delete --numeric-ids --exclude 'proc/*' --exclude 'sys/*' -e ssh"
VZ_RSYNC_OPTS: "-aH --delete --numeric-ids --exclude '/proc/*' --exclude '/sys/*' -e ssh"
set:
IFNAME: 'ip'
SEARCHDOMAIN: 'ru'

使用

子命令依次如下:

start 容器名
启动指定容器,没什么好说的。

stop 容器名
停止指定容器,没什么好说的。

create [选项] 容器名
创建一个指定名称的容器,选项如下:
--ostemplate:指定容器的模板
--config:指定配置文件路径,默认使用/etc/lxctl/lxctl.yaml
--macaddr:指定容器的MAC
--ipaddr:指定容器的IP
--mask:指定容器的掩码
--hostname:指定容器的主机名
--defgw:指定容器的网关
--dns:指定容器的DNS
--searchdomain:指定容器的DNS域
--tz:设置容器时区
--root:指定容器根目录
--roottype:指定容器根目录保存方式,可选lvm创建LVM子卷,file创建一个磁盘镜像,raw使用块设备,此时必须指定--device
--rootsz:指定容器根逻辑卷的大小,默认为10G
--autostart:设置容器开机自启
--no-save:默认情况下创建容器后会保存一个容器配置文件在$CONF_PATH/vmname.yaml,禁用这个行为
--load:从yaml文件中读取虚拟机
--debug:显示更多信息
--empty:创建一个完全为空的容器,仅供迁移使用

set [选项] 容器名
修改容器设置,选项如下:
--rootsz:指定容器根逻辑卷的大小,默认为10G,一般情况下只能扩大
--macaddr:指定容器的MAC
--ipaddr:指定容器的IP
--mask:指定容器的掩码
--hostname:指定容器的主机名
--defgw:指定容器的网关
--dns:指定容器的DNS
--searchdomain:指定容器的DNS域
--userpasswd 用户名:密码:设置用户
--onboot yes|no:容器是否开机自启
--tz:设置当前时区
--cpu-shares:设置容器CPU使用占比
--cpus:设置容器可用的CPU核心
--mem:设置容器可用的内存大小,单位为byte
--io:设置容器的IO速度上限

freeze 容器名
冻结容器,没什么好说的。

unfreeze 容器名
解冻容器,没什么好说的。

list [选项]
列出容器,可选选项如下:
--ipaddr:列出IP
--hostname:列出主机名
--cgroup:列出资源配置
--mount:列出根分区路径
--diskspace:列出可用空间
--all:列出全部信息
--raw:只列出容器名

backup 容器名 [选项]
对容器进行远程备份或恢复,选项如下:
备份选项:
--create:创建备份
--tohost:指定远程主机
--todir:远程路径
恢复选项:
--restore:恢复备份
--fromhost:指定远程主机
--fromdir:远程路径
其他选项:
--remuser:远程用户名
--remport:指定端口,默认使用SSH
--remname:远程容器名
--userpasswd 用户名:密码:指定用户密码

migrate 容器名 --tohost 远程主机 [选项]
迁移容器到远程主机,可选选项如下:
--clone:进行克隆
--remuser:远程用户名
--remport:指定端口,默认使用SSH
--remname:远程容器名
--userpasswd 用户名:密码:设置用户
--onboot yes|no:容器是否开机自启
--rootsz:指定容器根逻辑卷的大小,默认为10G,一般情况下只能扩大
--cpu-shares:设置容器CPU使用占比
--cpus:设置容器可用的CPU核心
--mem:设置容器可用的内存大小,单位为byte
--io:设置容器的IO速度上限
--ipaddr:指定容器的IP
--mask:指定容器的掩码
--defgw:指定容器的网关
--dns:指定容器的DNS
--searchdomain:指定容器的DNS域

vz2lxc --fromhost 远程主机 --remname 容器名
迁移远程的VZ主机,选项如下:
--remuser:远程用户名
--remport:指定端口,默认使用SSH
--onboot yes|no:容器是否开机自启
--rootsz:指定容器根逻辑卷的大小,默认为10G,一般情况下只能扩大
--cpu-shares:设置容器CPU使用占比
--cpus:设置容器可用的CPU核心
--mem:设置容器可用的内存大小,单位为byte
--io:设置容器的IO速度上限