Dockerfile常见环境使用
ubuntu(国内镜像)
由于网内网络环境的问题,ubuntu不换源的话基本没法用,我们首先参考普通 ubuntu 系统如何换源,然后将其转换成命令形式,便于应用于 Dockerfile 中。
随便在网上搜 "ubuntu换源",可以搜出一大堆教程,这里我们直接参考阿里云开源镜像站的官方教程,官方教程中给出了操作办法,就是手动将所有的源地址替换为阿里云的镜像地址:
首先我们使用 docker 跑一个 ubuntu 的镜像看下基本情况:
1 | docker run -it --rm ubuntu:20.04 /bin/bash |
注:后面也会出写一个笔记记录常用的 docker 命令。
在 ubuntu 下使用 sudo apt-get install xxx
安装软件,所有
apt
相关的配置项存储在 /etc/apt
文件夹下,其内容如下
其中 sources.list
以及 sources.list.d
就涉及软件源的配置信息,sources.list
用来存储默认的软件源信息(或者说官方源),而 sources.list.d
则用来存储第三方软件源,例如 docker 软件源、nvidia
驱动源等,一般安装软件我们只需要配置 sources.list
即可,第三方源的配置和官方源的配置差不太多。
使用 cat
命令查看 sources.list
文件内容(较长,可能显示不全)
1 | root@302e32342092:/etc/apt# cat sources.list |
我们可以看到其软件源就是通过以下格式指定的
1 | deb <apt source> |
从源的URL链接中不难发现,这些链接实际上都是同一个域名底下的,我们换源是可以只更换前面的域名(即最后一个/
之前的所有即可)。
既然都是替换了,那么肯定不能使用文本编辑器一个一个替换,最好的办法就是通过
sed
命令进行替换
sed
命令简单介绍
from GNU sed - GNU Project - Free Software Foundation
sed (stream editor) is a non-interactive command-line text editor.
其功能十分强大,目前我们只用到了其 s
命令
1 | s/regexp/replacement/[flags] |
例如将文本中所所有的foo
替换成bar
就可以使用如命令
1 | sed -i s/foo/bar/g test.txt |
测试如下
其中 -i
表示 inplace,即直接在当前文件中替换文本,最后的
g
表示对所有的匹配项进行替换
对于某些特殊字符,我们可以通过随意指定分隔符来避免频繁的转义,其格式如下所示
1 | s<delimiter>regexp<delimiter>replacement<delimiter>[flags] |
例如我们需要替换文本中所有的 a/b/c
为
e/f/g
,就可以使用如下命令:
1 | sed -i s@a/f/g@e/f/g@g test.txt |
测试如下
替换链接
那么这样我们就可以我们通过文本替换实现换源,命令如下
1 | sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list |
这条命令将所有的 archive.ubuntu.com
替换为
mirrors.aliyun.com
,对于security.ubuntu.com
也是一样,虽然我们并不会对容器中的
ubuntu 进行安全更新,但是 security.ubuntu.com
会严重影响软件源的更新速度,因此也需要一同替换掉,最终完整的换源命令如下:
1 | sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list && \ |
通过 &&
将两条命令合并成一条执行,最后执行
apt-get update
更新软件源即可
此时可以看到软件源更新速度就很快了。
注:上面的方法仅适用于 ubuntu x86_64 架构的版本,对于其他版本(arm64、RISC-V等)则需要使用另外的软件源,不过替换方法都是一致的,找到需要替换的链接,然后通过
sed
进行替换即可。例如在 arm 版的 ubuntu 中,其软件源为
http://ports.ubuntu.com/ubuntu-ports/
,其对应的阿里云镜像为https://mirrors.aliyun.com/ubuntu-ports/
,我们同样使用sed
进行替换
1 sed -i s/ports.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list(阿里云镜像地址来自: https://developer.aliyun.com/mirror/ubuntu-ports)
ssh 服务器
启动 docker 容器后,有可能需要连接到容器中修改代码什么的(一般用于深度学习模型训练环境,部署代码的话最好不要这么做),最朴素的办法就是安装一个命令行的文本编辑器,例如 vi,vim,nano 等,命令用熟了还是很轻松的,但是学习成本太高了,后面发现 vscode 的 Remote SSH 插件可以通过 SSH 连接到主机,之后就像本地代码一样进行调试运行等,简直不要太方便(实际上还有更好的办法,直接通过 vscode 的 Dev Containers 插件直接连接到容器中),不过设置 SSH 通用性更高,例如搭建 hadoop 等环境进行测试时就可以使用。
在配置脚本安装之前,我们首先看看手动如何安装 ssh 服务器(openssh-server),直接通过apt安装即可,运行如下命令
1 | apt-get install openssh-server |
之后提示我们具体需要安装哪些包,以及是否确定安装
但是我们可以看到,建议安装了一堆包,实际上有些包我们可以不装的,可以通过参数
--no-install-recommends
来取消安装这些包,运行命令
1 | apt-get install openssh-server --no-install-recommends |
结果如下
相比之前我们所需要安装的依赖就少多了
但是还有一个问题,安装过程中有一个
Do you want to continue? [Y/n]
,提示我们是否确定,但是我们需要用命令完成自动化安装过程,安装过程中是完全无交互的,因此需要默认安装,可以通过
-y
来进行默认安装(具体可以参考 apt-get 的 man 页面)
1 | apt-get install openssh-server --no-install-recommends -y |
这样当我们输入命令后就会自动安装了。
可以编写一个完整的安装 ssh 的 Dockerfile
1 | FROM ubuntu:20.04 |
其暴露一个端口 22(SSH的默认端口),而且使用
service ssh start -D
来确保命令前台运行(保证容器不会被杀死)
我们先尝试能否成功构建镜像,使用命令
1 | docker build -t openssh-server:ubuntu20.04 . |
进行构建,然后通过以下命令创建容器
1 | docker run -d --rm -p 54132:22 openssh-server:ubuntu20.04 |
其中 -d
表示 detach,将容器列入后台运行,由于我们在
CMD
中已经指定了一个前台应用,容器不会立刻被docker杀死,--rm
表示容器运行结束后会被直接删除(常用于测试场景),-p 54132:22
表示将宿主机的54132端口绑定到容器的22端口(即SSH端口上,这样我们就可以通过
54132端口连接到容器的SSH服务了)。
最后我们使用 ssh 连接命令尝试连接到容器中
1 | ssh -p 54132 root@localhost |
首先会提示我们是否需要建立连接(这一步可以说明SSH端口是通的)
但是此时直接提示我们输入密码,但是我们似乎并没有设置过 root 账户的密码,这就意味着我们永远无法连接到容器
我们可以通过一下命令连接到容器进行修改
1 | docker exec -it <container-id> /bin/bash |
然后输入一下命令设置明文密码
1 | echo "root:123" | chpasswd |
之后我们再尝试通过 SSH 进行连接,结果如下
提示我们 Permission denied,这是因为 SSH 默认是不允许 root
用户通过密码登录的,我们需要修改 /etc/ssh/sshd_config
通过 cat /etc/ssh/sshd_config
可以看到这一点
再查看 sshd_config 的相关配置选项(查看其相关的 man 页面 即可,注意查看版本,我们使用的 ubuntu 版本比较新,老的配置文档不一定适用)
可以看到 PermitRootLogin
的可选参数有四个:yes
,prohibit-password
,forced-commands-only
,no
注:其中
yes
,prohibit-password
以及no
的含义比较直观,但是forced-commands-only
还不太理解,其限定root
用户通过秘钥进行登录,但是只能执行特定的command
,这个command
实际上需要在 ssh 登录的时候指定,即authorized_keys
中指定,例如
1 command=”/bin/ps” ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAt0BETg9J6hZb5Kqxy+yfNtKHfwxUELz7PqGtGiM5eNb8DHC8kj02SCFoql5rpaecMGybWRiSK8/k+EsK7TMgd4O+p6WkNyLD3WZrmVzUEPaxAdYf1eeCQooTJ+B1TKXDNlF9t8xTVsHd67HmPWYU6i3+kaDSX7cbrz2ds2zUGSozj1UQ8AJDJMbGOqpjs3nVh2EpSDgY7znqmUDnygVPiM4c3OfEzs5iCxVd4ggpPhH8d0bwy8RmPsooxJYUY4rE1C5iWCvB7P810yUFB0OilxiX9AfZa9shC3n5bqaX0ioY1eC44hFFPL602fJyKMj6w/zxN5aIeFO03Sl9+FU4YQ== root@iZ23wan41azZ就限定通过
root
私钥登录的时候只能执行/bin/ps
,无法执行其他命令,在运维场景下较为实用
1 | echo "PermitRootLogin yes" >> /etc/ssh/sshd_config |
然后重启一下 ssh 服务
1 | service ssh restart |
(此处重启时由于没有前台应用,容器会被 docker 给杀死,再重启一下容器即可)
再尝试登录的时候就可以成功了,提示我们输入密码
每次登录都输入密码还是有点麻烦(可以通过 sshd_config
设置密码可为空来绕过,但是还会提示用户进行交互,无法实现自动化远程登录),为此我们需要配置远程登录,首先通过
ssh-keygen
创建一对秘钥
1 | ssh-keygen -t rsa -f ~/key -N "" |
其中 -t rsa
指定加密算法为 rsa,-N ""
表示私钥密码为空 ,输出私钥路径为 ~/key
,公钥路径会在私钥后添加 .pub
,即 ~/key.pub
,此处创建的秘钥是可以复用的,不过不推荐这么做,安全性较低。
然后将公钥信息拷贝到 ~/.ssh/authorized_keys
里即可
1 | mkdir ~/.ssh |
然后再重启一下 ssh,最终使用私钥登录测试
1 | ssh -p 54132 -i key root@localhost |
测试结果如下
提示我们权限不对,需要调整 key
的权限,按照下面的方式调整权限即可(适用于Windows,对于 macos 直接使用
chmod
命令修改权限即可)
再尝试登录的时候就可以登陆了
最后给出完整的 Dockerfile
1 | FROM ubuntu:20.04 |
miniconda(python)
跑深度学习时经常需要配环境,需要安装 cuda 驱动、pytorch 等必要的软件库,虽然使用 anaconda 已经可以很好的管理 python 环境,使用 docker 来运行可以确保代码的可移植性,便于在另外一台机器上跑代码。
anaconda 官方起始有 docker 镜像,但是只有 debian 和 alpine,且通常我们会在 nvidia 的 cuda 容器中进行安装,可以参考官方的安装脚本进行配置,仓库地址:docker-images/miniconda3/debian/Dockerfile at master · ContinuumIO/docker-images · GitHub
代码如下
1 | FROM debian:bullseye-slim |
理解起来很简单,第一步安装 miniconda
的依赖(有些依赖我们并不需要,可根据自己需要调整),第二步根据系统版本选择对应的
miniconda 安装脚本进行安装,将其添加到 /etc/profile.d
和
~/.bashrc
中(确保 miniconda
的初始化脚本在用户登录或用户交互前自动执行),最后清理掉无用的数据即可
miniconda 换源
国内 miniconda 也很慢,同样可以对其进行换源,这里参考清华源的换源教程,直接在用户目录下创建
.condarc
,然后填写如下信息
1 | channels: |
这一部分最好直接通过 COPY
命令的方式,将本地文件拷贝至容器中进行替换
1 | COPY .condarc /root/.condarc |
创建新环境并安装包
配置好 miniconda 后我们就可以安装指定版本的 python
并配置我们需要的依赖包了,现在的问题在于,创建新环境后需要激活环境,然后再进行安装,那么如何在
dockerfile 中激活环境呢?可以通过 Dockerfile
中的
SHELL
命令指定所需要使用的 shell。
首先给出完整的 Dockerfile,然后再进行解析
1 | FROM continuumio/miniconda3:23.3.1-0 |
其中的关键在于下面这条语句:
1 | SHELL [ "/bin/bash","--login","-c" ] |
这一条语句的含义就是我们后面所执行的所有命令,都会通过 SHELL 指定的
shell 进行调用,例如我们后面有语句
RUN conda activate base
,其会就等价于
1 | /bin/bash --login -c "conda activate base" |
关键在于 --login
参数,其强制 Shell
以登录模式调用命令,其关键在于 --login
在启动 shell
时会加载 /etc/profile
以及 /etc/profile.d
中指定的脚本文件,从前面的 Dockerfile 中我们也可以看到
1 | ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh |
这个 conda.sh
就是用来激活环境的脚本,我们可以通过
/bin/bash --noprofile --norc
来测试
可以看到当我们加载 /etc/profile
后就可以激活环境了,在非交互模式下也是如此。
下面给出一个完整的 pytorch + cuda 的环境
1 | FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04 |
构建时间可能较长(pytorch 包太大了),启动时注意参数
--shm_size
,尤其是在多线程数据加载中
总结
个人理解 dockerfile 实际上就是运维脚本的另外一种形式,关键还是在于脚本语言的熟悉程度,linux 命令较为熟悉的话就可以很轻松的构建出合适的镜像。
参考资料
- ubuntu镜像_ubuntu下载地址_ubuntu安装教程-阿里巴巴开源镜像站 (aliyun.com)
- apt-get(8) - Linux man page (die.net)
- sshd_config(5) - Linux manual page (man7.org)
- sshd_config 中 PermitRootLogin 的探讨_huigher的博客-CSDN博客
- Linux下service xxx start/stop/restart启动服务、关闭服务、重启服务深入理解@_service start_King-Long的博客-CSDN博客
- windows ssh Permissions for “xxx“ are too open错误详细解决方案_菜到不知所措的博客-CSDN博客
- anaconda | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror
- LPIC Shell 基础:105.1 Lesson 1 (lpi.org)
- 共享内存简介及docker容器的shm设置与修改_docker shm_Adenialzz的博客-CSDN博客