Alpine Docker镜像

作为兼顾安全和轻量级的 Alpine LinuxDocker 默认的基础镜像,能够极大地缩减Docker镜像,并且降低运行资源,真正体现容器的轻量、灵活。

如果你像我一样,尝试从0开始在 ARM 架构( 例如 Apple ARM架构芯片M1 ProRaspberry Pi )构建自己的 移动云计算构建 ,那么你可以从最精简的 Alpine Linux 基础镜像开始,只在镜像中添加和运行程序直接相关的库和工具,避免引入不必要的系统开销和安全隐患。

Alpine Docker官方镜像

Alpine DOI(Docker Official Image)是一个包含执行软件堆栈的Alpine Linux Docker镜像,包含你的源代码,库,工具以及应用程序运行的必要核心依赖。

和其他Linux发行版不同:

  • Alpine基于 musl libc 实现C标准库

  • Alpine使用 BusyBox (用一个执行程序替代一组核心功能程序)替代 GNU coreutils

Alpine Linux 吸引了不需要强制性兼容和功能的开发人员,并且提供了友好和直接的使用体验(没有复杂的软件组合)。

采用 Alpine Linux 构建的运行容器镜像都非常精简:

alpine linux镜像大小

镜像内容

镜像大小

alpine-base

11MB

alpine-bash

13MB

alpine-nginx

14.5MB

基础运行 alpine-base

  • alpine-base 目录下 Dockerfile :

基础alpine linux镜像Dockerfile
FROM alpine:latest
RUN apk update && apk upgrade

ENTRYPOINT ["/bin/ash"]

警告

这里你很可能和我一样遇到GFW屏蔽docker registry导致的报错:

由于docker registry被屏蔽的报错
[+] Building 30.8s (2/2) FINISHED                                                              docker:rancher-desktop
 => [internal] load build definition from Dockerfile                                                             0.4s
 => => transferring dockerfile: 111B                                                                             0.0s
 => ERROR [internal] load metadata for docker.io/library/alpine:latest                                          30.0s
------
 > [internal] load metadata for docker.io/library/alpine:latest:
------
Dockerfile:1
--------------------
   1 | >>> FROM alpine:latest
   2 |     RUN apk update && apk upgrade
   3 |
--------------------
ERROR: failed to solve: alpine:latest: failed to resolve source metadata for docker.io/library/alpine:latest: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/latest": dial tcp 157.240.8.50:443: i/o timeout

请参考 配置Rancher Desktop的Docker Daemon 或者 配置Docker使用代理

  • 构建 alpine-base 镜像:

构建基础alpine linux镜像
docker build -t alpine-base .
  • 如果在本地Docker中运行,则直接执行:

运行alpine linux基础镜像的容器
docker run --name alpine -it alpine-base

基础运行 alpine-bash

备注

很多运维人员习惯使用 bash ,所以也可以构建支持 bashAlpine Linux

  • alpine-bash 目录下 Dockerfile :

提供bash的alpine linux镜像Dockerfile
FROM alpine:latest
RUN apk update && apk upgrade
RUN apk add --no-cache bash

ENTRYPOINT ["/bin/bash"]
  • 构建 alpine-bash 镜像:

构建提供bash的alpine linux镜像
docker build -t alpine-bash .
  • 运行 alpine-bash :

运行提供bash的alpine linux容器
docker run --name alpine-bash -it alpine-bash

NGINX服务 alpine-nginx

备注

提供nginx服务的alpine镜像: 我准备将数据存放在 /data 目录下( Kubernetes ):

  • html 子目录存放 WEB 内容( ZFS NFS )

  • nginx 软件包安装后默认 nginx.conf 将读取的 /etc/nginx/http.d 目录下配置,其中 /etc/nginx/httpd.defult.conf 配置可覆盖修改以使用自己定制的数据目录

  • alpine-nginx 目录下 Dockerfile :

提供nginx的alpine linux镜像Dockerfile
FROM alpine:latest
RUN apk update && apk upgrade
RUN apk add --no-cache bash
RUN apk add --no-cache nginx

COPY default.conf /etc/nginx/http.d/
RUN mkdir -p /data/html
COPY index.html /data/html/

EXPOSE 80/tcp

ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]

备注

修订默认的 /etc/nginx/http.d/default.conf 是在 Kubernetes 中运行自定义AlpineLinux NGINX的重要一步

Dockerfile ENTRYPOINT 和 CMD 辨析

  • 构建 alpine-nginx 镜像:

构建提供nginx的alpine linux镜像
docker build -t alpine-nginx .
  • 注意: 默认的 default.conf :

默认nginx的网站配置 /etc/nginx/http.d/default.conf
erver {
        listen 80 default_server;
        listen [::]:80 default_server;

        # Everything is a 404
        location / {
                return 404;
        }

        # You may need this to prevent return 404 recursion.
        location = /404.html {
                internal;
        }
}

