背景

在最开始使用Docker容器的时候,一直把Docker当作一个Linux虚拟机,后来用久了发现Docker只会起来一个特殊的进程Pid=1,Dockerfile里有两个关键字可以指定指令,ENTRYPOINTCMD。当主进程退出的时候容器所拥有的PID命名空间就会被销毁,容器生命周期也会结束。
但是有时候需要在Docker容器中运行多个进程,基本做法用shell或者第三方守护进程(dumb-inittiniMonitSupervisorskawares6-overlayrunitSystemd)作为主进程运行其他程序。

S6介绍

S6使用

这里主要介绍s6-overlay使用

  • s6-overlay生命周期:
    • step1:容器初始化会启动s6-svscan作为pid1
    • step2:s6-svscan扫描服务目录并由s6-supervise执行相应脚本和服务
      • 1、使用修复所有权和权限/etc/fix-attrs.d
      • 2、执行中包含的初始化脚本/etc/cont-init.d
      • 3、将服务(/etc/services.d)复制到s6文件夹下,并向s6-supervise发出信号,并交由s6-supervise接管。
    • step3:收到docker stop,转送s6-supervise进程,停止服务并执行终结脚本/etc/cont-finish.d,确保内置服务不会成为僵尸进程。

依照文档描述,系统启动会扫描几个文件夹

  • /etc/fix-attrs.d : 权限脚本文件夹
  • /etc/cont-init.d :初始化脚本文件夹
  • /etc/services.d : 服务启动执行脚本文件夹
  • /etc/cont-finish.d : 服务结束执行脚本文件夹

脚本例子

权限脚本

linux下执行程序需要很多权限授权,例如文件挂载后需要对挂载文件进行赋予权限等,需要在容器启动后操作。s6-overlay提供了初始化脚本文件夹/etc/fix-attrs.d。匹配格式如下:

1
path(路径) recurse(是否嵌套) account(用户名) fmode(文件模式) dmode(目录模式)
  • path: 文件或路径
  • recurse: 如果时文件夹,是否嵌套所有子文件
  • account: 用户名。如果用户没找到就按照默认UID和GID
  • fmode: 文件模式。如:0644
  • dmode: 目录模式。如:0755

举例:

1
/var/lib/mysql true mysql 0600 0700

初始化脚本

执行完权限脚本(/etc/fix-attrs.d/)后,在启动服务(/etc/services.d/)前可以做一些准备工作,如环境变量设置、文件夹创建等。

举例:

/etc/cont-init.d/02-confd-onetime:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/execlineb -P

with-contenv
s6-envuidgid nginx
multisubstitute
{
import -u -D0 UID
import -u -D0 GID
import -u CONFD_PREFIX
define CONFD_CHECK_CMD "/usr/sbin/nginx -t -c {{ .src }}"
}
confd --onetime --prefix="${CONFD_PREFIX}" --tmpl-uid="${UID}" --tmpl-gid="${GID}" --tmpl-src="/etc/nginx/nginx.conf.tmpl" --tmpl-dest="/etc/nginx/nginx.conf" --tmpl-check-cmd="${CONFD_CHECK_CMD}" etcd

服务启动脚本

  • 自定义启动脚本

/etc/services.d/myapp/run:

1
2
3
#!/usr/bin/execlineb -P

nginx -g "daemon off;"
  • 自定义重启策略

守护进程默认是自动重启服务,如果想要不自动重启可以在finish脚本里写终止

/etc/services.d/myapp/finish:

1
2
3
#!/usr/bin/execlineb -S0

s6-svscanctl -t /var/run/s6/services
  • 可以实现更高级做法,当服务崩溃,就不再重启

/etc/services.d/myapp/finish:

1
2
3
4
5
#!/usr/bin/execlineb -S1
if { s6-test ${1} -ne 0 }
if { s6-test ${1} -ne 256 }

s6-svscanctl -t /var/run/s6/services

日志输出

s6-overlay采用s6已经提供了开箱即用的s6-log,在这基础上提供了进一步封装logutil-service,主要提供以下几个功能

  • 在s6-log中执行环境变量S6_LOGGING_SCRIPT中的脚本
  • 移除权限,任何人都可以写文件,不需要再通过s6-setuidgid来启动
  • 清除所有环境变量
  • 初始化s6-log日志程序

在初始化时候创建日志文件夹和任意操作权限

/etc/cont-init.d/myapp-logfolder:

1
2
3
#!/bin/sh
mkdir -p /var/log/myapp
chown nobody:nogroup /var/log/myapp

输出stdin所有日志

/etc/services.d/myapp/log/run:

1
2
#!/bin/sh
exec logutil-service /var/log/myapp

Docker基础镜像

Dockerfile文件参考smebberson/docker-alpine,我增加了镜像时区设置

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
FROM alipine:3.9
LABEL [email protected]

