Docker 从入门到实战:容器、镜像与 Compose 全攻略

1. 核心概念

Docer 是目前高效的软件部署技术,简单来说就是给应用程序封装独立的运行环境,每一个运行环境就是一个容器(Container) ,运行容器的计算机成为宿主机。

Docker 和虚拟机最大区别是:虚拟机需要完整的操作系统内核,而 Docker 共享宿主机的操作系统内核,所以 Docker 更轻量,资源占用少,启动更快。

Docker 还有一个更重要的概念是:镜像(Image) ,镜像是容器模版,可以把镜像类比成软件安装包,而容器是安装出来的软件。举一个现实世界的例子,镜像是食谱,容器就是根据食谱做出来的菜。可以使用一个菜谱,做出很多个菜,还可以把食谱分享给别人使用。

Docker 仓库(Docker Registry) 就是用来存放分享镜像的地方,每个人都可以把自己的镜像上传到仓库里面,然后其他人就可以下载镜像并且使用了。Docker 的官方仓库就是 Docker Hub,存储了许多人分享的镜像。

了解容器、镜像、镜像仓库这些概念就足够使用 Docker 了。

2. Docker 安装

2.1. windows 安装

  1. 任务栏搜索功能,启用"适用于Linux的Windows子系统" + "虚拟机平台"
  1. 管理员权限打开命令提示符,安装wsl2
css 复制代码
wsl --set-default-version 2
wsl --update --web-download
  1. 下载Windows版本 desktop,进入此项目的Release
    github.com/tech-shrimp...

如果想自己指定安装目录,可以使用命令行的方式 参数 --installation-dir=D:\Docker可以指定安装位置

sql 复制代码
start /w "" "Docker Desktop Installer.exe" install --installation-dir=D:\Docker

2.2. mac 电脑安装

(macOS 从零安装 Docker 完全指南(含镜像加速与代理配置)macOS官网下载Docker Desktop)

2.3. Linux 服务器安装

3. 1.1 Linux

一键安装命令

arduino 复制代码
sudo curl -fsSL https://get.docker.com| bash -s docker --mirror Aliyun

备用命令(每天自动从官网定时同步)

arduino 复制代码
sudo curl -fsSL https://github.com/tech-shrimp/docker_installer/releases/download/latest/linux.sh| bash -s docker --mirror Aliyun

备用2(如果Github访问不了,可以使用Gitee的链接)

arduino 复制代码
sudo curl -fsSL https://gitee.com/tech-shrimp/docker_installer/releases/download/latest/linux.sh| bash -s docker --mirror Aliyun

启动docker

sql 复制代码
sudo service docker start

如果以上不太懂也可以看看我的其他文章的配置,有我的踩坑经历!

手把手教你用 Docker 部署 Vue 项目(含国内镜像加速 + 踩坑指南)

4. Docker 常用命令

介绍几个比较常用的 Docker 命令

4.1. docker pull(拉取/下载镜像)

从仓库下载 nginx 镜像(默认从 Docker Hub)。

bash 复制代码
docker pull docker.io/library/nginx:latest
  • docker.io -> registry:仓库,docker.io 代表是官方仓库,可以省略仓库地址。
  • library -> namespace:命名空间(作者名),library 是官方仓库 的命名空间,可以省略。
  • latest -> tag:标签(版本号),可以指定下载特定版本,省略代表下载最新版本。

简化后是下面这样:

复制代码
docker pull nginx

4.2. docker images(查看镜像)

docker images 代表列出本地所有镜像。

复制代码
docker images

4.3. docker rmi(删除镜像)

docker rmi 代表删除镜像,rm 是 remove 的缩写,i 是 image 的缩写。

bash 复制代码
docker rmi nginx # 删除名字为nginx的镜像
docker rmi 1e5f3c4b56e # 删除id为1e5f3c4b56e的镜像

4.4. docker rm 删除容器

bash 复制代码
docker rm nginx # 删除名字为nginx的容器
docker rm 1e5f3c4b56e # 删除id为1e5f3c4b56e的容器

介绍一个参数:-f(force)强制删除,如果删除正在运行的容器就需要加这个参数

bash 复制代码
docker rm -f 1e5f3c4b56e

4.5. docker run(运行容器)

docker run 代表运行一个容器,这个命令是比较重要的

