这篇文章收录于Rust 实战专栏。这个专栏中的相关代码来自于我开发的笔记系统。它启动于是2023年的9月14日。相关技术栈目前包括:Rust,Javascript。关注我,我会通过这个项目的开发给大家带来相关实战技术的分享。
前言
上上周了吧,写了一篇Rust-后端服务调试入坑记,现在看来那个坑根本就不算什么。这次这个坑才是真正的坑。到写文章这一刻,我只是确定了复现这个问题的最简单代码(参考目录验证4:基于Debian:11镜像创建最简单Rust的http服务)。但依然没有找到解决这个问题的办法。
现在把这个问题以及我的验证过程整理出来,以便路过的朋友了解,我相信肯定有大神知道其中的缘由和解决办法。还望各位大神留言赐教。
问题描述
我用Rust语言创建了一个基于Rocket框架(0.5-rc)的Restful服务api(以下简称api),并将其部署在docker容器内调试。
容器相关描述:
- 容器名称: notes-api
- 网络模式: bridge
- 端口映射: 8003:8000
当容器成功启动后,问题及相关情况描述:
- 不能从宿住机访问容器中的api,错误码56,命令行: curl http://localhost:8003/api/notes
- 能够从宿主机上ping通容器,命令行:ping 172.22.0.2
- 登录到容器中,能够运行命令行成功访问api,命令行: sudo docker exec -it notes-api curl http://localhost:8000/api/notes
- 如果将网络模式改为host,能够从宿主机上正常访问容器中的api,命令行: curl http://localhost:8000/api/notes
对自己的怀疑
这个问题导致了我对我的Docker使用经验和相关的记忆产生怀疑。难道Docker容器在网络模式为bridge时,不能从宿住机访问?很快,这个问题被否定了,因为我早些时候就在Nginx服务器上配置过多个api的路由,这些api都通过Docker部署在一台服务器上,通过不同的端口来访问部署在这些容器中的api。
即然是早些时候,我的下一个判断是,会不会是因为我现在这台服务器上安装的Docker版本比较新,新版本的Docker Engine是否有一些关于端口映射的设置,导致部署在Docker容器中的api不能够被正常访问呢?
验证之路
1. Python应用
说实话,我的关于用Docker容器部署Api的经验主要来至于Python。我还是第一次将Rust构建的api部署到Docker容器中。因此,我决定用Python创建一个简单的api,将其部署到Docker容器中,看是否能够从宿主机上正常访问。
docker-compose.yml
version: '3'
services:
web:
build:
context: ./
dockerfile: Dockerfile
container_name: python-api
network_mode: bridge
ports:
- 8004:5000
Dockerfile
from python:latest
workdir /app
copy ./main.py ./requirements.txt /app/
run pip install -r requirements.txt
cmd ["python3", "main.py"]
结果:能够从宿主机上成功访问到容器内的api。
因此,是不是镜像本身的问题呢?
我对比了Python镜像和Rust镜像所使用的linux系统。Python镜像使用的是"Debian GNU/Linux 11",Rust使用的是"Debian GNU/Linux 12"。
2. 从Debian:11镜像来创建Rust的镜像
因此,我决定基于Debian:11的镜像来制作Rust的api容器的镜像。
docker-compose.yml
version: '3'
services:
web:
build:
context: ./
dockerfile: Dockerfile
container_name: notes-api1
network_mode: bridge
ports:
- 8004:8000
Dockerfile
from debian:11
run apt update
run apt install -y curl gcc
run curl -s --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
env PATH=/root/.cargo/bin:$PATH
run rustup default nightly
workdir /app
copy ./Cargo.toml ./config.toml ./Cargo.lock /app/
copy ./src /app/src
cmd ["cargo", "run"]
结果:不能从宿主机访问Docker容器中的api。
会不会是Python的镜像虽然基于"Debian GNU/Linux 11",但是,在制作镜像的时候有一些设置关于端口映射的设置呢?
3. 从Debian:11镜像来创建Python的镜像
因此,为了排出关于“设置”的猜想,我决定基于Debian:11的镜像来制作Python的api容器的镜像。
如果能够正常访问,说明不存在关于“设置”的猜想。
docker-compose.yml
version: '3'
services:
web:
build:
context: ./
dockerfile: Dockerfile
container_name: my-python2
network_mode: bridge
ports:
- 8009:5000
Dockerfile
from debian:11
run apt update
run apt install -y curl gcc
run apt install python3 pip -y
workdir /app
copy ./main.py ./requirements.txt /app/
env PATH=/usr/local/bin:$PATH
run pip install -r requirements.txt
cmd ["python3", "main.py"]
结果:能够从宿主机上成功访问到容器内的api。
说明,Python的镜像中,关于端口映射的相关“设置”猜想不存在。
因此,我的视线又回到了Rust项目,我的api是基于Rocket 0.5-rc开发的,是不是因为这个框架的原因呢?
4. 基于Debian:11镜像创建最简单Rust的http服务
因此,我让AI帮我写了一个没有任何依赖的Rust的http服务,然后将这个服务部署到基于Debian:11的镜像中。
docker-compose.yml
version: '3'
services:
web:
build:
context: ./
dockerfile: Dockerfile
container_name: rust-simple-http
network_mode: bridge
ports:
- 8012:8080
Dockerfile
from debian:11
run apt update
run apt install -y curl gcc
# 安装rust
run curl -s --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
env PATH=/root/.cargo/bin:$PATH
workdir /app
copy ./Cargo.toml ./entry.sh /app/
copy ./src/main.rs /app/src/main.rs
entrypoint ["./entry.sh"]
main.rs
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
match stream {
Ok(stream) => {
std::thread::spawn(|| {
handle_client(stream);
});
}
Err(e) => {
eprintln!("Failed to establish a connection: {}", e);
}
}
}
}
entry.sh
#!/bin/bash
echo "cargo build"
cargo build --release
echo "cp bin"
cp target/release/app ./
echo "run app"
./app
结果: 不能从宿主机访问Docker容器中的api。
因此,貌似和Rocket 0.5-rc这个框架没有关系。
目前的情况可以归纳为:
- Rust创建的api部署到容器中,容器的网络模式为bridge,不能够从宿主机通过容器的端口访问到容器中运行的api。
- Python创建的api部署到容器中,容器的网络模式为bridge,能够从宿主机通过容器的端口访问到容器中运行的api。
这个特殊现象是否只存在于Rust和Python之间呢?如果再找一个应用部署到网络模式为bridge的Docker容器中,能否从宿主机通过容器端口访问到容器中运行的应用呢?如果答案是“能”,那么,多少可以得出结论:
Rust创建的api部署到网络模式为bridge的Docker容器中,不能从宿主机通过容器的端口访问到容器中运行的api。
5. 基于Debian:11镜像从apt安装简单的http服务
docker-compose.yml
version: '3'
services:
web:
build:
context: ./
dockerfile: Dockerfile
container_name: simple-http-server
network_mode: bridge
ports:
- 8013:8080
Dockerfile
from debian:11
run apt update
run apt install -y curl gcc
run apt install libhttp-server-simple-perl -y
workdir /app
copy entry.sh /app/
entrypoint ["./entry.sh"]
entry.sh
#!/bin/bash
perl -MHTTP::Server::Simple -e 'my $server = HTTP::Server::Simple->new(); $server->run()'
结果:能够从宿主机上成功访问到容器内的api。
结论
看来目前只能得出这样的结论了:
Rust创建的api部署到网络模式为bridge的Docker容器中,不能从宿主机通过容器的端口访问到容器中运行的api。
问题整理好了,先放在这里。
后面的工作如何开展
暂时将容器的网络模式设置为host进行调试。如果后面配置的服务很多,估计需要做一个cli来管理各个服务端口的配置(这可是我的强项,哈哈)。
如有问题,欢迎大家留言交流。关注我,后面会在Rust 实战专栏中给大家带来更多关于Rust开发实战的分享。