# Add s6-overlay
ENV S6_OVERLAY_VERSION=v1.22.1.0 \
GO_DNSMASQ_VERSION=1.0.7 \
TIME_ZONE=Asia/Shanghai

RUN apk add --update --no-cache bind-tools curl libcap && \
curl -sSL https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-amd64.tar.gz \
| tar xfz - -C / && \
curl -sSL https://github.com/janeczku/go-dnsmasq/releases/download/${GO_DNSMASQ_VERSION}/go-dnsmasq-min_linux-amd64 -o /bin/go-dnsmasq && \
chmod +x /bin/go-dnsmasq && \
apk del curl && \
# create user and give binary permissions to bind to lower port
addgroup go-dnsmasq && \
adduser -D -g "" -s /bin/sh -G go-dnsmasq go-dnsmasq && \
setcap CAP_NET_BIND_SERVICE=+eip /bin/go-dnsmasq

RUN ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime && \
echo "${TIME_ZONE}" > /etc/timezone

COPY root /

ENTRYPOINT ["/init"]
CMD []

root文件下目录结构

1
2
3
4
5
6
7
8
9
10
11
├─ configroot
│ ├─etc
│ │ ├─fix.attrs.d
│ │ │ ├─01-resolver-resolv
│ │ ├─cont.init.d
│ │ │ ├─30-resolver
│ │ │ ├─40-resolver
│ │ ├─services.d
│ │ │ ├─resolver
│ │ │ │ ├─run
│ │ │ │ ├─finish

构建S6程序

Dockerfile

这里拿Hexo作为守护服务

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
FROM dobor/alpine-base:latest
LABEL [email protected]

ENV HEXO_MODE=server

# change ALIYUN apk source
#RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

RUN addgroup hexo && \
adduser -D -g "" -s /bin/sh -G hexo hexo

WORKDIR /home/hexo

RUN apk --update --no-progress --no-cache add git nodejs npm openssh && \
npm config set registry https://registry.npm.taobao.org && \
npm install -g hexo-cli && \
hexo init . && \
npm install hexo-deployer-git && \
#npm install hexo-generator-json-content && \
npm install hexo-tag-aplayer && \
npm install [email protected] && \
npm install hexo-renderer-pug && \
npm install hexo-renderer-stylus && \
npm install hexo-wordcount && \
npm install hexo-abbrlink && \
rm -rf /var/cache/apk/*

# copy local files
ADD root /

VOLUME /home/hexo/source /home/hexo/themes /home/hexo/.ssh

RUN chown -R hexo .

EXPOSE 4000

目录结构

新增/root文件结构

1
2
3
4
5
6
7
8
9
10
├─ configroot
│ ├─etc
│ │ ├─fix.attrs.d
│ │ │ ├─02-hexo
│ │ ├─cont.init.d
│ │ │ ├─10-hexo
│ │ │ ├─20-hexo
│ │ ├─services.d
│ │ │ ├─hexo
│ │ │ │ ├─run

权限

git配置文件.config赋予访问权限

/etc/fix-attrs.d/02-hexo:

1
/root/.config true root 0666 0666

初始化

设置执行命令的HEXO_RUNAS环境变量

/etc/cont-init.d/10-hexo:

1
2
3
4
5
6
#!/usr/bin/with-contenv sh

# Unless this has already been defined, set it.
if [ -z "$HEXO_RUNAS" ]; then
printf "hexo" > /var/run/s6/container_environment/HEXO_RUNAS
fi

生成博客静态文件和启用插件

/etc/cont-init.d/20-hexo:

1
2
3
4
5
6
7
#!/usr/bin/with-contenv sh

# Generate Blog
exec s6-setuidgid $HEXO_RUNAS hexo g -f --cwd /home/hexo

# Generate Douban Page
exec s6-setuidgid $HEXO_RUNAS hexo douban --cwd /home/hexo

服务启动

通过环境变量HEXO_MODE来作为web站点服务还是发布静态文件到GitHub上。

/etc/services.d/run:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/with-contenv sh

if [ $HEXO_MODE = 's' ] || [ $HEXO_MODE = 'server' ]; then
# Start Hexo Server.
exec s6-setuidgid $HEXO_RUNAS hexo server -p 4000 --cwd /home/hexo
fi

if [ $HEXO_MODE = 'd' ] || [ $HEXO_MODE = 'deploy' ]; then
# Start Hexo Deploy.
exec s6-setuidgid $HEXO_RUNAS hexo deploy --cwd /home/hexo
fi

总结

通过把s6整合进hexo镜像内部,使得hexo容器更新一个稳定的微型linux服务器,既保留容器占用资源低,启动快等特点,又使得容器内部可以执行多进程,可以整合更复杂的功能于一个容器内。