Distrobox运行Alpine Linux

在完成 Alpine Linux运行Distrobox 部署之后,我初步运行起 Disgrobox运行Debian 系统。但是我也想构建一个更为轻量级的容器,也就是 alpine on alpine : 在Alpine Linux Host 主机上通过 distrobox 来运行一个 Alpine Linux podman 容器。

快速起步

  • 创建alpine系统容器:

创建alpine容器
distrobox create --name alpine-dev --init --image alpine:latest
  • 运行

运行alpine容器
distrobox enter alpine-dev

基于Dockerfile构建

使用 distrobox 来运行容器虽然简单,但是只是从官方下载标准镜像,首次进入初始化要做一次长时间安装更新,而且进入以后又要重新安装必要的软件,还是很繁琐的。

实际上 distrobox 底层使用的 podmanDocker 都能够基于 从Dockerfile构建Docker镜像 来构建自定义镜像,可以包含需要安装的软件以及必要的基础配置。这些工作都是有积累的可重复的,所以我现在都是先构建自定义镜像,再使用 distrobox ,以结合两者优势。

alpine-dev 镜像 Dockerfile
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 []
  • 构建镜像:

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

警告

不能在Dockerfile中使用早期语法 EXPOSE 22:1122 :

原先Dockerfile中我习惯配置 EXPOSE 22:1122 以表明希望后续 docker run 时使用 -p 1122:22 ,但是在 distrobox 实践中发现会检查Docker镜像中 EXPOSE 并报错显示端口语法错误:

Dockerfile中 EXPOSE 22:1122 导致 distrobox 报错 "invalid port number"
Creating 'alpine-dev' using image alpine-dev:latest	Error: unable to convert image EXPOSE: invalid port number: strconv.Atoi: parsing "22:1122": invalid syntax
 [ ERR ] failed to create container.

原因是现在 Dockerfile 中配置(文档) EXPOSE 语法应该是:

DockerfileEXPOSE 语法
# 现在已经不能使用(早期docker版本可以) EXPOSE 22:1122

# 定义输出多个端口使用
EXPOSE 22 80 443 8080 9000

# 定义输出端口范围
EXPOSE 5000-5010

# 定义输出端口范围及协议
EXPOSE 20000-20100/udp

备注

我参考 Disgrobox运行Debian 使用 --init-hook "sudo service ssh start" 来启动ssh服务,配置:

配置 --init-hooks "sudo rc-service sshd start" 尝试容器启动时启动ssh服务
distrobox create --name alpine-dev --image alpine-dev:latest --init-hooks "sudo rc-service sshd start" --additional-flags "-p 1122:22"

结果启动:

运行容器
distrobox enter alpine-dev

失败

检查 podman logs alpine-dev 显示ssh服务早已启动,重复执行启动ssh导致报错:

由于容器内ssh服务已启动,再执行 --init-hooks "sudo rc-service sshd start" 报错
...
+ sudo rc-service sshd start
 * WARNING: sshd is already starting
+ '[' 1 -ne 0 ]
+ printf 'Error: An error occurred\n'
Error: An error occurred

真是这样吗?

请参考 Distrobox环境容器中ssh服务 调查