从Dockerfile构建Docker镜像

Dockerfile

Docker容器分配静态IPDocker容器中运行ssh服务 我们实现了启动一个富容器(包含了SSH和很多必要工具)、设置静态IP地址、启动时运行多个服务(例如SSH),但是每次去手工从官方镜像开始处理容器效率低下,而自定义镜像也至少需要相对麻烦的手工处理。Docker提供了通过Dockerfile方式构建镜像,类似脚本方式,也就容易实现重放(在脚本基础上改进)。

Dockerfile就是包含用户调用命令行来组装镜像所有命令的一个文本文档,并且可以通过 docker build 来使用这个文档自动创建镜像。

docker build .

这个build过程将通过Docker daemon运行,而不是命令行。最好在一个包含Dockerfile的独立目录中运行上述命令。

警告

不要在根目录下运行 docker build . ,这会导致把整个硬盘内容传输给Docker daemon。

备注

如果目录下有多个文件,为了加快build,可以过滤掉目录下的不需要传递的文件,采用一个 .dockerignore 文件(类似 .gitignore

docker build参数

  • -f /path/to/a/Dockerfile 传递Dockerfile文件名,这样可以不用默认的 Dockerfile 作为文件名,例如:

    docker build -f ubuntu18.04-ssh .
    

也可以使用如下方法(只传递Dockerfile,不传递目录中的文件):

docker build - < ubuntu18.04-ssh
  • -t 标签 ,并且可以使用多个 -t 参数来标记镜像属于多个镜像仓库,例如:

    docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .
    

备注

authorized_keys 公钥文件存放到当前目录下,然后再执行 docker build 命令以构建时把公钥复制到容器内部。

当使用 -f 参数指定Dockerfile来build镜像时,创建的镜像是没有 REPOSITORYTAG 的,所以引用起来非常不便,类似:

REPOSITORY              TAG                  IMAGE ID            CREATED             SIZE
<none>                  <none>               845219589871        12 hours ago        277MB

强烈 建议结合 -f-t 参数,这样就可以对镜像标记:

docker build -f ubuntu18.04-ssh -t local:ubuntu18.04-ssh .

docker build -f centos8-ssh -t local:centos8-ssh .

这样创建的镜像具有仓库和标签,显示如下:

REPOSITORY              TAG                  IMAGE ID            CREATED             SIZE
local                   ubuntu18.04-ssh      1db298cb83f2        2 hours ago         278MB
local                   centos8-ssh          389c6ed34b34        16 minutes ago      286MB

备注

从 18.09 版本开始,Docker支持在后台执行build过程,这样可以并行执行多个build,并且可以检测并跳过不实用对build状态。这个实现是通过 moby/buildkit 项目实现对。要使用 BuildKit 后端,需要设置环境变量 DOCKER_BUILDKIT=1 ,然后再执行 docker build

集成SSH的镜像制作Dockerfile

备注

Dockerfile案例分为Ubuntu和CentOS

dockerfile/ubuntu18.04-ssh
# Build ubuntu18.04 image with ssh:
# ---------------------------------
# docker build -f ubuntu18.04-ssh -t local:ubuntu18.04-ssh .

# create container:
# -----------------
# docker run -itd -p 1122:22 --hostname myserver --name myserver local:ubuntu18.04-ssh

# create container with volume:
# -----------------------------
# docker volume create data
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data local:ubuntu18.04-ssh

# create container with volume and static ip:
# -------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.11 local:ubuntu18.04-ssh

# create container with volume and static ip, then limit resource and map port:
# -----------------------------------------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.11 --memory=2048M --cpus="1.5" \
#   -p 2211:22 local:ubuntu18.04-ssh

FROM docker.io/ubuntu:latest
MAINTAINER vincent huatai <vincent@huatai.me>

RUN apt update
RUN apt -y upgrade
RUN apt -y install openssh-server iproute2 net-tools sudo vim screen dnsutils gnupg iputils-ping

# Prepare sshd host key
RUN ssh-keygen -A
RUN mkdir /run/sshd

# add account "huatai" and give sudo vprivilege
# groupid 20(dialout=>staff) userid 501
RUN groupdel staff
RUN groupmod --new-name staff dialout
RUN useradd -g 20 -u 501 -d /home/huatai -m -s /bin/bash huatai
RUN usermod -aG sudo huatai
RUN sed -i '/^%sudo/d' /etc/sudoers
RUN echo "%sudo   ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers

# Add ssh public key for login
RUN mkdir -p /home/huatai/.ssh
COPY authorized_keys /home/huatai/.ssh/authorized_keys
RUN chown -R huatai:staff /home/huatai/.ssh
RUN chmod 600 /home/huatai/.ssh/authorized_keys
RUN chmod 700 /home/huatai/.ssh

# run service when container started - sshd
EXPOSE 22:1122
#CMD ["/usr/sbin/sshd", "-D"]

# ----------
# WANT run sshd and get a bash
# ENTRYPOINT will not be override by commandline
# ----------
ENTRYPOINT /usr/sbin/sshd && /bin/bash

备注

  • 安装了openssh服务之后,需要创建主机key才能启动服务,所以需要执行 RUN ssh-keygen -A

  • 将host主机上 authorized_keys 复制到镜像中,这里可以使用 COPY 指令或 ADD 指令。推荐使用 COPYADD 指令用于添加 tar.gz 文件,可以自动展开: ADD rootfs.tar.xz / ) 。请参考 Dockerfile COPY vs ADD: key differences and best practices

  • EXPOSE 指令用来将指示容器中运行端口输出到host主机,这样其他人在使用 docker inspect 检查镜像时就能够发现这个端口映射提示(hint)。但是,需要 注意 :如果没有在 docker run 命令显式使用 -p 2211:22 ,则实际从镜像创建的容器依然是没有输出端口映射的。 请参考 What is the use of EXPOSE in Docker file

  • 虽然能够使用 CMD ["/usr/sbin/sshd", "-D"] 在镜像最后启动一个sshd服务,但是在Dockerfile中,只能执行一次 CMD ,多条 CMD 时后面的命令会覆盖前面的命令(即只有最后一条命令生效);所以如果有多条命令需要运行,则需要使用 ENTRYPOINT