arduino 复制代码
docker run nginx # 运行名字为nginx的镜像
docker run 1e5f3c4b56e # 运行id为1e5f3c4b56e的镜像
  • 其实 docker pull 可以省略,直接执行 docker run,如果 docker 发现本地不存在镜像会先自动拉取一份,然后再创建并运行容器。

接下来看几个重要参数:

css 复制代码
docker run -d --name web -p 8080:80 nginx

4.5.1. -d(后台运行)

detached 模式(后台运行),没有这个参数时,容器会在前台运行,终端会被"占用"。

4.5.2. --name(命名)

给容器起名字叫 web

  • 如果不指定,Docker 会随机生成一个名字(比如 adoring_turing)。
  • 有名字后,可以用 docker stop webdocker rm web 等来操作。
  • 注意这个名字在整个宿主机上是唯一的

4.5.3. -p(端口映射)

端口映射

  • 8080 → 宿主机端口。
  • 80 → 容器内部的端口(nginx 默认监听 80)。
  • 含义:访问宿主机 http://localhost:8080 就能访问到容器里的 nginx 服务。

Docker 容器的网络和宿主机是隔离的,宿主机访问不到容器(比如我在宿主机安装了一个 nginx 在浏览器输入 localhost:80,就能访问这个 ngxin 提供的网页,但是使用 docker 启动 nginx ,在浏览器 localhost:80 则无法访问对应网页)。用 -p 宿主机端口:容器端口 参数,就能把容器端口映射到宿主机端口,从而在宿主机通过浏览器访问到容器里的服务。

4.5.4. -v(挂载卷)

挂载卷(Volume)就是把宿主机的目录和容器内的目录绑定在一起,容器和宿主机对这个目录的修改会相互影响。它的最大作用是实现数据持久化,即使容器被删除,数据仍保存在宿主机上。

4.5.4.1. -v 宿主机目录:容器内目录(绑定挂载)

简单一句话记: "容器数据不随容器消失,宿主机和容器共享目录"。

xml 复制代码
docker run -d -v <宿主机路径>:<容器路径> --name web nginx

接下来实战一下:

bash 复制代码
docker run -d -p 80:80 -v /root/web:/usr/share/nginx/html nginx

此时出现 403 页面不要慌是因为 web 目录是空的被覆盖了

bash 复制代码
cd web
vi index.html

然后粘贴下面代码,按下 esc 输入:wq!,访问 80 端口就能看到页面了

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Hello Docker Nginx</title>
</head>
<body>
    <div class="box">
        <h1>Hello Docker + Nginx</h1>
        <p>这个页面是从 <code>/root/web/index.html</code> 提供的</p>
        <p>容器挂载卷运行成功</p>
    </div>
</body>
</html>
4.5.4.2. -v 卷的名字:容器内目录(命名卷挂载)

上面的例子直接把宿主机的目录写在了 docker run 命令里面,这种挂载方式叫做绑定挂载。还有另外一种挂载方式是让 Docker 自动创建啊一个存储空间,我们为这个存储空间起一个名字,然后挂载的时候直接使用名字叫可以了,这种挂载方式叫做命名卷挂载。

一句话总结:

  • 绑定挂载:把宿主机指定目录和容器目录直接绑定,容器改动会实时影响宿主机目录。
  • 命名卷挂载:由 Docker 管理的数据卷,不依赖宿主机目录,常用于持久化和共享数据。
arduino 复制代码
docker run -d -p 宿主机端口:容器端口 -v 卷名:容器目录 镜像名
lua 复制代码
docker volume create nginx_html

接下来实战一下

javascript 复制代码
docker run -d -p 80:80 -v nginx_html:/usr/share/nginx/html nginx
  • 这里就不需要写路径了,直接使用挂在卷的名字

查看挂载卷的详情信息

复制代码
docker volume inspect nginx_html 
bash 复制代码
# 然后再进入这个文件修改index.html(注意这个文件路径只有linux系统可以使用,mac或者windows的存在 Docker 自己管理的 Linux 虚拟机里)
cd /var/lib/docker/volumes/nginx_html/_data
vi index.html # 然后把内容替换成上面的index.html,按下i编辑,按下esc,输入:wq!,再访问一下内容就变了
4.5.4.3. 关于挂载卷的一些命令
bash 复制代码
docker volume list   # 列出所有创建的卷
docker volume rm nginx_html   # 删除卷
docker volume prune -a   # 删除所有没有任何容器在使用的卷

