Docker 从入门到实战:容器化全栈项目,再顺手编排一个 Milvus 向量数据库

Docker 从入门到实战:容器化全栈项目,再顺手编排一个 Milvus 向量数据库

写在前面

你大概率遇到过这种情况:本地跑得好好的项目,发给同事或者部署到服务器,就各种 npm install 报错、Node 版本不对、缺依赖、端口冲突......

Docker 要解决的,正是这个问题:"我的电脑能跑,你的电脑怎么跑?"

一句话概括:

Docker = 应用 + 运行环境

它把除了代码之外那一堆有版本要求的运行环境(Node、Redis、MySQL、Nginx......)一起打包成一个整体容器,方便地在任何设备上部署。

这个类比很有意思:

ini 复制代码
Agent  = LLM + Harness(tool + mcp + rag + skill + ...)
Docker = 应用 + 运行环境

下面我们从一个最朴素的 Node 服务开始,一步步走到全栈项目容器化,最后再用 docker-compose 编排一个 Milvus 向量数据库。


一、先搞清楚两个核心概念

Docker 世界里最重要的两个词:

概念 理解 类比
image(镜像) 应用程序 + 环境的只读打包 一个"配方/模具"
container(容器) 镜像跑起来后的实例 + 隔离资源 按配方做出来的"菜"
  • 镜像可以理解为:git pull image,把别人打包好的镜像拉下来。
  • 容器可以理解为:镜像被 docker run 之后运行起来的进程,拥有隔离的 CPU、内存、网络。

举个特别现实的场景:你入职接手一个 N 年前的 Vue2 项目,要求 Node 16 + npm 8,而你电脑装的是 Node 22,直接跑不起来。容器化就是把这些依赖各自隔离安装,互不干扰。


二、实战一:一个 Node 服务的 Dockerfile

先写一个最简单的 HTTP 服务 index.js

js 复制代码
// node 早期的 commonjs 规范
const http = require('http');
const server = http.createServer((req, res) => {
  res.end("hello world");
});
server.listen(1314, '0.0.0.0', () => {
  console.log('node service run on 1314');
});

再写一个 Dockerfile

dockerfile 复制代码
# 1. 选一个基础镜像,node 是官方 Node 镜像,后续指令都在这之上叠加
FROM node

# 2. 设置工作目录为 /app,不存在会自动创建(相当于 cd /app)
WORKDIR /app

# 3. 把 Dockerfile 所在目录的 index.js 复制到容器 /app 下
COPY index.js .

# 4. 启动容器时执行的命令
CMD ["node", "index.js"]

怎么理解 Dockerfile?

一个很贴切的比喻------它是蜜雪冰城的标准操作手册(SOP)

写清"先加奶茶、再加奶、放 3 勺糖、摇匀",任何人照着做,出来的味道都一样,就成了连锁店。

Dockerfile 就是一个文本"配方文件",里面写着一步步"做菜"的指令,Docker 照着做就能自动做出一个一模一样的镜像。

构建、推送、拉取三板斧:

bash 复制代码
docker build -t my-hello .   # 根据 Dockerfile 构建镜像
docker push my-hello          # 推送到镜像仓库
docker pull my-hello          # 从仓库拉取

Dockerfile 是发布项目的标准方式之一。


三、实战二:Nginx 反向代理

为什么需要 Nginx?

我们的 Node 服务监听在 1314 端口,但用户浏览器访问的是 http://localhost,默认就是 80 端口。

用户不知道后端具体跑在哪个端口上,这时候需要一个对外的入口------这就是 Nginx 的职责:监听 80 端口,把请求反向代理转发到后端真实的 1314 端口。

配置文件

写一个 nginx.conf

nginx 复制代码
events {}
http {
    server {
        listen 80;   # 监听自己的 80 端口,接住这个请求
        location / {
            # host.docker.internal 是 Docker 提供的特殊域名,
            # 从容器内部解析出来就是宿主机(我的电脑)的 IP
            proxy_pass http://host.docker.internal:1314;
            proxy_set_header Host $host;
        }
    }
}

启动命令拆解

bash 复制代码
docker run --name my-nginx-demo \
  -p 80:80 \
  -v C:/Users/13361/Desktop/docker/demo/nginx.conf:/etc/nginx/nginx.conf \
  -d nginx