警告

虽然可以使用 docker build - < ubuntu18.04-ssh 来构建镜像,但是不如使用 docker build -f ubuntu18.04-ssh . 。因为后者可以从当前目录复制文件到容器中(使用 COPY 指令或者 ADD 指令),而前者会失去当前目录引用,复制文件会失败。

备注

如果要在容器内部使用某个用户身份执行命令,则在命令前先使用 USER 指令切换,例如 USER huatai 切换。类似 Python的 virtualenv 环境创建,都需要先切换身份再执行指令。

dockerfile/centos7-ssh
# Build centos 7 image with ssh:
# ------------------------------
# docker build -f centos7-ssh -t local:centos7-ssh

# create container:
# -----------------
# docker run -itd -p 1122:22 --hostname myserver --name myserver local:centos7-ssh

# create container with volume:
# -----------------------------
# docker volume create data
# docker run -itd -p 1122:22--hostname myserver --name myserver -v data:/data local:centos7-ssh

# create container with volume and static ip:
# -------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.11 local:centos7-ssh

# create container with volume and static ip, then limit resource and map port:
# -----------------------------------------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.11 --memory=2048M --cpus="1.5" \
#   -p 22 -p 8080:80 local:centos7-ssh

FROM docker.io/centos:7
MAINTAINER vincent huatai <vincent@huatai.me>

RUN yum clean all
RUN yum -y update

RUN yum -y install which sudo openssh-clients openssh-server initscripts

# Prepare sshd host key
RUN ssh-keygen -A

# add account "admin" and give sudo privilege
RUN groupadd -g 505 admin
RUN useradd -g 505 -u 505 -d /home/admin -m admin
RUN usermod -aG wheel admin
RUN echo "%wheel        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers

# Add ssh public key for login
RUN mkdir -p /home/admin/.ssh
COPY authorized_keys /home/admin/.ssh/authorized_keys
RUN chown -R admin:admin /home/admin/.ssh
RUN chmod 600 /home/admin/.ssh/authorized_keys
RUN chmod 700 /home/admin/.ssh

