从“docker容器设置ssh登录(ubuntu)”这个需求出现频次之高,就能看出很多人虽然天天用docker,却还是习惯用老一套的远程登录方式来管理容器。说实话,日常维护用docker exec -it进入容器是完全没问题的,但在某些场景下——比如统一运维通道、集成到已有的SSH批量管理工具、给同事开一个临时的调试入口——容器里跑一个sshd反而更顺手。这篇我结合自己的实操经验,把这套流程完整拆一遍,包括为什么这么配、踩过哪些坑、以及怎么让容器重启后配置不丢。
1. 为什么要在容器里跑SSH:先搞清楚需求再动手
1.1 容器SSH的真实使用场景
很多人第一次接触这个需求,都是因为看了某些教程说“容器里应该用docker exec进去”,但真到了工作里,你会发现有几个场景是docker exec替代不了的。
第一种是批量运维场景。公司里如果已经有了一套基于SSH的自动化脚本或者堡垒机流程,你想把这套机制延伸到容器环境里,就得给每个目标容器开放SSH端口,否则你的脚本全要重写成调用Docker API,这个改造成本很高。第二种是给团队同事开放调试入口。你自己可以docker exec,但同事不一定有宿主机权限,你又不想把Docker的socket直接暴露给他,那在容器里开一个SSH端口,把凭据发过去,对方用Xshell或者直接用命令行就能连上来,权限边界也清楚。第三种是模拟传统虚拟机的行为。有些从VM迁移过来的项目,内部服务之间习惯通过SSH互相访问、传文件,到了容器环境下,为了减少改动,保留SSH服务是最平滑的方案。
1.2 SSH进容器和docker exec的底层区别
这两条路看起来都是“进入容器执行命令”,实际上路径完全不同。docker exec是客户端通过Docker API和容器运行时守护进程通信,本质上是让Docker帮你启动一个进程挂到容器的命名空间里;而SSH登录是纯粹的网络连接,客户端在TCP层连上容器里的sshd进程,通过身份认证之后获得一个shell会话。
所以你会发现,SSH方式更“标准”,它不依赖宿主机上的Docker命令权限,只要网络通、凭据对就能登录。打个比方:docker exec像你直接拿到门锁钥匙进门,SSH像按门铃之后里面的人给你开门。前者权限要求高,但管理起来直接;后者适合做成一个对外提供服务的入口。理解了这一层,后面配置的时候你就不容易迷糊——你要做的事情,本质上是“在一个已经运行起来的Ubuntu容器内部,启动一个监听22端口的sshd进程,并让它能通过密码或密钥方式完成登录认证”。
2. 前期准备:镜像选择和容器启动姿势
2.1 拉取Ubuntu镜像时的几个版本坑
版本选择是第一个坑。很多教程让你直接docker pull ubuntu:latest,但这个latest指向的是当前最新的LTS甚至非LTS版本。ubuntu的镜像tag一直在滚动更新,今天拉的是24.04,过几个月可能就变成24.10,有些行为特性会有细微差异。如果是测试环境无所谓,但项目里最好锁定具体版本,比如ubuntu:22.04或ubuntu:20.04。这两个版本在软件源、库依赖方面足够稳定,安装openssh-server基本不会遇到兼容问题。
还有个细节:Docker Hub上的ubuntu镜像是最小化裁剪过的,原本的root用户是没有密码的,默认你从容器里是直接以root身份操作,不需要也不能用密码登录。所以后面如果想用密码方式SSH登录,必须手动给root设置一个密码,或者新建一个普通用户。另外,精简镜像里service、systemctl这些工具经常是缺失的,或者说存在但用不了,因为容器默认没有init进程和systemd,这直接影响到sshd的启动方式,后面第3章会专门讲。
2.2 启动容器时的端口映射和运行参数
镜像准备好之后,启动容器就别用默认方式了。如果你直接docker run -it ubuntu:22.04 /bin/bash,进去配完SSH再退出,容器就停了,没意义。正确做法是用后台方式启动,并让容器保持运行状态。
我常用的启动命令是这样的:
docker run -d --name ubuntu-ssh \ -p 2222:22 \ ubuntu:22.04 \ sleep infinity后半段sleep infinity是让容器启动后挂起一个长驻进程,本身不做任何事,只保证容器不退出。我见过不少人用/bin/bash然后发现容器秒退,因为bash命令执行完就结束了,容器自然就停了。-p 2222:22的作用是宿主机2222端口映射到容器的22端口,这样你在外面连宿主机IP:2222就能到达容器内的sshd。映射端口可以按需改,项目里跑多个容器时,每个人用不同的宿主机端口来区分。
顺带多说一句端口映射的三种写法:
-p 2222:22:将宿主机的2222端口绑定到容器的22端口,外网能访问的是宿主机级别的端口。-p 127.0.0.1:2222:22:仅监听本机回环地址,外部机器无法访问,适合本机调试、防止端口直接暴露到局域网。-p 22:随机分配一个宿主机端口映射到容器22,好处是不会冲突,坏处是你还得docker port去查实际端口。
实际做演示或者联调的时候,我一般建议用第一种或第二种。第一种方便别人访问,第二种安全一些,适合你自己先验证配置。
3. 容器内安装和配置sshd:核心步骤拆解
3.1 安装openssh-server并排查常见依赖问题
容器起来之后,先进去:
docker exec -it ubuntu-ssh bash进去之后第一步先更新软件源,再安装:
apt update apt install -y openssh-server这个过程正常情况下不会超过两三分钟。但有两个问题是高频出现的:一是apt update速度极慢甚至超时,这通常和网络环境有关,可以考虑配置一个更快的镜像源;二是提示部分依赖装不上,比如libssl版本冲突,这种情况多发生在旧的Ubuntu版本上,解决办法是先apt upgrade把系统基础库升上去再装。
安装完成之后,你先别急着启动。第一步要做的是生成主机密钥。很多精简镜像里/etc/ssh/目录是空的,没有ssh_host_rsa_key、ssh_host_ecdsa_key这些文件,直接在容器里执行service ssh start会报“sshd: no hostkeys available”。正确做法是:
ssh-keygen -A这条命令会自动生成sshd需要的所有主机密钥类型。如果不执行这一步,后面启动sshd时大概率报错,而且日志里写的错误信息非常有迷惑性,新手根本看不出来是缺密钥。
3.2 修改sshd_config的三个关键参数
sshd的主配置在/etc/ssh/sshd_config,但Ubuntu的较新版本里还有一个/etc/ssh/sshd_config.d/目录,目录下的配置会自动覆盖主配置里的同名项。我见过很多人只改主配置文件,折腾半天不生效,结果发现是sshd_config.d里某个配置项顶掉了。所以改之前先看一下这个目录下有没有相关文件。
大多数容器场景下,你只需要关心下面三个参数:
PermitRootLogin:默认Ubuntu是prohibit-password,意思是允许root用密钥登录但不允许密码登录。如果你想让root直接输密码进来,要改成yes;如果是测试环境,嫌麻烦就改yes;生产环境建议保持prohibit-password甚至改成no。PasswordAuthentication:默认是yes,但如果你发现密码正确却一直登不进去,先看看这个参数是不是被某处的配置覆盖成了no。UsePAM:默认yes。容器环境里经常没有完整的PAM模块,登录时容易报“PAM authentication failed”之类的错误,可以直接改成no。密码认证不受影响,还能少一些麻烦。
我的做法是用sed直接改,免去vi在精简容器里可能不够友好的问题:
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config echo 'UsePAM no' >> /etc/ssh/sshd_config改完配置之后,先执行sshd -t测试一下语法,如果有问题会直接在终端提示你哪个文件的哪一行出错。这一步能帮你省下很多排查时间。
3.3 设置登录凭据:密码登录与密钥登录
先说密码登录。给root设置一个密码就行:
passwd root输入两遍新密码,这就够了。但这里有个安全细节我强调一下:容器的root密码千万别设置成和宿主机root一样,因为容器端口一旦暴露到网络,暴力破解的成本很低,用独立密码能把风险隔离一部分。
再说密钥登录,这个更常用,也更安全。先在宿主机上生成一对密钥,如果你已经有现成的~/.ssh/id_rsa,可以直接用;没有的话:
ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/id_rsa然后把公钥放进容器的authorized_keys:
mkdir -p /root/.ssh chmod 700 /root/.ssh echo '你的公钥内容' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys这里的700、600权限不是随便写的。sshd在验证密钥时如果发现.ssh目录或authorized_keys文件的权限过于开放(比如744、666),会出于安全考虑直接拒绝使用密钥登录,这就是所谓的StrictModes检查。很多人在宿主机上能正常密钥登录,到了容器里死活登不上,查来查去最后发现是权限问题。
两种认证方式各有用处:密码登录配置简单,适合临时测试;密钥登录更安全,适合长期使用。我自己的测试环境一般两者都开,但外部可达的容器只会开密钥登录。
4. 三种登录方式实测对比:从宿主机、跨容器、外网访问
4.1 宿主机端口映射登录
这是最典型的用法。确认容器里sshd已经启动之后,直接从宿主机连:
ssh root@localhost -p 2222如果一切正常,你输入密码或者用密钥直接就能进去,看到root@容器ID的提示符。这里有个小细节,SSH客户端的-p参数指定的是连接目标的端口,也就是宿主机的2222端口,不是容器里的22端口。因为端口映射已经在容器启动时做好了,网络层会自动把2222上的流量转发到容器的22端口。
启动sshd之前,还要确保sshd进程在前台或后台正常运行。容器里没有systemd,直接用的方式如下:
/usr/sbin/sshd执行完不会输出任何提示,想看它是否在跑,用ps -ef | grep sshd检查。如果你想让它始终在前台运行并打印日志,可以加-D参数:
/usr/sbin/sshd -D这个方式在接下来做Dockerfile的时候非常有用,因为容器主进程一旦退出,容器就停了。
4.2 通过容器IP直连登录
如果你和容器在同一个网络里,也可以不走端口映射,直接拿容器的IP连。比如用docker inspect或docker exec查看容器IP:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ubuntu-ssh输出类似172.17.0.2。这时候你在宿主机上执行:
ssh root@172.17.0.2不用指定-p,因为默认就是22端口,容器的22端口直接就是它的监听端口。这里有两个前提:宿主机和容器之间网络能通(默认的bridge网络下通常可以);防火墙没有拦掉容器网段。需要注意,容器默认的bridge网络IP是动态的,容器重启之后可能会变,如果用了这种方式,要及时更新IP。
还有一个衍生场景,如果你从宿主机外部(比如另一台电脑)访问容器IP,那基本是行不通的,因为容器的bridge网络只在宿主机内部可见,外部机器根本路由不到这个IP地址。这就是为什么大多数情况下你要把ssh端口映射到宿主机端口来提供服务。
4.3 从另一个容器SSH登录到目标容器
有时候你的场景是容器A要访问容器B,比如从一个批处理容器去SSH进各个worker容器执行命令。这种场景和宿主机访问没有本质区别,只是你要保证两个容器在同一个Docker网络上。
建议你启动容器时用一个自定义网络,这样还能用容器名代替IP,省得记地址:
docker network create my-net docker run -d --name ubuntu-ssh --network my-net -p 2222:22 ubuntu:22.04 sleep infinity docker run -it --name ubuntu-client --network my-net ubuntu:22.04 bash在client容器里装一个SSH客户端:
apt update && apt install -y openssh-client ssh root@ubuntu-ssh因为同一个my-net网络下可以直接解析容器名,这条命令和上面直连IP的效果一样,但更方便、IP变了也不受影响。这个方式是批量运维场景里最常用的,你要做的就是在目标容器里把SSHD配置好,然后在client容器里循环执行SSH命令。
4.4 外部机器远程访问的配置思路
如果你的最终目的是从办公室或者家里直接连到服务器上的容器,光有上面的配置还不够。你需要保证宿主机本身的SSH端口可达,然后在路由器或安全组上把宿主机的2222端口放行,再通过ssh -p 2222 user@宿主机公网IP来连接。
这里我强烈建议加上防火墙规则,只放行特定来源IP访问2222端口,而不是对全网开放。容器本身不是安全边界,ssh端口暴露在公网上风险远高于普通虚拟机场景。如果业务允许,用-p 127.0.0.1:2222:22的方式,再通过宿主机SSH隧道来访问容器,是更稳妥的选择。
5. 容器重启后SSH配置丢失:持久化的三种解法
5.1 用docker commit打包当前状态
我早期就是这么干的:全都配好之后,退出容器,执行:
docker commit ubuntu-ssh ubuntu-ssh:with-sshd这个命令会把当前容器的文件系统快照保存成一个新镜像。以后再想跑一个带SSH的容器,直接用这个新镜像启动就行。优点是无脑、快,缺点也很明显:每次改动都要重新commit,镜像层会越来越臃肿;而且commit保存的是可写层的完整内容,里面可能包含你测试时产生的一些垃圾文件、历史命令等,不够干净。所以它适合你自己临时用,不适合做交付给别人的镜像。
5.2 用Dockerfile重建镜像
更工程化的方案是写Dockerfile,把SSH的安装和配置固化进去。下面是我项目里经常用的一个模板:
FROM ubuntu:22.04 RUN apt update && \ apt install -y openssh-server && \ mkdir -p /run/sshd && \ echo 'root:your-passwd' | chpasswd && \ sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \ sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ echo 'UsePAM no' >> /etc/ssh/sshd_config && \ ssh-keygen -A EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"]这里有两个细节特别值得说。第一个,mkdir -p /run/sshd。sshd启动时需要这个目录,缺失时在某些环境下会报“Missing privilege separation directory: /run/sshd”。宿主机上开机会自动创建,但容器里没有这个机制,所以要手动建。第二个,CMD ["/usr/sbin/sshd", "-D"],用前台模式运行sshd作为容器主进程,这样容器启动即SSH可用,进程退出容器也就退出,逻辑是自洽的。
构建也很简单:
docker build -t ubuntu-ssh:1.0 .相比commit,这种方式可以重复build、版本化、代码审查,是正规做法。
5.3 通过挂载目录实现配置热更新
还有一类需求是:我已经跑了一个容器,不想重建镜像,就想改个sshd配置或者换一对密钥,然后重启生效。这种情况用数据卷挂载最合适。把宿主机的某个目录挂到容器的/etc/ssh或者/root/.ssh上:
docker run -d --name ubuntu-ssh \ -v /data/container/ssh/config:/etc/ssh \ -v /data/container/ssh/root:/root/.ssh \ -p 2222:22 \ ubuntu:22.04 \ sleep infinity这样你直接在宿主机上编辑配置文件,容器内立刻能看到。改完配置后执行docker exec ubuntu-ssh sshd -t验证,然后kill掉容器内的sshd进程让它重启,或者直接docker restart ubuntu-ssh。这种方式的优点是方便管理和备份密钥,缺点是挂载整个/etc/ssh目录时,如果宿主机的配置目录内容不完整,容器内sshd可能起不来,要小心处理。
综合来看,三种方式的适用场景完全不同:临时测试用commit,交付/复用用Dockerfile,频繁调整配置用挂载目录。我个人的推荐是Dockerfile为主、挂载为辅,这样既有清晰的定义,又有灵活调整的空间。
6. 常见问题排查与避坑经验
6.1 端口映射正确却连不上的排查顺序
这个问题的表象是:docker exec进去一切正常,sshd也启动了,但宿主机ssh -p 2222连接时提示超时或拒绝连接。
我的排查顺序是固定的:
- 先在容器内执行
ss -tlnp | grep 22,确认sshd进程确实在监听22端口。如果没有任何输出,说明sshd根本没有起来,回看第3.1节,先补ssh-keygen -A。 - 再在宿主机执行
ss -tlnp | grep 2222,确认端口映射生效。如果这里空着,说明-p 2222:22没生效,检查容器启动参数。 - 然后执行
telnet 127.0.0.1 2222,能通的话说明端口转发链路没问题,问题在SSH认证层面,继续看第6.2节。 - 最后检查防火墙。如果宿主机开了ufw,即使端口映射存在,外部访问也会被挡,先放行2222端口。
这个顺序能帮你快速定位问题到底出在网络层、进程层还是认证层,不会毫无头绪地乱试。
6.2 密码正确却认证失败
密码肯定没错,但登录时提示Permission denied (publickey,password),这是最折磨人的问题之一。绝大多数情况下,问题出在sshd_config和实际生效的配置不一致。
先用这条命令看真实生效的配置:
sshd -T | grep -E 'permitrootlogin|passwordauthentication|usepam'sshd -T会展开所有配置文件(包括sshd_config.d目录下的覆盖项),输出最终生效的参数。如果显示passwordauthentication no,说明你的yes被某个文件里的no覆盖了。解决办法是在/etc/ssh/sshd_config.d/目录下新建一个配置文件,比如99-custom.conf,把你的配置放进去,因为文件名数字最大的优先读取,可以保证覆盖掉前面所有的定义。
还有一种情况是UsePAM yes导致PAM模块不完整而认证失败。容器里通常没有完整的PAM栈,尤其在非systemd环境下很常见。这种情况把UsePAM改成no就能解决,这个我在第3.2节已经提过,这里再强调一遍是因为遇到的人实在太多了。
6.3 SSH密钥登录失败的几个隐蔽原因
密钥登录失败的坑更多,而且每条的错误信息你都见过,但未必对得上号。
Permissions 0777 for '/root/.ssh/authorized_keys' are too open.:权限检查不过。容器里执行chmod 600 /root/.ssh/authorized_keys和chmod 700 /root/.ssh。Server refused our key:公钥内容没有正确写入。检查authorized_keys文件里是否有完整的ssh-rsa AAAA...开头的内容,粘贴时不要漏字符、不要换行截断。no mutual signature algorithm:宿主机SSH客户端和容器sshd之间支持的密钥生成算法不匹配。老Ubuntu镜像里的OpenSSH版本较旧,新的客户端默认禁用了ssh-rsa签名,可以在宿主机连接时加参数-o PubkeyAcceptedAlgorithms=+ssh-rsa测试,确认后写入客户端的~/.ssh/config。- 主机密钥换了:容器重启后如果你没有持久化
/etc/ssh里的host key,sshd在每次启动时会重新生成,客户端会提示REMOTE HOST IDENTIFICATION HAS CHANGED。解决办法是清理客户端~/.ssh/known_hosts里对应的旧记录,或者按第5.3节把主机密钥挂载持久化。
6.4 关于安全的一点硬话
容器里跑SSH本身就是一个需要权衡的决定。容器设计哲学是单进程隔离,多开一个sshd意味着多一个暴露面,但如果业务确实需要,那就至少做到这几点:
- 不要用默认密码,尤其不要把root密码设置得太简单。容器SSH端口一旦暴露到公网,暴力破解脚本几分钟之内就会来敲门。
- 生产环境能用密钥登录就不要开密码登录,把
PasswordAuthentication设为no。 - 宿主机端口映射时优先绑
127.0.0.1,或者用防火墙只放行指定来源IP。 - 不要给容器加
--privileged,SSH服务完全不需要特权模式,加了等于把容器隔离的防线拆了大半。 - 定期做镜像安全扫描,至少把基础镜像更新到最新的安全补丁版本。
SSH配置这种事情,demo一次通了不稀奇,真正实用的是在不出问题的时候就想好出问题时怎么排查,以及把安全底线提到足够高。
我自己平时用得最多的是Dockerfile方式 + 密钥登录 + 挂载配置目录的组合,一台宿主机上跑一堆Ubuntu容器,每个映射一个宿主机端口,用批量脚本统一执行命令或者拉日志。如果只是临时调试,我还是老老实实docker exec,配置SSH那点时间省下来,多跑几次测试不香么?但当你确实需要一条标准的网络通道进入容器时,上面这套方案已经足够你平滑落地了。