Distrobox环境容器中ssh服务
我在使用 distrobox 中发现默认的网络环境,容器和Host主机是使用 同一个网络堆栈
容器内部无法启动 sshd 服务
在 Distrobox运行Alpine Linux 实践中,我尝试:
--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"
结果启动失败,检查 podman logs alpine-dev 显示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
但是实际上,我去除掉 --init-hooks "sudo rc-service ssh start 进入到容器内部检查,发现 sshd 服务并没有启动
检查 ssh 进程:
ssh 进程air13:/etc/ssh# ps aux | grep ssh
root 9296 0.0 0.0 1624 924 pts/1 S+ 10:17 0:00 grep ssh
admin 15128 0.0 0.0 7896 5024 ? S Nov03 0:00 ssh: /home/admin/.ssh/192.168.1.20-22-admin [mux]
admin 21636 0.0 0.0 7596 5480 ? S+ Nov03 0:00 ssh acloud-dev
admin 21641 0.0 0.0 7936 5036 ? S Nov03 0:08 ssh: /home/admin/.ssh/192.168.1.6-1122-admin [mux]
等等! 为何看到的进程和Host主机是一样的,我并没有在容器内部执行 ssh acloud-dev 等命令,为何会在容器内部看到Host物理主机上的 ssh 命令进程?
我真的已经进入容器内部了吗? ( distrobox enter alpine-dev )
df -h 显示确实已经进入了容器,根文件系统挂在是 overlay :
overlay 模式📦[admin@alpine-dev .ssh]$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 3.9G 27M 3.9G 1% /tmp
devtmpfs 10M 0 10M 0% /dev
shm 3.9G 14M 3.9G 1% /dev/shm
/dev/sda3 107G 13G 89G 13% /run/host
overlay 107G 13G 89G 13% /home/admin/.local/share/containers/storage/overlay/964da9628fd23b5b826733e8889fd4b0fd4bea87e3ec4e0016aade014f10b5b1/merged
tmpfs 1.6G 520K 1.6G 1% /run/host/run
tmpfs 789M 176K 789M 1% /run/user/1000
/dev/sda1 511M 288K 511M 1% /run/host/boot/efi
overlay 107G 13G 89G 13% /
📦[admin@alpine-dev .ssh]$ sudo su -
air13:~# df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 3.9G 27M 3.9G 1% /tmp
devtmpfs 10M 0 10M 0% /dev
shm 3.9G 14M 3.9G 1% /dev/shm
/dev/sda3 107G 13G 89G 13% /run/host
overlay 107G 13G 89G 13% /home/admin/.local/share/containers/storage/overlay/419fddbeedb5311d07420e40f50e8eb126c1eaa8d1d51f9cdcc56985ddb984ab/merged
overlay 107G 13G 89G 13% /home/admin/.local/share/containers/storage/overlay/964da9628fd23b5b826733e8889fd4b0fd4bea87e3ec4e0016aade014f10b5b1/merged
overlay 107G 13G 89G 13% /
tmpfs 1.6G 520K 1.6G 1% /run/host/run
tmpfs 789M 176K 789M 1% /run/user/1000
/dev/sda1 511M 288K 511M 1% /run/host/boot/efi
但是,启动和停止sshd服务都诡异地报告已经有其他启动和停止了
探索
我尝试前台启动 sshd 服务(这里我为了避免和Host主机ssh服务端口22冲突,修订了容器的 /etc/ssh/sshd_config 绑定 23 端口):
sshd 服务sudo /usr/sbin/sshd -D -e
出乎意料的时,容器内的root用户也没有权限绑定端口 23 :
sshd 服务显示没有权限监听23端口Bind to port 23 on 0.0.0.0 failed: Permission denied.
Bind to port 23 on :: failed: Permission denied.
Cannot bind any address.
我使用 python3 的 http.server 模块来测试容器能够绑定的端口,确认容器内部低于 1024 端口都无法监听:
1024 以下端口# 可以bind 1024端口
📦[admin@alpine-dev .ssh]$ sudo python3 -m http.server -b 0.0.0.0 1024
Serving HTTP on 0.0.0.0 port 1024 (http://0.0.0.0:1024/) ...
# 但是低于1024端口无法bind
📦[admin@alpine-dev .ssh]$ sudo python3 -m http.server -b 0.0.0.0 1023
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/usr/lib/python3.12/http/server.py", line 1314, in <module>
test(
File "/usr/lib/python3.12/http/server.py", line 1261, in test
with ServerClass(addr, HandlerClass) as httpd:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/socketserver.py", line 457, in __init__
self.server_bind()
File "/usr/lib/python3.12/http/server.py", line 1308, in server_bind
return super().server_bind()
^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/http/server.py", line 136, in server_bind
socketserver.TCPServer.server_bind(self)
File "/usr/lib/python3.12/socketserver.py", line 478, in server_bind
self.socket.bind(self.server_address)
PermissionError: [Errno 13] Permission denied
解决方案(思路)
想到在部署 Alpine Linux运行Podman 时,选择采用 rootless 模式运行,是不是这个原因导致呢?
确实如此...google提示: rootless 容器中 root 用户实际上没有真正Host系统的root权限,所以无法监听(bind)低于 1024 端口。解决的方法主要有两个:
方法一 (
推荐):在rootless容器中,将应用服务监听端口调整到1024及以上端口,然后在运行容器的--publish <HOST端口>:<Container端口>时映射出容器,同时在前面采用类似 Nginx反向代理 对外提供80/443这样的公共服务端口,反向代理到容器映射出的1024及以上端口方法二 (
不推荐): 调整Host主机net.ipv4.ip_unprivileged_port_start `` 内核参数,将非私有端口范围降低到需要bind的端口号以下,就能够允许 ``rootless容器绑定到原先无法绑定的私有端口(即设置为net.ipv4.ip_unprivileged_port_start = 80,则允许所有80及以上端口非root可以bind,但是不包括低于80端口,如22端口不包括)
net.ipv4.ip_unprivileged_port_start 为 1022 (降低2个端口号)# 设置1022及以上为非私有端口
echo 'net.ipv4.ip_unprivileged_port_start = 1022' >> /etc/sysctl.conf
# 生效
sysctl -p
根据上述解决方法,我采用方法一,即调整容器内部服务端口,采用
1024及以上非私有端口,所以 Distrobox运行Alpine Linux 中Dockerfile镜像我也相应做了调整另外在容器内部,虽然能够通过
--init-hooks去执行/usr/sbin/sshd -D &命令,但是进行一旦挂掉,容器内部没有进程管理器是无法自动重启sshd服务的,所以我最后还是决定通过tini进程管理器来维护sshd服务
调整容器内服务端口( 1024以上 )
由于我考虑 Docker tini进程管理器 管理容器内多进程可能更为优雅,所以我尝试采用 here document 改进过的 Dockerfile :
1122 端口来运行容器内 ssh 服务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 []
使用 podman 构建镜像:
podman build -t alpine-dev .
创建通过
Dockerfile构建镜像的容器:
init-hooks 参数,尝试通过 tini 启动容器中多个服务方式来运行 distroboxdistrobox create --name alpine-dev --image alpine-dev:latest --additional-flags "-p 1122:1122"
创建成功后,执行以下命令来进入容器(运行容器):
distrobox enter alpine-dev
但是发现容器内没有 sshd 进程,这是为什么?
distrobox 覆盖了 Dockerfile 的 ENTRYPOINT 和 CMD
原来 distrobox enter 不会执行 Dockerfile 中的 ENTRYPOINT ,因为 distrobox 的主要功能是提供一个交互的shell环境,通过 distrobox 内置的脚本和用户shell 覆盖镜像内的默认entrypoint和command 来实现:
轻量级容器环境:
Distrobox被设计成一个轻量级集成容器环境,就像一个常规的系统shell一样运行,而不是一个标准的面向应用的容器默认行为是shell:
distrobox enter默认命令进入容器时实际上是使用当前用户的shell(bash或zsh)这也解释了为何我在 Distrobox运行Swift(基于debian容器) 运行 Distrobox运行VS Code(基于debian容器) 时始终没有执行容器内部
~/.profile的原因
Distrobox使用自己的 entrypoint : 当执行distrobox create创建distrobox容器时,一个名为distrobox-init的脚本被设置为容器的实际entrypoint。这个脚本处理所有必要的设置,例如安装缺失的依赖,设置用户uid/gid,并挂载Host主机的目录。当完成设置以后,就会加载用户shell或指定命令,完全 bypass 掉原生镜像中的ENTRYPOINT和CMD
在 distrobox 初始化设置中执行定制脚本
distrobox create 阶段提供了 --init-hooks 或 --pre-init-hooks 来运行容器设置完成但还没有加载最终shell时可以运行的脚本。也就是说,可以在 --init-hooks 中指定运行 sshd :
distrobox 中使用 --init-hooks 运行 sshd 服务distrobox create --name alpine-dev --image alpine-dev:latest --init-hooks "sudo /usr/sbin/sshd" --additional-flags "-p 1122:1122"
通过上述方式构建的 distrobox 容器,使用 distrobox enter alpine-dev 检查,就可以看到容器内部运行了 sshd 服务,也就能够在Host主机通过 ssh localhost -p 1122 登录
更好的方案
实际上 Distrobox 不适合作为标准容器来运行远程服务,而是作为桌面系统的补充,无缝运行一些需要在轻量级隔离环境中安装的桌面应用,特别是类似 Alpine Linux musl 库无法正常运行的依赖 glibc 的应用。
对于上述需要运行 sshd 服务,甚至多个应用服务的远程开发测试环境,实际上适合采用标准的 podman 或 Docker 来 run ,这样就能够充分利用 Docker tini进程管理器 进程管理器来运行和监视多个服务进程,并在服务进程退出时及时终止容器,符合标准 Container 运行或 Kubernetes 运行:
podman 运行镜像来实现 tini 启动多个服务podman run -dt --name alpine-dev --hostname alpine-dev \
-p 1122:1122 \
-v /home/admin:/home/admin \
alpine-dev:latest