这个就是挂载卷在宿主机的真实目录

4.5.5. -e(传递参数)

-e 常用场景是给容器传递配置参数,如数据库账号密码、应用端口、用户名等,让容器启动时自动初始化或配置环境。

ini 复制代码
docker run -d \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -e MYSQL_DATABASE=mydb \
  --name my-mysql \
  mysql:8

然后连接

css 复制代码
mysql -h 49.233.249.191 -P 3306 -u root -p

填写自己宿主机的 ip 地址,如果出现 Welcome 就代表连接成功,如果连接不上要设置入站规则把 3306 端口打开。

4.5.6. -it(交互)和 --rm(容器停止时候删除容器)

-i:Interactive(交互式) ,保持容器的标准输入(stdin)打开,这样你可以在容器里输入命令进行交互。

--rm:容器停止后自动删除,用于临时容器,避免占用磁盘空间。

bash 复制代码
docker run -it --rm ubuntu /bin/bash
  • 启动一个临时 Ubuntu 容器
  • 可以交互式操作
  • 容器退出后自动删除

这种用法非常适合 调试、临时测试环境

4.5.7. --restart(配置重启策略)

--restart 用来指定 容器退出后 Docker 是否自动重启容器,可以确保服务长期可用。

下面介绍两个比较常用的选项

4.5.7.1. always (总是重启)
ini 复制代码
docker run -d --restart=always nginx
  • 容器停止或 Docker 重启,nginx 容器会自动启动
4.5.7.2. unless-stopped
ini 复制代码
docker run -d --restart=unless-stopped nginx
  • unless-stopped 表示 除非手动停止,否则容器会自动重启
    • 容器异常退出 → Docker 自动重启
    • Docker 守护进程重启 → 容器自动重启
    • 手动执行 docker stop → 容器不会再自动重启

4.6. docker ps(查看容器状态)

docker ps 代表查看容器状态,ps 就是 process status(进程状态)的缩写

复制代码
docker ps

5. Docker 调试命令

5.1. docker inspect(查看信息)

docker inspect 用来 查看 Docker 对象的详细信息 ,返回结果是 JSON 格式,包括容器、镜像、网络、卷等的各种属性。

  • 容器信息:IP、端口映射、挂载卷、环境变量、状态等
  • 镜像信息:ID、标签、创建时间、大小、层结构
  • 网络信息:子网、网关、关联容器
  • 卷信息:挂载路径、创建时间、驱动类型
bash 复制代码
docker ps nginx   # 可以加名字或者id

5.2. docker create(创建容器)

docker create 用来 创建一个容器,但不启动它

  • 它只会在 Docker 中生成一个容器实例
  • 不会像 docker run 那样立即启动容器,如果想启动需要输入 docker start
bash 复制代码
docker create nginx   # 可以加名字或者id

5.3. docker logs(查看日志)

复制代码
docker logs nginx -f
  • -f 等于 --follow ,也就是追踪输出

5.4. docker exec (执行命令)

docker exec 用来 在已经运行的容器中执行命令 。 容器必须是 正在运行

bash 复制代码
docker exec -it nginx 
  • -it:加上这个参数可以进入容器的交互式终端 , 可以在里面敲命令、查看文件、修改内容 , 类似直接在 Linux 系统里操作 。

6. Dockerfile

Dockerfile 是一个文本文件,用来 定义 Docker 镜像的构建过程

如果说菜是容器,镜像是食谱,那么详细的烹饪步骤和食谱说明书,告诉你怎么一步步做出镜像 。

在本地运行 Vue 项目时,我们通常先安装 Node,然后执行 npm install 下载依赖,再通过 npm run dev 启动项目。将这个流程打包成 Docker 镜像的原理类似,只需要通过编写 Dockerfile 就能实现自动化构建和运行。

  1. 首先要创建一个 Dockerfile 文件
bash 复制代码
# 1. 使用 Node 镜像
FROM node:20-alpine    # 相当于安装node环境依赖 

# 2. 设置工作目录
WORKDIR /app

# 3. 复制 package.json 和 package-lock.json
COPY package*.json ./    # 需要安装的依赖

# 4. 安装依赖
RUN npm install

