一文搞懂 Docker + Dockerfile:从镜像构建到全栈部署

一文搞懂 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 链接

相关推荐
kyriewen5 小时前
我踩了3次同一个坑才明白:JavaScript里比0.1+0.2更隐蔽的5个数字陷阱
前端·javascript·面试
众人皆醒我独醉8 小时前
自动扩缩容:KPA/HPA/KEDA 三条路径
面试·kubernetes·llm
黄敬峰9 小时前
一文搞懂 SQL 建表、索引与约束:博客系统数据库设计实战
面试
我叫黑大帅12 小时前
关于没有对生产者做校验的思考
前端·面试·架构
码匠许师傅12 小时前
【C++ 面试真题】聊聊 C++ 的类型特征( type_traits)
java·c++·面试
FPC工厂——皇榜科技12 小时前
【皇榜科技线路板质量课堂·第40篇】PPAP(生产件批准程序):批量生产前的“终极面试”
科技·面试·职场和发展·制造·pcb工艺
校招VIP12 小时前
简历魔改短链接?不是好选择
面试·秋招·校招·简历·实习·27届·开发岗
Lafar13 小时前
iOS 文件下载 断点续传
面试