一文搞懂 Docker + Dockerfile:从镜像构建到全栈部署
蜜雪冰城为什么能开遍全国?因为它有一份标准操作手册 SOP------写清"先加奶茶、再加奶、放3勺糖、摇匀",任何人照着做,出来的味道都一样。
Dockerfile 就是程序世界的 SOP。
前言
最近在学 Docker,发现这玩意儿真的是解决了一个很头疼的问题:"我的电脑能跑,你的电脑怎么跑不了?"
你将会收获:
- 🔑 Docker 核心概念:镜像 vs 容器到底啥关系
- 🔑 Dockerfile 每一行指令的含义
- 🔑 如何构建、推送、拉取镜像
- 🔑 Nginx 反向代理解决跨域
- 🔑 一个完整的全栈项目容器化实战
技术栈:React + NestJS + Zustand + Axios + Nginx + Docker
一、Docker 到底是什么?用大白话讲清楚
没有 Docker 的时候
你到公司接手一个 N 年前的 Vue2 项目,要求 Node 16 + npm 8,但你电脑装的是 Node 22,跑不起来。
传统做法:
卸载 Node 22 → 装 Node 16 → 跑项目 → 换个项目又要 Node 22 → 再切回来......
这种依赖地狱,相信不少同学都踩过坑。
有 Docker 之后
你写的代码 + Node 16 + npm 8 + 各种依赖 = 一个容器
Docker 把应用和它的运行环境打包成一个容器,在任何机器上都能跑。
🔥 一句话记住:Docker = 应用 + 运行环境
类比理解
| 概念 | 类比 | 说明 |
|---|---|---|
| 镜像 Image | 光盘 / 菜谱 | 只读模板,包含应用 + 环境 |
| 容器 Container | DVD 播放 / 做出来的菜 | 镜像的运行实例 |
一个镜像可以创建多个容器,就像一个菜谱可以做出很多盘菜。
scss
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 镜像 │ → │ 容器 1 │ │ 容器 2 │
│ (Image) │ │ (Container) │ │ (Container) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└────────────────────┴────────────────────┘
一个镜像可以创建多个容器
二、快速体验:hello world
先别急着写代码,跑个 demo 感受一下:
bash
# 拉取镜像
docker pull hello-world
# 运行容器
docker run hello-world
看到输出就说明 Docker 安装成功了。就这么简单。
三、Dockerfile 详解:逐行拆解
Dockerfile 是一个文本配方文件,里面写着一步步"做菜"的指令,Docker 照着做就能自动做出一个一模一样的镜像。
dockerfile
# 1. 选一个基础镜像
FROM node:alpine
# 2. 设置工作目录
WORKDIR /app
# 3. 把本地代码复制进容器
COPY index.js .
# 4. 容器启动时执行的命令
CMD ["node", "index.js"]
逐行讲解
| 指令 | 作用 | 类比 |
|---|---|---|
FROM node:alpine |
基础镜像 | 选食材底料 |
WORKDIR /app |
设置工作目录 | 准备案板 |
COPY index.js . |
复制代码进容器 | 把菜放进去 |
CMD ["node", "index.js"] |
启动命令 | 开火炒菜 |
⚠️ 避坑:alpine vs 完整版
dockerfile
# ❌ 完整版,约 900MB,下载慢
FROM node
# ✅ alpine 精简版,约 100MB,快 5-8 倍
FROM node:alpine
💡 一句话记住:FROM 选底料,WORKDIR 备案板,COPY 放菜,CMD 开火
四、构建 & 推送镜像
构建镜像
bash
# 别忘了最后的点 . 表示当前目录
docker build -t my-hello .
参数说明:
docker build--- 构建命令-t my-hello--- 给镜像起个名字(tag).--- 构建上下文(当前目录)
推送到 Docker Hub
bash
# 1. 登录
docker login
# 2. 打标签(格式:用户名/镜像名:版本)
docker tag my-hello 你的用户名/my-hello:latest
# 3. 推送
docker push 你的用户名/my-hello:latest
拉取镜像
bash
docker pull 你的用户名/my-hello:latest
🔥 Dockerfile 是发布项目的标准方式之一。一次构建,到处运行。
五、Nginx 反向代理
为什么需要 Nginx?
前端跑在 localhost:5173,后端跑在 localhost:3000,端口不一样就会有跨域问题。
markdown
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 浏览器 │ → │ Nginx │ → │ 后端 │
│ localhost │ │ :80 │ │ :3000 │
└─────────────┘ └─────────────┘ └─────────────┘
│
↓
┌─────────────┐
│ 前端 │
│ :5173 │
└─────────────┘
Nginx 的作用:
- 监听 80 端口(用户访问
http://localhost) - 把
/api请求转发给后端 3000 端口 - 把其他请求转发给前端
启动 Nginx 容器
bash
docker run --name my-nginx-demo -p 80:80 -d nginx
参数说明:
--name my-nginx-demo--- 容器起个名字-p 80:80--- 本机 80 端口映射到容器 80 端口-d--- 后台运行
挂载配置文件
bash
docker run --name my-nginx-demo \
-p 80:80 \
-v /本地路径/nginx.conf:/etc/nginx/nginx.conf \
-d nginx
通过 -v 把本地的 nginx 配置文件挂载到容器里,实现反向代理。
六、全栈项目容器化:Todos 实战
项目结构
bash
todos-fullstack/
├── todos/ # 前端 React + TS
│ ├── src/
│ │ ├── api/ # Axios 封装
│ │ ├── store/ # Zustand 状态管理
│ │ └── components/ # 组件
│ └── package.json
└── todos-backend/ # 后端 NestJS
├── src/
│ └── main.ts
└── package.json
6.1 前端 API 封装
typescript
// src/api/config.ts
import axios from 'axios';
// 创建独立的 axios 实例,避免污染全局配置
const service = axios.create({
baseURL: '/api', // 统一请求前缀,配合 Nginx 反向代理
timeout: 10000,
});
// 请求拦截器:注入 Token
service.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一处理数据
service.interceptors.response.use((response) => {
// 直接返回 response.data,省去业务代码中重复的 .data 解析
return response.data;
});
export default service;
typescript
// src/api/todos.ts
import service from './config';
import { type Todo } from '../types/todo';
// 获取所有 Todos
export const fetchTodos = () => service.get<Todo[]>('/todos');
// 新增 Todo
export const createTodo = (title: string) => service.post<Todo>('/todos', { title });
// 更新 Todo 状态
export const updateTodo = (id: number, patch: Partial<Todo>) =>
service.patch<Todo>(`/todos/${id}`, patch);
// 删除 Todo
export const deleteTodo = (id: number) => service.delete(`/todos/${id}`);
6.2 Zustand 状态管理
typescript
// src/store/todoStore.ts
import { create } from 'zustand';
import { type Todo } from '../types/todo';
import { fetchTodos as getTodos, 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 getTodos();
// 兼容不同返回格式
set({ todos: Array.isArray(res) ? res : res.data || [] });
},
addTodo: async (title: string) => {
const res = await createTodo(title);
set((state) => ({ todos: [...state.todos, res.data || res] }));
},
}));
6.3 组件
tsx
// src/components/TodoList.tsx
import React, { useEffect } from 'react';
import { useTodoStore } from '../store/todoStore';
const TodoList: React.FC = () => {
const { todos, fetchTodos, addTodo } = useTodoStore();
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
const handleAdd = () => {
const title = prompt('Enter todo title:');
if (title) addTodo(title);
};
return (
<div>
<h1>Todo List</h1>
<button onClick={handleAdd}>Add Todo</button>
<ul>
{Array.isArray(todos) && todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
};
export default TodoList;
七、跨域解决方案
跨域的根本原因:同源策略
前端 localhost:5173 请求后端 localhost:3000,端口不同,浏览器会拦截。
markdown
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 前端 │ ✗ │ 浏览器 │ │ 后端 │
│ :5173 │ ←────│ 同源策略 │ ────→│ :3000 │
└─────────────┘ └─────────────┘ └─────────────┘
端口不同,拦截!
方案一:后端开启 CORS
typescript
// NestJS main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 开启跨域
app.enableCors({
origin: 'http://localhost:5173', // 允许的前端地址
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
方案二:Nginx 反向代理(推荐生产环境)
用户访问 http://localhost:80 → Nginx 把 /api 转发给后端 3000 端口,前端完全不知道后端在哪,不存在跨域问题。
nginx
# nginx.conf 示例
server {
listen 80;
location / {
root /usr/share/nginx/html; # 前端静态文件
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000/; # 转发给后端
}
}
⚠️ 面试考点:CORS 和 Nginx 反向代理都能解决跨域,区别是什么?
- CORS:后端设置响应头,告诉浏览器"这个请求我允许"
- Nginx:请求根本不跨域,因为都是从 80 端口发出去的
八、运维常用命令速查
bash
# 查看运行中的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 停止所有容器
docker stop $(docker ps -q)
# 删除所有容器
docker rm $(docker ps -q)
# 删除镜像
docker rmi nginx
# 进入容器终端
docker exec -it mysql-demo /bin/bash
MySQL 容器化示例
bash
# 拉取镜像
docker pull mysql:8.0
# 启动容器
docker run -d \
--name mysql-demo \
-p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
# 进入容器
docker exec -it mysql-demo /bin/bash
# 登录 MySQL
mysql -uroot -p123456
💡 一句话记住:docker ps 看容器,docker images 看镜像
九、常见坑 / 易错点
❌ 坑1:构建时忘记最后的点
bash
# ❌ 错误:缺少构建上下文
docker build -t my-hello
# ✅ 正确:最后的点表示当前目录
docker build -t my-hello .
❌ 坑2:镜像太大下载慢
dockerfile
# ❌ 完整版,约 900MB
FROM node
# ✅ alpine 精简版,约 100MB
FROM node:alpine
❌ 坑3:国内下载慢,不配置镜像源
在 Docker Desktop → Settings → Docker Engine 里添加:
json
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io"
]
}
总结
核心概念速查表
| 概念 | 一句话解释 |
|---|---|
| 镜像 Image | 只读模板,包含应用 + 环境 |
| 容器 Container | 镜像的运行实例 |
| Dockerfile | 构建镜像的配方文件 |
| Nginx | 反向代理服务器 |
| CORS | 后端告诉浏览器"允许跨域" |
核心命令速查
| 场景 | 命令 |
|---|---|
| 构建镜像 | docker build -t 名称 . |
| 运行容器 | docker run --name 名称 -p 宿主端口:容器端口 -d 镜像 |
| 推送镜像 | docker push 用户名/镜像名 |
| 拉取镜像 | docker pull 镜像名 |
核心代码骨架
dockerfile
# Dockerfile 最简模板
FROM node:alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]
结尾
Docker 解决了"我的电脑能跑,你的电脑怎么跑"的问题。Dockerfile 就是那份标准操作手册,写好配方,一键构建,到处部署。
Agent = LLM + Harness (Tool + MCP + RAG + Skill + ...)
Docker = 应用 + 运行环境
希望这篇文章对你有帮助!有问题欢迎在评论区交流 🔥
项目源码:GitHub 链接