逐个参数看:

  • docker run:启动一个镜像,成为可运行的容器
  • --name my-nginx-demo:容器名字
  • -p 80:80本机 80 端口 : 容器 80 端口,把本机 80 映射到 nginx 容器监听的 80
  • -v 本机配置文件:/etc/nginx/nginx.conf:把本地配置文件挂载进容器
  • -d nginx:后台运行 nginx 镜像

一张图看懂完整链路(运维考点)

scss 复制代码
用户上网 intent
  → browser(chrome)  (正向代理 http)
  → localhost:80
  → docker -p 端口映射 → container(80)
  → -v 映射配置文件(local:/etc/nginx/nginx.conf)
  → nginx(image) → nginx:80
  → nginx.conf 反向代理转发
  ← :1314 (后端真实端口)

关键结论:Nginx 作为对外的统一入口,用户只知道 localhost:80,完全不知道后端实际运行在 1314 端口------这就是"反向代理"的考点。


四、实战三:全栈项目容器化(React + NestJS + 跨域)

接下来是一个标准的全栈 Todo 项目。

技术栈

  • 前端:React 19 + TypeScript + Zustand + Axios(Vite 构建)
  • 后端:NestJS 11
  • 代理 :Nginx 把 80 转发到后端 3000
  • 核心问题:跨域(CORS)

跨域是怎么来的?

浏览器同源策略要求:协议、域名、端口三者一致才算同源。

前端 Vite 开发服务器跑在 5173,后端 NestJS 跑在 3000,端口不同 → 跨域。

yaml 复制代码
5173 : 3000   ← 同源策略(安全机制)触发

解决跨域的两条路

方案一:后端 CORS(服务器端配置响应头)

main.ts 里开启 CORS:

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();   // 允许跨域
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

方案二:前端代理(Vite 代理 / Nginx 反向代理)

这里我们用 Nginx 反向代理的思路:前端统一请求 /api 前缀,交给 Nginx 转发到后端,从浏览器视角看就是"同源"。

前端:封装 Axios 实例

config.ts 里创建统一的 axios 实例:

ts 复制代码
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';

// 1. 创建独立 axios 实例,baseURL 配合 Nginx 反向代理
const service = axios.create({
  baseURL: '/api',   // 统一请求前缀
  timeout: 10000,
});

// 2. 请求拦截器:注入 Token
service.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 3. 响应拦截器:直接返回 response.data
service.interceptors.response.use(
  (response) => response.data,
  (error) => {
    console.error('API Error:', error.message);
    return Promise.reject(error);
  }
);

export default service;

接口定义 todos.ts

ts 复制代码
export const fetchTodos = () => service.get<Todo[]>('/todos');
export const createTodo = (title: string) => service.post<Todo>('/todos', { title });
export const updateTodo = (id: number, patch: Partial<Todo>) =>
  service.patch<Todo>(`/todos/${id}`, patch);
export const deleteTodo = (id: number) => service.delete(`/todos/${id}`);

状态管理 todoStore.ts

ts 复制代码
import { create } from 'zustand';
import { fetchTodos, createTodo } from '../api/todos';

interface TodoStore {
  todos: Todo[];
  fetchTodos: () => Promise<void>;
  addTodo: (title: string) => Promise<void>;
}

export const useTodoStore = create<TodoStore>((set) => ({
  todos: [],
  fetchTodos: async () => {
    const res = await fetchTodos();   // 请求通过 Nginx 代理
    set({ todos: res });
  },
  addTodo: async (title: string) => {
    const res = await createTodo(title);
    // ...
  },
}));

注意:这里 fetchTodos 返回的已经是被响应拦截器解包过的 data,所以直接 set({ todos: res }),不再需要 res.json()


五、实战四:docker-compose 编排 Milvus 向量数据库

前面都是"单个镜像"的玩法。但真实项目往往由多个镜像协作组成,比如 Milvus 向量数据库就不是一个镜像,而是依赖 etcd(元数据)+ MinIO(对象存储)+ Milvus(计算引擎)三兄弟。

这时候就需要 docker-compose 来编排多个 image 的工作流。

编排文件

milvus-standalone-docker-compose.yml 核心结构:

yaml 复制代码
version: '3.5'

services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.25
    # ... 环境变量、健康检查、数据卷挂载

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2024-05-28T17-19-04Z
    ports:
      - "9001:9001"
      - "9000:9000"
    # ...

  standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v3.0.0
    command: ["milvus", "run", "standalone"]
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

networks:
  default:
    name: milvus

三个服务的职责:

服务 作用
etcd 存储元数据(集合结构、索引信息等)
minio 对象存储(存放实际的向量/数据文件)
standalone Milvus 单机版核心服务,对外暴露 19530 端口