# 5. 复制项目文件
COPY . .

# 6. 暴露开发服务器端口
EXPOSE 5173

# 7. 启动 Vue 开发服务器
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
  1. Dockerfile 准备好之后就可以构建镜像了,进入到项目根路径
bash 复制代码
docker build -t docker_demo .
# -t意思是给镜像起一个名字,.的意思是在当前文件夹构建
  1. 镜像构建好了之后,可以使用 docker run 命令基于这个镜像创建一个容器并且运行起来。
yaml 复制代码
docker run -d -p 5173:5173 docker_demo 
# -d是后台运行容器,-p是端口映射,docker_demo是镜像的名字

然后在浏览器输入http://localhost:5173/就可以看到了

7. 上传 Docker Hub

首先要去官网注册 Docker Hub,然后再使用命令登录

复制代码
docker login

推送镜像的时候必须在前面加上作者的用户名,所以现在重新打一个镜像

bash 复制代码
docker build -t mingli03/docker_demo .

接下来 push 自己的镜像

bash 复制代码
docker push mingli03/docker_demo

docker.io 是仓库的地址,mingli03 是命名空间,docker_demo 是镜像名/

推送成功之后,在 docker hub 里面输入 命名空间/镜像名 就能搜索到镜像了,其他人使用 docker pull 就可以拉取镜像了

bash 复制代码
docker pull mingli03/docker_demo

8. Docker 网络

Docker 网络是容器之间以及容器与外部世界通信的基础。可以把它理解为 容器的虚拟网络环境

8.1. Docker 网络的作用

  • 同一宿主机的容器互相通信
  • 容器访问外部网络(如互联网)
  • 控制容器之间的 隔离和访问权限
  • 支持 多主机容器互联(Docker Swarm/Overlay 网络)

8.2. Docker 默认网络类型

Docker 创建容器时,如果不指定网络,会自动连接到 默认网络(bridge),主要有三类:

网络类型 描述
bridge 默认网络,容器与宿主机隔离,但同一桥接网络的容器可以互通
host 容器共享宿主机网络,端口直接映射到宿主机
none 容器没有网络

8.3. bridge 模式

Docker 默认使用 bridge(桥接)网络 ,所有容器都会分配一个内部 IP(通常以 172.17 开头)。在同一网络内,容器可以通过 IP 或容器名互相访问,但容器网络与宿主机网络相互隔离。通过 docker network create 可以创建自定义子网(也是桥接模式),同一子网内的容器可以互通,跨子网容器默认无法通信,同时可以直接用容器名访问而无需记 IP。

  1. 创建自定义网络
lua 复制代码
docker network create --driver bridge my-net
  • my-net → 网络名称
  • --driver bridge → 桥接模式
  1. 运行两个容器加入同一网络
css 复制代码
# 运行 Nginx 容器
docker run -d --name web --network my-net nginx

# 运行 BusyBox 容器,用来测试网络
docker run -it --name tester --network my-net busybox
  1. 测试容器互通

tester 容器里执行:

arduino 复制代码
# ping web 容器
ping web

说明 同一子网内,容器可以通过名字互相通信。

8.4. host 模式

Docker 容器直接共享宿主机的网络,容器直接使用宿主机的 IP 地址,而且无需 -p 进行端口映射,容器内的服务直接运行在宿主机的端口上,通过宿主机的 IP 和端口就能访问到容器。

arduino 复制代码
docker run -d --network host nginx

8.5. docker network list(查看网络)

展示出所有 Docker 网络

复制代码
docker network list

这个 my-net 就是刚刚创建的自定义网络

8.6. docker network rm(删除网络)

bash 复制代码
docker network rm 713a88f03530
  • 默认的三种网络模式不能删除,只能删除自定义网络。
  • 这里删除了刚刚自定义的网络,再执行 list 就查看不到这个网络了。

9. Docker Compose

9.1. 概念

在实际开发中,一个完整的应用往往由多个部分组成,比如前端、后端和数据库。

如果我们把所有模块都打包进一个"大容器",表面上看似简单,但会带来严重问题:

  • 可靠性差:一旦某个模块(如后端内存泄漏)出问题,整个大容器都可能崩溃。
  • 扩展性差:无法单独扩容某个模块,比如想多开几个数据库实例,只能把整个大容器再复制一份,既浪费资源又不灵活。

