在Ubuntu容器中启用SSH登录:Docker配置与持久化

发布时间:2026/10/5 11:03:47
在Ubuntu容器中启用SSH登录:Docker配置与持久化 从“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_keysmkdir -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 rootlocalhost -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查看容器IPdocker inspect -f {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} ubuntu-ssh输出类似172.17.0.2。这时候你在宿主机上执行ssh root172.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 rootubuntu-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|usepamsshd -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 PubkeyAcceptedAlgorithmsssh-rsa测试确认后写入客户端的~/.ssh/config。主机密钥换了容器重启后如果你没有持久化/etc/ssh里的host keysshd在每次启动时会重新生成客户端会提示REMOTE HOST IDENTIFICATION HAS CHANGED。解决办法是清理客户端~/.ssh/known_hosts里对应的旧记录或者按第5.3节把主机密钥挂载持久化。6.4 关于安全的一点硬话容器里跑SSH本身就是一个需要权衡的决定。容器设计哲学是单进程隔离多开一个sshd意味着多一个暴露面但如果业务确实需要那就至少做到这几点不要用默认密码尤其不要把root密码设置得太简单。容器SSH端口一旦暴露到公网暴力破解脚本几分钟之内就会来敲门。生产环境能用密钥登录就不要开密码登录把PasswordAuthentication设为no。宿主机端口映射时优先绑127.0.0.1或者用防火墙只放行指定来源IP。不要给容器加--privilegedSSH服务完全不需要特权模式加了等于把容器隔离的防线拆了大半。定期做镜像安全扫描至少把基础镜像更新到最新的安全补丁版本。SSH配置这种事情demo一次通了不稀奇真正实用的是在不出问题的时候就想好出问题时怎么排查以及把安全底线提到足够高。我自己平时用得最多的是Dockerfile方式 密钥登录 挂载配置目录的组合一台宿主机上跑一堆Ubuntu容器每个映射一个宿主机端口用批量脚本统一执行命令或者拉日志。如果只是临时调试我还是老老实实docker exec配置SSH那点时间省下来多跑几次测试不香么但当你确实需要一条标准的网络通道进入容器时上面这套方案已经足够你平滑落地了。