AWD平台3 狠狠记录,从镜像打包到ssh自启动再到指定容器中root密码
狠狠记录,从镜像打包到ssh自启动再到指定容器中root密码。
镜像打包
其实镜像打包是一件非常简单的事情,但是用代码实现的时候还是会有坑。
先说明,我用的是docker官方提供的sdk
这里先展示我的dockerfile
FROM alpine:3.14
RUN mkdir "/app"
WORKDIR "/app"
COPY easyweb /app/app
COPY start.sh /app/start.sh
RUN apk 'add' '--no-cache' 'openssh-server'
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN ssh-keygen -A
CMD ./start.sh
这个dockerfile会拷一些东西(编译好的go程序和启动脚本)进容器,然后启动一个脚本,这个脚本做两件事:启动ssh,启动我的web应用。
打包必须用tar包。
打包函数长这个样子
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
他的第二个参数buildContext是一个io.Reader类型的,这个上下文指的就是我们打包镜像时的上下文,而且必须是一个文件,不能是文件夹,因此我们需要达成一个tar包
所以写出了这样的代码
func buildImg() {
ctx := context.Background()
cli, _ := client.NewClientWithOpts()
buildContext, _ := os.Open("../eastweb.tar")
defer buildContext.Close()
buildResponse, err := cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{
Dockerfile: "Dockerfile", //相对buildContext下的路径
SuppressOutput: false,
Remove: true,
ForceRemove: true,
Tags: []string{"web"}, // 给你的镜像打的tag
})
if err != nil {
fmt.Println(err.Error())
return
}
response, err := ioutil.ReadAll(buildResponse.Body)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(string(response))
}
这里面就注意Dockerfile这一项,它指定的是Dockerfile的相对路径而不是绝对路径。
这里展示我的tar包是啥样的:
所以这里就直接填"Dockerfile".
imageBuild会返回一个buildResponse,它是这么定义的
// ImageBuildResponse holds information
// returned by a server after building
// an image.
type ImageBuildResponse struct {
Body io.ReadCloser
OSType string
}
我们于是就用了ioutil.ReadAll去读取出来,其实它里面的内容就是打包镜像的过程,例如下面这个:
ymk@ymk:test$ ./test
{"stream":"Step 1/9 : FROM alpine:3.14"}
{"stream":"\n"}
{"stream":" ---\u003e 0a97eee8041e\n"}
{"stream":"Step 2/9 : RUN mkdir \"/app\""}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 3ebb98b41468\n"}
{"stream":"Step 3/9 : WORKDIR \"/app\""}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 103148b20052\n"}
{"stream":"Step 4/9 : COPY easyweb /app/app"}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 7071bec37683\n"}
{"stream":"Step 5/9 : COPY start.sh /app/start.sh"}
{"stream":"\n"}
{"stream":" ---\u003e 4fa1d332a3e9\n"}
{"stream":"Step 6/9 : RUN apk 'add' '--no-cache' 'openssh-server'"}
{"stream":"\n"}
{"stream":" ---\u003e Running in 9a47f0e0a94d\n"}
{"stream":"fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz\n"}
{"stream":"fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz\n"}
{"stream":"(1/3) Installing openssh-keygen (8.6_p1-r3)\n"}
{"stream":"(2/3) Installing openssh-server-common (8.6_p1-r3)\n"}
{"stream":"(3/3) Installing openssh-server (8.6_p1-r3)\n"}
{"stream":"Executing busybox-1.33.1-r6.trigger\n"}
{"stream":"OK: 7 MiB in 17 packages\n"}
{"stream":"Removing intermediate container 9a47f0e0a94d\n"}
{"stream":" ---\u003e 5fde29deeffc\n"}
{"stream":"Step 7/9 : RUN echo \"PermitRootLogin yes\" \u003e\u003e /etc/ssh/sshd_config"}
{"stream":"\n"}
{"stream":" ---\u003e Running in d2c2dca56a47\n"}
{"stream":"Removing intermediate container d2c2dca56a47\n"}
{"stream":" ---\u003e f6c202958b3a\n"}
{"stream":"Step 8/9 : RUN ssh-keygen -A"}
{"stream":"\n"}
{"stream":" ---\u003e Running in 1e611532d66f\n"}
{"stream":"ssh-keygen: generating new host keys: RSA "}
{"stream":"DSA "}
{"stream":"ECDSA "}
{"stream":"ED25519 "}
{"stream":"\n"}
{"stream":"Removing intermediate container 1e611532d66f\n"}
{"stream":" ---\u003e 0293f3603bc4\n"}
{"stream":"Step 9/9 : CMD ./start.sh"}
{"stream":"\n"}
{"stream":" ---\u003e Running in cedc3c842033\n"}
{"stream":"Removing intermediate container cedc3c842033\n"}
{"stream":" ---\u003e a6f4ffdd5139\n"}
{"aux":{"ID":"sha256:a6f4ffdd51398aa82411bf038a34d4a70c27a44aab78e14b2c5eeef55630b8c2"}}
{"stream":"Successfully built a6f4ffdd5139\n"}
{"stream":"Successfully tagged web:latest\n"}
注意Dockerfie中的相对路径
我们正常写Dockerfile,里面会涉及拷贝之类的操作,比如这一行
COPY easyweb /app/app
显然它就是把下图中的easyweb这个文件拷到了容器中的/app/app的位置,然后把工作目录设未/app,进去之后直接./app就能启动应用了,然而我用代码打包的镜像并不是这样的,我启动不了。
原因是什么呢,这几个文件都在一个叫easyweb的文件夹里
然后我对easyweb打包,达成easyweb.tar。
这时候我们点进easyweb.tar可以看到里面还有一层easyweb
就是这一层导致了我的一个问题
在来看之前打包镜像的代码
buildContext, _ := os.Open("../easyweb.tar")
defer buildContext.Close()
buildResponse, err := cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{
Dockerfile: "Dockerfile", //相对buildContext下的路径
SuppressOutput: false,
Remove: true,
ForceRemove: true,
Tags: []string{"web"}, // 给你的镜像打的tag
})
我的上下文是进入这个easyweb.tar这个tar包里面的一层,就是上面图里的easyweb文件夹那一层,然后这个上下文,就是Dockerfile相对路径所相对的上下文,所以这时候,Dockerfile里的相对路径不再相对自己,而是相对easyweb这个文件夹。于是COPY easyweb /app/app 变成了将easyweb这个文文件夹拷到/app目录下,并且改名为app,于是再使用./app启动就会报错。
如何解决呢?打包的时候不要夹上那一层,直接针对文件打包,这是我的另一个tar包
打成这样就ok了。
或者改启动脚本。
alpine如何设置ssh自启动
其实,设置alpine就是把dockerfile写好就行了,但是我卡了好长时间,因为我分不清RUN,CMD,和ENTRYPOINT.
现在我分清了。
RUN
RUN后面执行的命令,的结果会体现在打好的镜像中,比如创建文件夹。但是你用RUN去启动程序不可以。
CMD
CMD可以有多条,但是只有最后一条有用,可以用来启动程序
ENTRYPOINT
ENTRYPOINT只有一条游泳,也是用来启动程序
重点是CMD和ENTRYPOINT最好只写一条
因为如果两个配合用,CMD的内容会加到ENTRYPOINT后面,具体看这张图
所以我这个镜像既要启动ssh,又要启动web进程,本来想用CMD的shell模式,通过
CMD ./app && /usr/sbin/sshd
启动,但是我写反了,先启动./app,它是在前台启动的,因此后买你的ssh启动不了,我当时懵逼了,就换了方法,写了一个脚本用ENTRYPOINT启动脚本,但是用脚本也要考虑前后台的问题。
然后回过头我们浅说一下怎么给alpine搞ssh自启动吧
其实就是这几行
RUN apk 'add' '--no-cache' 'openssh-server'
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
RUN ssh-keygen -A
把ssh的包下好,然后把"PermitRootLogin yes" 输入到配置文件中去,这样的话就可以使用root账号远程登录。
然后执行ssh-keygen -A 生成密钥啥的,就可以顺利启动了。
最后就是在容器启动时执行
/user/sbin/sshd
就ok了。
设置修改容器中root密码
现在ssh服务起起来了,如何给他指定我们想要的root密码呢?
我最初希望在容器启动的时候指定进去,但是只能通过CMD,但是Dickerfile有这类命令了,不兼容了,于是我想到一行命令就可以改密码了,就是这个
echo root:newpassword | chpasswd
但是我尝试用docker exec 容器名 echo root:newpassword | chpasswd 执行的,执行出错,说我认证令牌操作错误。然后只能用docker exec 容器名 passwd,但是这个需要两步去确认,考虑到我不太会用go语言搞命令行的操作,于是我去翻文档找到了三个函数
- ContainerExecCreate
- ContainerExecAttach
- ContainerExecStart
这三个函数可以帮我完成。
代码如下
func exec() {
ctx := context.Background()
cli, err := client.NewClientWithOpts()
if err != nil {
fmt.Println(err.Error())
return
}
id,_:= cli.ContainerExecCreate(ctx, "testweb", types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"passwd"},
})
res, _ := cli.ContainerExecAttach(ctx, id.ID, types.ExecStartCheck{})
cli.ContainerExecStart(ctx, id.ID, types.ExecStartCheck{
Tty: true,
})
res.Conn.Write([]byte("ymk0910!\n"))
res.Conn.Write([]byte("ymk0910!\n"))
fmt.Println("结束")
}
这个测试就是帮我把名为testweb的容器的root密码改成ymk0910!
他是这个原理,先把exec给Create出来,然后用ContainerExecAttach去跟踪这一条exec,然后Start执行,执行起来我们通过Attach给我们返回的Conn去代替命令行里写的过程,因为我们知道passwd他要先输密码再确认,所以我这里Write两边,然后就成功,我通过ssh root@ip 连上了容器。
当ssh能连上之后就好办了,可以通过ssh在容器里执行命令或者还通过这个方式,然后可以刷新flag,可以检测容器是否正常等等。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)