# run service when container started - sshd
EXPOSE 22:1122
#CMD ["/usr/sbin/sshd", "-D"]

# ----------
# WANT run sshd and get a bash
# ENTRYPOINT will not be override by commandline
# ----------
ENTRYPOINT /usr/sbin/sshd && /bin/bash
dockerfile/centos8-ssh
# Build centos 8 image with ssh:
# ------------------------------
# docker build -f centos8-ssh -t local:centos8-ssh

# create container:
# -----------------
# docker run -itd -p 1122:22 --hostname myserver --name myserver local:centos8-ssh

# create container with volume:
# -----------------------------
# docker volume create data
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data local:centos8-ssh

# create container with volume and static ip:
# -------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.252 local:centos8-ssh

# create container with volume and static ip, then limit resource and map port:
# -----------------------------------------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.252 --memory=2048M --cpus="1.5" \
#   -p 22 -p 8080:80 local:centos8-ssh

FROM docker.io/centos:8
MAINTAINER vincent huatai <vincent@huatai.me>

RUN dnf clean all
RUN dnf -y update

RUN dnf -y install which sudo openssh-clients openssh-server initscripts

# Prepare sshd host key
RUN ssh-keygen -A

# add account "admin" and give sudo privilege
RUN groupadd -g 505 admin
RUN useradd -g 505 -u 505 -d /home/admin -m admin
RUN usermod -aG wheel admin
RUN echo "%wheel        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers

# Add ssh public key for login
RUN mkdir -p /home/admin/.ssh
COPY authorized_keys /home/admin/.ssh/authorized_keys
RUN chown -R admin:admin /home/admin/.ssh
RUN chmod 600 /home/admin/.ssh/authorized_keys
RUN chmod 700 /home/admin/.ssh
RUN mv /var/run/nologin /var/run/nologin.bak

# run service when container started - sshd
EXPOSE 22:1122
#CMD ["/usr/sbin/sshd", "-D"]

# ----------
# WANT run sshd and get a bash
# ENTRYPOINT will not be override by commandline
# ----------
ENTRYPOINT /usr/sbin/sshd && /bin/bash

警告

2021年12月31日,CentOS 8系列终止更新,所以在 nerdctl 中使用dokcer.io官方镜像已经无法正常更新。解决方法是切换到 CentOS Stream 8 。以下为修订后Dockerfile

2021年12月31日之后需要切换到CentOS Stream 8
# Build centos 8 image with ssh:
# ------------------------------
# docker build -f centos8-ssh -t local:centos8-ssh

# create container:
# -----------------
# docker run -itd -p 1122:22 --hostname myserver --name myserver local:centos8-ssh

# create container with volume:
# -----------------------------
# docker volume create data
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data local:centos8-ssh

# create container with volume and static ip:
# -------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.252 local:centos8-ssh

# create container with volume and static ip, then limit resource and map port:
# -----------------------------------------------------------------------------
# docker volume create data
# docker network create --subnet=172.18.0.0/16 data-net
# docker run -itd -p 1122:22 --hostname myserver --name myserver -v data:/data \
#   --net data-net --ip 172.18.0.252 --memory=2048M --cpus="1.5" \
#   -p 22 -p 8080:80 local:centos8-ssh

FROM centos:latest
MAINTAINER vincent huatai <vincent@huatai.me>

RUN dnf clean all
RUN dnf --disablerepo '*' --enablerepo extras swap centos-linux-repos centos-stream-repos -y
RUN dnf distro-sync -y
RUN dnf -y update

RUN dnf -y install which sudo openssh-clients openssh-server initscripts

# Prepare sshd host key
RUN ssh-keygen -A

# add account "admin" and give sudo privilege
RUN groupadd -g 505 admin
RUN useradd -g 505 -u 505 -d /home/admin -m admin
RUN usermod -aG wheel admin
RUN echo "%wheel        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers

# Add ssh public key for login
RUN mkdir -p /home/admin/.ssh
COPY authorized_keys /home/admin/.ssh/authorized_keys
RUN chown -R admin:admin /home/admin/.ssh
RUN chmod 600 /home/admin/.ssh/authorized_keys
RUN chmod 700 /home/admin/.ssh
RUN mv /var/run/nologin /var/run/nologin.bak

# run service when container started - sshd
EXPOSE 22:1122
#CMD ["/usr/sbin/sshd", "-D"]

# ----------
# WANT run sshd and get a bash
# ENTRYPOINT will not be override by commandline
# ----------
ENTRYPOINT /usr/sbin/sshd && /bin/bash

备注

Docker systemd进程管理器 需要比较繁杂的配置(探索),不过现在 CentOS DockerHUB官方镜像 也提供了非常方便的 Docker systemd进程管理器 (详见该文Dockerfile)

alpine linux ssh容器

随着云计算发展, 边缘云计算构建 成为轻量级容器技术的运用场景。使用 Alpine Linux 实现精简的Linux容器可以最大化系统硬件性能发挥,同时降低系统被攻击面。

我在 K3s - 轻量级Kubernetes 部署中采用 Alpine Linux 构建运行容器,采用如下Dockerfile构建支持ssh的容器:

  • 使用密码登陆的ssh容器 Dockerfile :

dockerfile/alpine-ssh
FROM alpine:latest
RUN apk add --update --no-cache openssh sudo
RUN apk update && apk upgrade
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
RUN adduser -u 502 -G wheel -h /home/huatai -s /bin/sh -D huatai
RUN echo -n 'huatai:some_password_here' | chpasswd
RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 22
COPY entrypoint.sh /
  • 并准备 entrypoint.sh :

dockerfile/entrypoint.sh
#!/bin/sh
ssh-keygen -A
exec /usr/sbin/sshd -D -e "$@"
  • 然后执行构建镜像命令:

    chmod +x -v entrypoint.sh
    docker build -t alpine-ssh .
    
  • 使用以下命令启动容器:

    docker run -itd --hostname x-dev --name x-dev -p 122:22 alpine-ssh:latest
    

上述Dockerfile参考 How to install OpenSSH server on Alpine Linux (including Docker) ,原文提供的Dockerfile方法非常精简,我做了一点点修改:

  • 增加sudo,并配置普通用户 huatai 能够免密码sudo

启动容器

  • 既然已经构建成功image,现在可以非常容易就启动容器:

    docker run -it -d --hostname ubuntu18 --name ubuntu18 local:ubuntu18.04-ssh
    

此时使用 docker ps 可以看到新启动的容器:

CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS               NAMES
1a1535ce00ed        local:ubuntu18.04-ssh   "/bin/sh -c '/usr/sb…"   3 seconds ago       Up 2 seconds        22/tcp              ubuntu18
  • 启动上述 centos8-ssh 案例命令:

    docker run -it -d -p 1122:22 --hostname studio --name studio local:centos8-ssh
    

启动后检查 docker ps 可以看到:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                            NAMES
f4928fa6c20d        local:centos8-ssh   "/bin/sh -c '/usr/sb…"   2 seconds ago       Up 2 seconds        1122/tcp, 0.0.0.0:1122->22/tcp   studio"'"

登陆

为了方便ssh登陆,可以在 ~/.ssh/config 中添加配置:

Host studio
    HostName 127.0.0.1
    Port 1122
    User huatai

但是 ssh stuido 提示错误:

"System is booting up. Unprivileged users are not permitted to log in yet. Please come back later. For technical details, see pam_nologin(8)."

原因可以通过 man pam_nologin 获得:

pam_nologin是一个PAM模块,当 /var/run/nologin/etc/nologin 文件存在时,就会拒绝用户登陆。这个文件的内容就是显示给被拒绝用户的内容。但是pam_nologin模块对root用户登陆不影响。

果然,我发现CentOS 8的Docker镜像中默认包含了一个 /var/run/nologin ,是用来防范Docker容器使用ssh服务登陆的。只需要移除这个文件就可以。所以,还需要修订Dockerfile自动移除这个文件。

参考