必须被配置允许访问的 default.conf 替换,否则类似 在kind运行简单的容器 无法启动(见下文)

  • 简单的运行 alpine-nginx 验证:

简单地运行alpine-nginx验证镜像
docker run -p 8080:80 --name cloud-atlas \
    -v /home/huatai/docs/docker/alpine/nginx/default.conf:/etc/nginx/http.d/default.conf \
    -v /home/huatai/docs/github.com/cloud-atlas/build/html:/data/html \
    alpine-nginx

现在验证通过的NGINX镜像,是否就可以用到 在kind运行简单的容器 呢?

不能在Kubernetes启动的 alpine-nginx

实际 Alpine Linux 出于安全考虑(这是一个注重安全的面向嵌入式平台的发行版),将 default.conf 设置成禁止访问(直接返回 404 ),而不是一般发行版默认允许访问 index.html

这导致我在 在kind运行简单的容器 遇到始终出现 Kubernetes健康检测 失败的 Kubernetes pod CrashLoopBackOff错误排查

解决的方法也很简单,就是准备好一个允许正常访问的 default.conf 在 BUILD 时候覆盖镜像中的配置文件,这个 default.conf 可以如下:

修订后nginx的网站配置 /etc/nginx/http.d/default.conf,可以让nginx正常运行满足Kubernetes健康检查
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        location / {
                root   /data/html;
                index  index.html index.htm;
        }

        # You may need this to prevent return 404 recursion.
        location = /404.html {
                internal;
        }
}

下一步

我将部署到 kind(本地docker模拟k8s集群) 集群中来模拟 Kubernetes 运行:

SSH服务 alpine-ssh

备注

参考 Debian镜像(tini进程管理器) 配置 alpine-ssh 镜像

  • 使用 tini 进程管理器启动需要的服务 ssh cron :

采用 tini 进程管理启动ssh服务
FROM alpine:latest

ENV container=docker

RUN apk update && apk upgrade

# Alpine Linux 仓库内置tini,可以直接安装
RUN apk add --no-cache tini

# Copy tini entrypoint script
COPY entrypoint_ssh_cron_ash /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Devops utilities
RUN apk add --no-cache openssh sudo bind-tools tmux

# add account "admin" and give sudo privilege
RUN adduser -u 501 -G wheel -h /home/admin -s /bin/sh -D admin
RUN echo "%wheel        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers

# set TIMEZONE to Shanghai
RUN apk add --no-cache tzdata
RUN ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# init sshd
RUN ssh-keygen -A
# alpine linux sshd default use password auth, empty password will lock account, change to disable password auth
RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config

# run service when container started - sshd
EXPOSE 22:1122

# Run your program under Tini
# CMD ["/your/program", "-and", "-its", "arguments"]
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]
  • 构建镜像:

构建包含tini和ssh的alpine linux镜像
docker build -t alpine-ssh .
  • 运行容器: 其中2个bind的卷是Lima提供的HOST物理主机目录

运行容器,挂载2个从HOST主机映射到Lima虚拟机的卷(这样可以直接访问HOST主机数据)
docker run -dt --name alpine-ssh --hostname alpine-ssh \
    -p 1122:22 \
    -v /Users/admin/secrets:/home/admin/.ssh \
    -v /Users/admin/docs:/home/admin/docs \
    alpine-ssh:latest

这里挂载了host主机上提供的 secret 目录,包含了容器ssh登陆的公钥

备注

这里我遇到一个比较奇怪的问题,alpine linux系统用户账号 admin 配置了ssh公钥之后,但是我使用密钥登陆总是失败。后来发现,在alipine linux中为该 admin 设置一个密码之后,立即就能够使用密钥登陆。就好像用户账号需要密码才能激活一样,这在其他发行版中没有遇到过。

原来 alpine linux 的发行版sshd默认配置和其他发行版不同,默认配置是 PasswordAuthentication yes ,此时如果账号没有设置密码,就是 lock 状态。解决方法是:

  • 为账号设置密码

  • 修订sshd为 PasswordAuthentication no 就能够ssh密钥登陆

  • 强制 passwd -u [USER] 解锁/激活账号(但是账号存在无密码本地登陆风险)

详见 账号未设置密码时的ssh密钥登陆

开发环境 alpine-dev

  • alpine-dev 包含了安装常用工具和开发环境:

包含常用工具和开发环境的alpine linux镜像Dockerfile
FROM alpine:latest

ENV container=docker

RUN apk update && apk upgrade

# Alpine Linux 仓库内置tini,可以直接安装
RUN apk add --no-cache tini

