曾经,在挂载了/var/run/docker.sock
之后,在Docker容器中即可控制整个Docker Daemon了。此时,在这个容器中,便有了启动更多容器的可能,配上一个合适的hostname,会让使用者在使用时,陷入“盗梦空间”式迷雾之中。
既然一个容器提供的cli可以无限接近一台实机,那么其实不妨跳出容器的哲学,让它在可控的范围之内运行一个完整的init,以提供一部分类似虚拟机的功能,比如服务,或者cron、rsyslog、DBus一类的。
必备条件
运行这个Systemd容器,必备的条件有以下的几点:
- 系统支持CGroup,并挂载
- 拥有
SYS_ADMIN
能力 - 禁用对容器的安全上下文限制
不过,如果你使得这个容器进入特权模式,也是可取的。
Systemd在使用CGroup时,会用以区分不同的Slice的进程,从而对其资源消耗进行统计。
Dockerfile参考
FROM ubuntu:18.04
# necessary envs
ENV container docker
ARG LC_ALL=C
ARG DEBIAN_FRONTEND=noninteractive
# modify source.list
RUN sed -i 's/# deb/deb/g' /etc/apt/sources.list \
&& sed -i 's/archive.ubuntu.com/mirrors.163.com/g' /etc/apt/sources.list
# install systemd and ssh
RUN apt-get update \
&& apt-get install -y systemd ubuntu-minimal \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# remove unnecessary units
RUN rm -f /lib/systemd/system/sysinit.target.wants/*.mount \
&& systemctl disable networkd-dispatcher.service
STOPSIGNAL SIGRTMIN+3
WORKDIR /
VOLUME ["/sys/fs/cgroup", "/tmp", "/run", "/run/lock"]
CMD ["/sbin/init"]
运行相关
在上述的Dockerfile中,有许多内容是与Systemd在Docker下运行息息相关的。
ENV container docker
STOPSIGNAL SIGRTMIN+3
VOLUME ["/sys/fs/cgroup", "/tmp", "/run", "/run/lock"]
CMD ["/sbin/init"]
第一行指定了环境变量container="docker"
,经测试,这一条环境变量会使得Systemd识别到Docker是它的虚拟化环境。同时,也会使得对init
发送信号SIGRTMIN+3
可以使Systemd正常关闭,也就是第二行里面的STOPSIGNAL SIGRTMIN+3
。
第三行里面指定了四个Volumes。其中,/sys/fs/cgroup
是系统CGroup的挂载点,Systemd依赖CGroup,这里显式地提示用户挂载CGroup目录(只读即可)。同时,Systemd会尝试在以下三个路径挂载tmpfs:
/tmp
/run
/run/lock
之后容器镜像在启动时,是需要运行Systemd进程的,往往系统都会将其软链接到/sbin/init
中以兼容旧的Sysvinit模式。因而运行这个路径即可启动容器中的init也就是Systemd。
镜像构建相关
镜像构建过程,影响到的变量有这两个:
ARG LC_ALL=C
ARG DEBIAN_FRONTEND=noninteractive
第一个LC_ALL=C
会将容器的地区设置为无,兼容所有基本的地区设置。
第二个DEBIAN_FRONTEND=noninteractive
告知apt
相关应用目前的进程是不可交互的,避免出现需要变更配置文件或者交互式配置情况出现,从而保证构建顺利进行。
不必要的unit
在上文已经阐述过了,既然是在容器内又无特权,那么需要将一部分和完全虚拟相关的部分——挂载去除。因而可以直接删除*.mount
型unit。同时,因为容器网络由外部管理,其也没有NET_ADMIN
的权限,无需响应变更,所以可以去除networkd-dispatcher.service
这个unit。
# remove unnecessary units
RUN rm -f /lib/systemd/system/sysinit.target.wants/*.mount \
&& systemctl disable networkd-dispatcher.service
运行方式
运行方式这里,只提出docker-compose
的方法,使用命令或者像Nomad一样的调度平台也是类似的。
version: "2.1"
services:
test:
tty: true
build: ./build/bionic
restart: on-failure
network_mode: bridge
mem_limit: 1G
security_opt:
- seccomp=unconfined
cap_add:
- SYS_ADMIN
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
- /run/lock
对于一些security_opt
、cap_add
、volumes
以及tmpfs
在上文其实也已经讲述过许多了。这里主要讲一个tty
的问题。
这个很简单,就是只有分配了tty
,Systemd才会在前台模式打出启动过程方便调试。
后记
在Docker里运行Systemd其实是一种禁术,使用之前记得评估自己的使用场景,合理使用。同时,如果将hostname
也挂载进来,请提供一种方式来判断自己处于容器中抑或是宿主机上,以防造成误操作。
yulin
帅!
FANG
有一说一,Docker 里面跑 systemd 确实牛逼
FANG
嗯,测试了一下,实际上这种玩法在Debian buster 以及之前的版本是可行的,因为内核默认用的 cgroupv1,但是从 Debian bullseye 之后内核默认用的是 cgroupv2,对于一些比较旧的镜像(比如 CentOS 7)自带的 systemd 还不支持 cgroupv2,就会出现问题。实际上从 cgroupv2 开始就不应该把 /sys/fs/cgroup 映射到容器内了。具体怎么操作可以参考这个GitHub链接:https://gist.github.com/pinkeen/bba0a6790fec96d6c8de84bd824ad933
以及这个链接 https://serverfault.com/questions/1053187/systemd-fails-to-run-in-a-docker-container-when-using-cgroupv2-cgroupns-priva/1054414#1054414