depends_on 保证启动顺序:先起 etcd 和 minio,再起 Milvus。

连接并写入向量数据

index.mjs 展示了完整流程:连接 → 建集合 → 建索引 → 加载 → 插入。

js 复制代码
import "dotenv/config";
import { MilvusClient, DataType, MetricType, IndexType } from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from "@langchain/openai";

const VECTOR_DIM = 1024;

// 用 OpenAI 做向量化
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  dimensions: VECTOR_DIM,
});

// 连接 Milvus(19530 是上面 compose 暴露的端口)
const client = new MilvusClient({ address: 'localhost:19530' });

// 1. 创建集合(字段:id / vector / content / date / mood / tags)
await client.createCollection({
  collection_name: 'ai_diary',
  fields: [
    { name: 'id', data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
    { name: 'vector', data_type: DataType.FloatVector, dim: VECTOR_DIM },
    { name: 'content', data_type: DataType.VarChar, max_length: 5000 },
    { name: 'tags', data_type: DataType.Array, element_type: DataType.VarChar, max_capacity: 10, max_length: 50 },
  ],
});

// 2. 建索引(IVF_FLAT + COSINE 余弦相似度)
await client.createIndex({
  collection_name: 'ai_diary',
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE,
  params: { nlist: 1024 },
});

// 3. 加载集合
await client.loadCollection({ collection_name: 'ai_diary' });

// 4. 给日记文本生成 embedding 并插入
const diaryData = await Promise.all(
  diaryContents.map(async (diary) => ({
    ...diary,
    vector: await embeddings.embedQuery(diary.content),
  }))
);
await client.insert({ collection_name: 'ai_diary', data: diaryData });

这个例子非常典型:Docker 负责把 Milvus 这套多组件系统一键跑起来,SDK 负责在应用层做向量化的读写


六、常用 Docker 命令速查

bash 复制代码
docker pull nginx                 # 拉取任意想要的镜像
docker run ...                    # 运行镜像成为容器
docker build -t my-hello .        # 构建镜像
docker push my-hello              # 推送镜像

docker stop $(docker ps -q)       # 停止所有容器
docker rm $(docker ps -aq)        # 删除所有容器
docker rmi nginx                  # 删除 nginx 镜像

docker pull mysql:8.0             # 拉取 MySQL 8.0 镜像

总结

这篇文章从零梳理了 Docker 的完整学习路径:

  1. 核心概念image(镜像)与 container(容器)的区别。
  2. Dockerfile:用 SOP 的思路理解镜像的"配方"构建。
  3. Nginx 反向代理:理解"用户只知道 80,后端在 1314"的运维考点。
  4. 全栈容器化:React + NestJS 项目中用 CORS 和 Nginx 代理解决跨域。
  5. docker-compose 编排:多个镜像协作跑起 Milvus 向量数据库,并结合 LangChain 做向量化写入。

Docker 的本质,始终是那句话------把"代码 + 运行环境"打包成一个可移植的整体,让你的项目在任何机器上都能一致地跑起来。

相关推荐
SatanII2 小时前
容器运行时Containerd完整学习笔记|原理、安装、ctr/nerdctl/crictl实操全梳理
运维·docker·华为云·containerd
九皇叔叔13 小时前
Kubernetes 核心概念:集群架构、核心组件与资源对象
docker·容器·kubernetes·k8s
rustfs13 小时前
MinIO 国产开源平替正式 GA
分布式·docker·云原生·rust
wdfk_prog18 小时前
ROS教程07:从 ros::start() 顺着源码读懂 Master、XML-RPC 与 Topic 注册发现
运维·缓存·docker·容器·ros
mengge.cloud18 小时前
0917Docker 小白实验教程(理论+实操)
linux·服务器·网络·docker
wdfk_prog19 小时前
用 Git Submodule + Sparse Checkout 管理 RT-Thread:内核、BSP、第三方库与业务代码分层实践
运维·缓存·docker·容器·ros
努力努力再努力wz19 小时前
【Docker入门系列】镜像为什么能复用?一文吃透 Docker Image、Registry、运行时架构与常用命令
缓存·docker·容器
java_logo1 天前
Docker 部署填鸭表单完整教程:搭建私有化问卷与表单收集平台
docker·表单·问卷·tduck·轩辕镜像·填鸭表单·tduck platform
玉&心1 天前
K8s HPA自动扩缩容
docker·云原生·容器·hpa·kubernates