# Copy tini entrypoint script
COPY entrypoint_ssh_cron_ash /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Devops utilities
RUN apk add --no-cache openssh sudo bind-tools tmux

# add account "admin" and give sudo privilege
RUN adduser -u 501 -G wheel -h /home/admin -s /bin/sh -D admin
RUN echo "%wheel        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers

# set TIMEZONE to Shanghai
RUN apk add --no-cache tzdata
RUN ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# init sshd
RUN ssh-keygen -A
# alpine linux sshd default use password auth, empty password will lock account, change to disable password auth
RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config

# developement
RUN apk add build-base
RUN apk add gdb strace
RUN apk add go
RUN apk add rust
# nodejs选择LTS版本
RUN apk add nodejs-lts

# run service when container started - sshd
EXPOSE 22:1122

# Run your program under Tini
# CMD ["/your/program", "-and", "-its", "arguments"]
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]

备注

详细说明见 容器化开发环境 debian-dev

  • 构建镜像:

构建镜像
docker build -t alpine-dev .
  • 运行容器:

运行容器
docker run -dt --name alpine-dev --hostname alpine-dev \
    -p 1122:22 \
    -v /Users/admin/secrets:/home/admin/.ssh \
    -v /Users/admin/docs:/home/admin/docs \
    alpine-dev:latest

改进 alpine-dev

在学习实践 Distroboxpodman 以及 Docker tini进程管理器 时,我改进了上述 Dockerfile 来更加灵活地配置容器内用户目录以及启动多个由 tini 管理的进程:

  • entrypoint.sh 脚本通过 here document 方式结合到 Dockerfile

  • 参考 Distrobox 的用户目录映射方式,将Host主机的admin HOME目录映射进容器,并使用相同的 uid/gid

  • 适应 podman rootless 无法运行低于 1024 端口服务的特性,将ssh服务调整到1122端口

同时运行crond和ssh的容器(也可以启动更多程序,如nginx),改进包装脚本
FROM alpine:latest

ENV container=docker

ARG USER=admin
ARG GROUP=admin
ARG UID=1000
ARG GID=1000

RUN cat << 'EOF' > /README

# How

use docker or podman to build and run container, command syntax is same:

- Host's admin HOME will bind to HOME in container, use Host's environment to set same uid/gid

  - if not use HOME in Host, can use other secret directory to store admin's public key and bind to container's /home/admin/.ssh directory

- ssh port change to 1122 because poeman rootless container cannot use port below 1024

# BUILD

podman build --build-arg ADMIN_UID=$(id -u) --build-arg ADMIN_GID=$(id -g) \
        -t alpine-dev:latest .

# RUN

export uid=$(id -u)
export gid=$(id -g)

podman run -dt --name alpine-dev --hostname alpine-dev \
     -p 1122:1122 \
     -v /home/admin:/home/admin \
     --user $uid:$gid \
     --userns keep-id:uid=$uid,gid=$gid \
     alpine-dev:latest
EOF

# tini entrypoint script: "here documents" / "here scripts"
# Enable BuildKit: Ensure your Docker daemon is configured to use BuildKit, which is typically enabled by default in recent Docker versions. If not, you might need to set the DOCKER_BUILDKIT=1 environment variable when running docker build.
RUN cat << 'EOF' > /entrypoint.sh
#!/usr/bin/env ash
set -e

# Function to gracefully shut down all background processes
function shutdown() {
    echo "Shutting down services..."
    kill ${CRON_PID} ${NGINX_PID} ${SSHD_PID}
    wait ${CRON_PID} ${NGINX_PID} ${SSHD_PID}
    echo "Services stopped."
}

# Trap termination signals to call the shutdown function
trap shutdown SIGTERM SIGINT

# --- Start the services in the background ---

# Start syslog (failed bind, now can't use)
sudo syslogd -n &
SYSLOGD_PID=$!
echo "SYSLOGD started with PID $SYSLOGD_PID"

# Start Crond 
# Use the -f flag to run in the foreground (daemon off mode)
sudo /usr/sbin/crond -f &
CRON_PID=$!
echo "Cron started with PID $CRON_PID"

# Start Nginx
# Use the -g "daemon off;" flag to run in the foreground
# nginx -g "daemon off;" &
# NGINX_PID=$!
# echo "Nginx started with PID $NGINX_PID"

# Start SSHD
# Use the -D flag to prevent forking into the background
# only sshd daemon (without flags) logging through syslogd, so set "-E log_file"
sudo /usr/sbin/sshd -D -E /var/log/sshd.log &
SSHD_PID=$!
echo "SSHD started with PID $SSHD_PID"

# Wait for any process to exit
# If any service fails, 'wait -n' returns its exit status, and 'set -e'
# ensures the script exits, which tells Tini to stop.
wait -n

