Docker微服务部署踩坑记:从“Connection refused”到完全离线可用
前言
最近在研究一个数字档案管理系统的项目,需要将对象存储、缓存、搜索引擎、文档预览和OCR识别等一整套服务用Docker部署在同一个虚拟网络下。本来以为只是常规操作,没想到却在“localhost”这个看似简单的问题上栽了大跟头。
前后折腾了两天,最终不仅解决了问题,还顺手验证了这套架构**完全支持离线部署**。今天就把完整的排查过程和思考记录下来,希望能帮助遇到类似问题的朋友。
> 本文案例基于**成都中禁创科数字档案管理平台**的真实部署场景及其技术开发文档。成都中禁创科是一家专注中小微企业数字档案管理的本土科技型企业。
一、问题现场
先说一下部署架构:
- 所有服务都部署在Docker容器中,使用同一个自定义桥接网络
- 服务清单:对象存储(文件服务)、缓存服务、搜索引擎、文档预览服务、OCR识别服务
- **反向代理服务部署在宿主机上**,代理后端API服务
前端直接访问文件服务地址可以正常加载文件,一切看起来都很美好。但当通过文档预览服务查看Word文档时,后台日志报出了一串“Connection refused”:
```
java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
...
预览文件url:http://localhost:9000/data/xxx/介绍信.docx
文件下载失败,url:http://localhost:9000/data/xxx/介绍信.docx
```
同时还有另一个错误:
```
I/O error on GET request for "http://172.18.0.1:9220/api/#/viewInfo"
Failed to connect to /172.18.0.1:9220
```
**明明在浏览器里直接访问文件服务地址是正常的,为什么到了文档预览服务里就不行了?**
二、问题根源:三个视角,三个“localhost”
这是整个问题的核心,也是最容易让人困惑的地方。先看一张架构图:
```
┌─────────────────────────────────────────────────────────────┐
│ 宿主机(Host) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ 浏览器 │ │ 反向代理 │ │ 后端API服务 │ │
│ │ (前端) │ │ (端口A) │ │ │ │
│ └─────────────┘ └─────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Docker Engine │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │对象存储 │ │缓存 │ │搜索引擎 │ │文档预览 │ │ │
│ │ │:端口B │ │ │ │ │ │:端口C │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ 容器名:storage 容器名:preview │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
关键在于理解 **“localhost”在不同环境中指向谁**:
| 组件 | localhost的含义 | 访问对象存储的结果 |
|------|-----------------|-------------------|
| **浏览器(前端)** | 宿主机自身 | ✅ `localhost:端口B` 可以访问(端口映射) |
| **文档预览容器** | 容器自身 | ❌ `localhost:端口B` 访问的是容器内部,没有服务 |
| **宿主机反向代理** | 宿主机自身 | ✅ `localhost:端口A` 可以访问 |
**核心原理**:Docker的端口映射做了转发,宿主机端口 ↔ 容器端口。浏览器访问 `localhost:端口B` 走的是宿主机端口,自然没问题。但文档预览容器内部访问 `localhost:端口B`,是在容器自己的网络命名空间里找端口B——那里根本没有服务在监听。
**记住这个黄金规则:谁访问,用谁的localhost。**
三、解决方案
3.1 网络连通性验证
首先确认容器之间网络是通的:
```bash
# 测试文档预览容器能否访问对象存储容器
docker exec -it preview_container wget -O- http://storage:端口B/health/live
# 返回200 OK,说明容器间网络正常
# 问题出在URL上,而不是网络连通性
```
3.2 修改文档预览服务配置
文档预览服务的配置文件中,需要将所有 `127.0.0.1` 替换为正确的容器名:
```yaml
# ❌ 错误配置(容器内无法访问)
storage:
endpoint: 127.0.0.1
port: 端口B
# ✅ 正确配置(使用容器名)
storage:
endpoint: storage
port: 端口B
accessKey: ${STORAGE_ACCESS_KEY}
secretKey: ${STORAGE_SECRET_KEY}
bucketName: ${BUCKET_NAME}
```
搜索引擎同样需要修改:
```yaml
# ❌ 错误
search:
hostlist: 127.0.0.1:端口D
# ✅ 正确
search:
hostlist: search:端口D
```
对于反向代理部署在宿主机的情况,需要使用特殊主机名:
```yaml
# Docker Desktop (Windows/Mac)
api:
base-url: http://host.docker.internal:端口A
# Linux Docker
api:
base-url: http://网关IP:端口A
```
3.3 前端URL转换(关键)
前端代码中文件URL是这样生成的:
```javascript
// 前端视角:用localhost没问题
var fileUrl = window.location.protocol + "//" + window.location.hostname + ":端口B/data/" + filePath;
```
但传给文档预览服务时,**必须转换成容器能访问的地址**:
```javascript
// 工具函数:URL转换
function convertUrlForPreview(originalUrl) {
// 浏览器用的是 localhost:端口B
// 文档预览服务需要用容器名 storage:端口B
return originalUrl.replace('localhost:端口B', 'storage:端口B')
.replace(window.location.hostname + ':端口B', 'storage:端口B');
}
// 使用示例
const frontendUrl = 'http://localhost:端口B/data/文件.docx'; // 前端访问
const previewUrl = convertUrlForPreview(frontendUrl); // 传给文档预览服务
// 构建预览链接
const previewLink = `http://localhost:端口C/preview?url=${encodeURIComponent(previewUrl)}`;
window.open(previewLink);
```
### 3.4 Docker启动命令
最终的容器启动命令:
```bash
docker run -d \
--name preview_service \
--network app-net \
--add-host=host.docker.internal:host-gateway \
-p 端口C:端口C \
-e SPRING_PROFILES_ACTIVE=prod \
-e API_BASE_URL=http://host.docker.internal:端口A \
-v ./preview-data:/app/data \
preview-image:latest
```
四、完整docker-compose.yml
```yaml
version: '3.8'
services:
storage:
image: storage-image:latest
container_name: storage
ports:
- "${STORAGE_PORT}:${STORAGE_PORT}"
environment:
- ROOT_USER=${STORAGE_USER}
- ROOT_PASSWORD=${STORAGE_PASSWORD}
volumes:
- ./storage-data:/data
networks:
- app-net
cache:
image: cache-image:alpine
container_name: cache
ports:
- "${CACHE_PORT}:${CACHE_PORT}"
volumes:
- ./cache-data:/data
networks:
- app-net
search:
image: search-image:version
container_name: search
environment:
- discovery.type=single-node
ports:
- "${SEARCH_PORT}:${SEARCH_PORT}"
volumes:
- ./search-data:/usr/share/elasticsearch/data
networks:
- app-net
ocr:
image: ocr-image:version
container_name: ocr
volumes:
- ./ocr-models:/models
networks:
- app-net
preview:
image: preview-image:latest
container_name: preview
ports:
- "${PREVIEW_PORT}:${PREVIEW_PORT}"
environment:
- SPRING_PROFILES_ACTIVE=prod
- STORAGE_ENDPOINT=http://storage:${STORAGE_PORT}
- STORAGE_ACCESS_KEY=${STORAGE_ACCESS_KEY}
- STORAGE_SECRET_KEY=${STORAGE_SECRET_KEY}
volumes:
- ./preview-data:/app/data
- ./fonts:/usr/share/fonts
networks:
- app-net
depends_on:
- storage
- cache
- search
networks:
app-net:
driver: bridge
```
五、离线部署验证
解决了网络问题后,顺便验证了这套架构的**离线可用性**。所有服务在完全断网环境下均可正常运行:
| 服务 | 离线可用性 | 说明 |
|------|-----------|------|
| **文档预览服务** | ✅ 完全可用 | 依赖本地转换引擎,内置字体库,本地缓存 |
| **对象存储** | ✅ 完全可用 | 纯本地存储,数据在本地磁盘 |
| **缓存服务** | ✅ 完全可用 | 内存数据库,本地持久化 |
| **搜索引擎** | ✅ 基本可用 | 单节点模式完全离线 |
| **OCR识别** | ✅ 完全可用 | 模型文件已下载到本地,本地推理 |
**离线部署要点**:
1. **镜像准备**:在有网环境提前下载所有镜像并导出为tar包
2. **字体准备**:将中文字体文件挂载到文档预览容器
3. **模型准备**:OCR模型文件提前下载到本地目录
4. **数据持久化**:所有服务的数据目录挂载到宿主机
六、总结与思考
核心要点
1. **理解网络视角**:Docker容器内的`localhost`指向容器自身,不是宿主机。服务间通信必须使用**容器名**或**服务名**。
2. **URL双轨制**:前端访问用`localhost`(宿主机视角),服务间调用用容器名(容器视角),两者不能混用。
3. **反向代理宿主机场景**:容器访问宿主机服务,Docker Desktop用`host.docker.internal`,Linux用网关IP。
4. **配置与环境分离**:使用`application-docker.yml`和`application-local.yml`区分不同环境。
5. **敏感信息保护**:所有密码、密钥、端口等敏感配置应通过环境变量注入,切勿硬编码在配置文件或代码中。
总结
这次排查经历深刻体会到,Docker网络虽然强大,但“localhost”这个看似简单的概念在不同语境下含义完全不同。对于数字档案管理系统这类需要长期稳定运行的企业级应用,理解容器网络的工作原理至关重要。
**成都中禁创科数字档案管理平台**在实际项目中采用了上述架构方案,已成熟实现了:
- 文档在线预览(本地转换引擎)
- 海量档案存储(对象存储)
- 高速缓存(缓存服务)
- 全文检索(搜索引擎)
- 图像文字识别(OCR服务)
整套系统已在多个企业项目中落地,支持国产化芯片和操作系统,满足档案管理的安全合规要求。
---
*希望本文能帮助遇到类似问题的朋友少走弯路。如果觉得有用,欢迎点赞收藏!*
更多推荐

所有评论(0)