因此最佳实践是将每个模块单独容器化,形成多个容器(前端容器、后端容器、数据库容器等),这样既能独立维护,也能灵活扩展。

但问题来了:

  • docker run 创建多个容器时,需要写多条命令。
  • 容器之间的网络、依赖、挂载卷等需要手动配置。
  • 容器数量一多,管理起来就很容易出错。

这时候就需要 容器编排工具 ------ Docker Compose

Docker Compose 使用一个 docker-compose.yml 文件,把多个容器的定义和它们之间的协作方式写进去。它可以看作是把多条 docker run 命令写成一个清晰的配置文件。通过一条命令:

复制代码
docker compose up -d

就能把整个应用(前端 + 后端 + 数据库)同时启动,并自动加入同一个网络。

9.2. 实战

新创建一个文件夹

复制代码
docker-bushu/
│
├─ docker-compose.yml
├─ backend/
│   └─ app.jar
├─ frontend/
│   ├─ dist/
│   └─ nginx.conf
└─ mysql_data/   (存放初始化数据库文件或持久化数据)
  1. nginx.conf
ini 复制代码
server {
    listen 80;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
  1. docker-compose.yml
yaml 复制代码
version: "3.9"

services:
  frontend:
    image: nginx:alpine
    container_name: my-frontend
    ports:
      - "8080:80"
    volumes:
      - ./frontend/dist:/usr/share/nginx/html
      - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - backend

  backend:
    image: openjdk:17-jdk-alpine
    container_name: my-backend
    volumes:
      - ./backend/app.jar:/app/app.jar
    command: ["java", "-jar", "/app/app.jar"]
    ports:
      - "8081:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: 123456
    depends_on:
      - db

  db:
    image: mysql:8
    container_name: my-mysql
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: mydb
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"
  1. vite.config.js
javascript 复制代码
server: {
    proxy: {
      "/api": {
        target: "http://localhost:8080", // 后端接口地址
        changeOrigin: true, // 修改请求头中的 origin
        rewrite: path => path.replace(/^/api/, ""), // 去掉前缀 /api
      },
    },
  },
  1. 前端请求路径
javascript 复制代码
 axios.post("/api/user/register", form).then((res) => {
    console.log("注册成功:", res);
  });
  1. 请求忽略(WebConfig)
typescript 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 正确的配置方式
        registry.addInterceptor(loginInterceptor)
        // 1. 先指定拦截所有路径
        .addPathPatterns("/**")
        // 2. 然后排除不需要拦截的路径
        .excludePathPatterns(
            "/user/login",
            "/user/register"    
        );
    }
}
  1. 请求路径
less 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public Result register(@RequestBody @Valid User user) {
        // 校验通过才会进入这个方法体
        // 否则会抛出 MethodArgumentNotValidException 异常

        // 1. 查找用户名字是否一样
        User existingUser = userService.findByUsername(user.getUsername());

        if (existingUser == null) {
            // 不一样就添加
            userService.register(user);
            return Result.success();
        } else {
            return Result.error("用户名已存在");
        }
    }
}

然后把这个文件夹传递到服务器上

复制代码
docker compose up -d

如果前端或后端更新了:

  • 停止对应容器:
arduino 复制代码
docker compose stop frontend
  • 替换 distapp.jar 文件

  • 再启动容器:

    docker compose up -d frontend

相关推荐
龙在天几秒前
你只会console.log就Out了
前端
用户681722457212 分钟前
h5实现点击电话进入拨打电话功能
前端
菠萝+冰1 小时前
在 React 中,父子组件之间的通信(传参和传方法)
前端·javascript·react.js
庚云1 小时前
一套代码如何同时适配移动端和pc端
前端
Jinuss1 小时前
Vue3源码reactivity响应式篇Reflect和Proxy详解
前端·vue3
海天胜景1 小时前
vue3 el-select 默认选中第一个
前端·javascript·vue.js
小小怪下士_---_1 小时前
uniapp开发微信小程序自定义导航栏
前端·vue.js·微信小程序·小程序·uni-app
前端W1 小时前
腾讯地图组件使用说明文档
前端
页面魔术1 小时前
无虚拟dom怎么又流行起来了?
前端·javascript·vue.js
胡gh2 小时前
如何聊懒加载,只说个懒可不行
前端·react.js·面试