# If we reach here, one process has exited.
# Call shutdown to terminate the others cleanly before exiting the script.
shutdown
EOF

RUN chmod +x /entrypoint.sh

RUN apk update && apk upgrade

# tini process manager
RUN apk add --no-cache tini

# Devops utilities
RUN apk add --no-cache openssh openssl sudo bind-tools tmux git neovim

# add account "admin" and give sudo privilege
# "admin" uid/gid same as host (alpine linux)
RUN addgroup -g ${GID} ${USER}
RUN adduser -u ${UID} -G ${GROUP} -h /home/${USER} -s /bin/sh -D ${USER}
RUN echo "%${GROUP}        ALL=(ALL)       NOPASSWD: ALL" >> /etc/sudoers
# account without password will be locked, so set admin's password randomly, only login through ssh
RUN RANDOM_PASSWORD=$(openssl rand -base64 12 | head -c 16) && echo "${USER}:$RANDOM_PASSWORD" | chpasswd

# set TIMEZONE to Shanghai
RUN apk add --no-cache tzdata
RUN ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# init sshd
RUN ssh-keygen -A
# vscode remote ssh need 'AllowTcpForwarding'
RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
# speed connect
RUN echo "UseDNS no" >> /etc/ssh/sshd_config
# alpine linux sshd default use password auth, empty password will lock account, change to disable password auth
RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
# podman rootless container can't use service port below 1024
RUN echo "Port 1122" >> /etc/ssh/sshd_config
# enable sshd logging
RUN echo "SyslogFacility AUTH" >> /etc/ssh/sshd_config
RUN echo "LogLevel INFO" >> /etc/ssh/sshd_config

# config syslogd, split auth log
RUN echo "auth,authpriv.* /var/log/auth.log" >> /etc/syslog.conf

# developement
RUN apk add build-base
RUN apk add gdb strace
RUN apk add go
RUN apk add rust
# nodejs choice LTS version
RUN apk add nodejs-lts
# I use graphviz for sphinx docs, you may not need
RUN apk add graphviz

# Python virtualenv
USER ${USER}
# machine learning environment
RUN sh -c "cd /home/${USER} && python3 -m venv venv/ml"
# Install NumPy, Matplotlib (Deep Learning from Scratch)
RUN sh -c "source /home/${USER}/venv/ml/bin/activate && pip install --upgrade pip && pip install numpy matplotlib"

# TensorFlow wheels built for glibc-based distros, so cannot install direct. Need compile from source. I will do it later...
#RUN sh -c "source /home/${USER}/venv/ml/bin/activate && pip install --upgrade pip && pip install numpy scipy matplotlib scikit-learn tensorflow-cpu==2.20.0 pillow"

# Sphinx doc environment
RUN sh -c "cd /home/${USER} && python3 -m venv venv/sphinx"
RUN sh -c "source /home/${USER}/venv/sphinx/bin/activate && pip install sphinx sphinx_rtd_theme sphinxnotes-strike sphinxcontrib-video sphinxcontrib-youtube myst-parser jieba"

# user profile
RUN echo "alias vi=nvim" >> /home/${USER}/.profile

# run entrypoint.sh needs root
USER root

# run services when container started
# NOW cannot use: EXPOSE 22:1122
# define multiple ports: EXPOSE 22 80 443 8080 9000
# define port range: EXPOSE 5000-5010 
# define port range with udp protocol: EXPOSE 20000-20100/udp 

# to development environment, not define any ports for freely use

# Use Tini as the ENTRYPOINT and pass the wrapper script as its argument.
# The `--` separates Tini arguments from the command to be executed.
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]

# The CMD is empty because the entrypoint handles everything.
CMD []
  • 构建镜像: 通过获取构建时当前用户的 uid/gid 来设置 podman buildDockerfile 参数,这样能够实现动态构建指定 uid/gid :

动态获取Host主机 admin用户 uid/gid 来对应构建镜像中 admin用户 uid/gid
podman build --build-arg USER=$(id -u -n) --build-arg GROUP=$(id -g -n) \
        --build-arg UID=$(id -u) --build-arg GID=$(id -g) \
        -t alpine-dev:latest .
  • 运行容器: 当前Host主机 admin 用户目录会和容器内 admin 用户目录绑定映射,方便使用

映射绑定 admin 用户目录
export user=$(id -u -n)
export uid=$(id -u)
export gid=$(id -g)

# multi ports map
podman run -dt --name alpine-dev --hostname alpine-dev \
     -p 1122:1122 \
     -p 8080:8080 \
     -p 8443:8443 \
     -p 9000:9000 \
     -v /home/$user:/home/$user \
     --user $uid:$gid \
     --userns keep-id:uid=$uid,gid=$gid \
     alpine-dev:latest

参考