
本文将从底层逻辑出发,用通俗的语言彻底讲透镜像、仓库、容器的本质,结合大量可直接运行的实战案例,覆盖从基础操作到企业级实战的全场景,帮助你真正夯实Docker基础,解决实际开发中的核心问题。
在深入细节之前,我们先通过一张架构图理清镜像、仓库、容器三者的关系,建立整体认知:

三者的核心流转逻辑:从仓库拉取镜像 → 基于镜像创建容器 → 容器运行应用 → 可将容器修改提交为新镜像 → 推送新镜像到仓库共享。
接下来,我们分别对这三大要素进行深度拆解。
很多开发者误以为镜像就是一个单一的文件,其实不然。Docker镜像采用联合文件系统(UnionFS) 构建,本质是由多个只读层(Layer)叠加而成的分层结构。
联合文件系统的核心能力是“将多个目录挂载到同一个虚拟文件系统下,实现‘叠加’的文件访问效果”。Docker利用这一特性,让镜像的每一层都对应一个独立的文件系统层,所有层叠加后形成一个统一的虚拟文件系统,供容器使用。
用通俗的话解释:镜像就像一个“千层蛋糕”,每一层都是一块独立的“蛋糕片”(只读层),叠加后形成完整的“蛋糕”(镜像)。容器运行时,会在最上层添加一个可读写层,所有运行时的修改都只发生在这一层,不会影响底层的只读镜像层。
以下操作均经过验证,可直接在Docker环境中运行(Docker版本建议20.10+)。
功能:从仓库拉取指定版本的镜像,未指定版本时默认拉取latest(最新稳定版)。
基础语法:
docker pull [仓库地址/][镜像名]:[标签]
实战示例:
# 拉取Docker Hub官方的openjdk镜像(JDK17,最新稳定版)
docker pull openjdk:17-jdk-slim
# 拉取阿里云镜像仓库的MySQL8.0镜像(企业级常用)
docker pull registry.cn-hangzhou.aliyuncs.com/library/mysql:8.0.36
# 拉取私有仓库的自定义镜像(假设私有仓库地址为registry.jam.com)
docker pull registry.jam.com/demo/app:1.0.0
功能:查看本地已下载的所有镜像,包含镜像ID、标签、大小等信息。
实战示例:
# 查看所有镜像
docker images
# 查看指定镜像(如openjdk)
docker images openjdk
# 格式化输出镜像信息(仅显示镜像ID、名称、标签)
docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}"
功能:删除本地指定镜像,需确保该镜像未被任何容器使用(否则需先删除容器)。
基础语法:
docker rmi [镜像ID/镜像名:标签]
实战示例:
# 通过镜像ID删除(推荐,唯一标识)
docker rmi 1e70071f49e2
# 通过镜像名:标签删除
docker rmi openjdk:17-jdk-slim
# 强制删除(即使镜像被容器引用,慎用)
docker rmi -f openjdk:17-jdk-slim
# 批量删除所有未使用的镜像(清理空间常用)
docker image prune -a -f
功能:用于镜像的离线传输(如无网络环境下部署)。
实战示例:
# 导出openjdk:17-jdk-slim镜像为tar包
docker save -o openjdk17.tar openjdk:17-jdk-slim
# 导入tar包为本地镜像
docker load -i openjdk17.tar
镜像构建是Docker核心技能之一,而Dockerfile是构建镜像的“脚本文件”,包含了一系列构建镜像的指令。掌握Dockerfile的编写规范和优化技巧,是打造轻量、高效镜像的关键。
以下是企业级开发中最常用的指令,每个指令对应镜像的一层,指令顺序直接影响镜像大小和构建效率:
指令 | 功能说明 | 核心注意事项 |
|---|---|---|
FROM | 指定基础镜像(必须是Dockerfile的第一条指令) | 优先选择官方精简镜像(如slim、alpine版本),减少基础镜像大小 |
WORKDIR | 指定容器运行时的工作目录,后续指令(RUN/COPY/CMD等)均基于此目录 | 避免使用绝对路径拼接,推荐使用相对路径,如WORKDIR /app |
COPY | 将本地文件/目录复制到镜像中 | 可使用.dockerignore文件排除无需复制的文件(如target/*.xml、.git),减少镜像大小 |
ADD | 类似COPY,支持自动解压压缩包、下载URL资源 | 非必要不使用(解压功能易导致镜像臃肿),优先使用COPY |
RUN | 构建镜像时执行的命令(如安装依赖、编译代码) | 多个RUN指令可通过&&合并,减少镜像层数;避免在RUN中执行无关操作 |
ENV | 设置环境变量(全局有效,容器运行时可覆盖) | 用于配置版本号、路径等可变参数,提高Dockerfile可维护性 |
EXPOSE | 声明容器运行时暴露的端口(仅为文档说明,不实际映射端口) | 需与容器运行时的-p参数配合使用,建议与应用实际端口一致 |
CMD | 指定容器启动时执行的命令(一个Dockerfile仅能有一个有效CMD,多则覆盖) | 可被docker run命令末尾的参数覆盖,推荐使用数组格式(如["java","-jar","app.jar"]) |
ENTRYPOINT | 容器启动时执行的命令(不可被docker run参数覆盖,可与CMD配合使用) | 适合用于固定启动流程的场景(如启动脚本),CMD作为参数传递给ENTRYPOINT |
以Spring Boot应用为例,编写符合阿里巴巴开发规范的Dockerfile,包含分层构建优化(减少镜像大小、提高构建效率):
首先,项目结构(核心文件):
demo-app/
├── src/
├── pom.xml
├── Dockerfile
└── .dockerignore
# 编译相关文件
target/*.log
target/*.txt
target/maven-archiver/
# 开发工具相关文件
.git
.idea
*.iml
# 系统文件
.DS_Store
# 第一阶段:构建阶段(使用maven镜像编译代码)
FROM maven:3.9.6-eclipse-temurin-17 AS builder
# 设置工作目录
WORKDIR /app
# 复制pom.xml和依赖文件,优先构建依赖层(利用Docker缓存)
COPY pom.xml .
# 下载所有依赖(单独分离出来,避免代码修改导致依赖重新下载)
RUN mvn dependency:go-offline -Dmaven.test.skip=true
# 复制源代码
COPY src ./src
# 编译打包(跳过测试,生成jar包)
RUN mvn package -Dmaven.test.skip=true -DskipTests
# 第二阶段:运行阶段(使用精简JDK镜像,减少镜像大小)
FROM eclipse-temurin:17-jre-slim
# 设置时区(解决时区不一致问题)
ENV TZ=Asia/Shanghai
# 创建非root用户(符合安全规范,避免容器以root权限运行)
RUN addgroup --system jam && adduser --system --group jam
# 设置工作目录
WORKDIR /app
# 从构建阶段复制编译好的jar包到当前目录
COPY --from=builder /app/target/*.jar app.jar
# 切换到非root用户
USER jam
# 声明暴露的端口(与Spring Boot应用端口一致)
EXPOSE8080
# 容器启动命令(数组格式,避免shell解析问题)
CMD ["java", "-jar", "app.jar"]
功能:基于Dockerfile构建镜像,指定镜像名称和标签。
实战示例:
# 基本构建(当前目录的Dockerfile,镜像名:demo-app,标签:1.0.0)
docker build -t demo-app:1.0.0 .
# 指定Dockerfile路径构建
docker build -t demo-app:1.0.0 -f ./docker/Dockerfile .
# 构建时指定环境变量(覆盖Dockerfile中的ENV)
docker build -t demo-app:1.0.0 --build-arg TZ=Asia/Beijing .
# 不使用缓存构建(适用于依赖更新场景)
docker build -t demo-app:1.0.0 --no-cache .
分层构建:如上述示例,将构建阶段和运行阶段分离,运行阶段仅保留必要的jar包和JRE,大幅减少镜像大小(从数百MB缩减到几十MB)。
利用Docker缓存:将不变的指令(如FROM、COPY pom.xml、RUN mvn dependency:go-offline)放在前面,频繁变化的指令(如COPY src、RUN mvn package)放在后面,避免每次构建都重新执行所有步骤。
选择精简基础镜像:优先使用alpine或slim版本的基础镜像,例如:
清理构建残留:在RUN指令中及时清理无用文件,例如:
RUN apt-get update && apt-get install -y gcc \
&& gcc --version \
&& apt-get remove -y gcc \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
避免以root用户运行:创建非root用户,减少容器被攻击的风险(如上述示例中的addgroup和adduser指令)。
以一个Spring Boot应用为例,对比优化前后的镜像大小:
构建方式 | 基础镜像 | 镜像大小 | 优化点 |
|---|---|---|---|
原始构建 | openjdk:17 | 680MB | 未分层构建,包含JDK和构建依赖 |
分层构建(slim) | eclipse-temurin:17-jre-slim | 180MB | 分层构建,仅含JRE,精简基础镜像 |
分层构建(alpine) | eclipse-temurin:17-jre-alpine | 120MB | 基于alpine,进一步缩减体积 |
通过优化,镜像大小减少了80%以上,大幅提升镜像拉取和部署效率。
很多开发者误以为容器是“轻量级虚拟机”,这是一个常见的认知误区。容器的本质是一个被Docker封装的、具有独立文件系统的进程,其隔离性是通过Linux内核的三大技术实现的,而非像虚拟机那样需要完整的操作系统内核。
通过流程图理解容器的创建过程:

特性 | 容器 | 虚拟机(VM) |
|---|---|---|
隔离级别 | 进程级隔离(共享宿主机内核) | 系统级隔离(独立内核) |
功能:基于镜像创建并启动容器,是最常用的Docker命令之一。
基础语法:
docker run [选项] 镜像名:标签 [启动命令参数]
企业级常用选项说明:
选项 | 功能说明 |
|---|---|
-d | 后台运行容器(守护进程模式) |
-p | 端口映射(宿主机端口:容器端口),支持多个-p参数 |
-v | 数据卷挂载(宿主机目录/文件:容器目录/文件),实现数据持久化 |
--name | 指定容器名称(唯一) |
--network | 指定容器使用的网络(如bridge、host、自定义网络) |
-e | 设置容器环境变量(覆盖镜像中的ENV指令) |
--memory | 限制容器最大内存(如--memory=2g) |
--cpus | 限制容器使用的CPU核心数(如--cpus=2) |
--restart | 容器重启策略(如always:容器退出时总是重启;on-failure:非0退出码时重启) |
--user | 指定容器运行用户(如--user=jam) |
--link | 链接到其他容器(已过时,推荐使用自定义网络) |
实战示例1:启动MySQL容器(数据持久化+环境变量配置)
# 启动MySQL 8.0容器,配置数据持久化、root密码、端口映射
docker run -d \
--name mysql8 \
-p 3306:3306 \
-v /data/mysql8/data:/var/lib/mysql \
-v /data/mysql8/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
-e MYSQL_DATABASE=demo_db \
-e TZ=Asia/Shanghai \
--restart always \
mysql:8.0.36 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
说明:
实战示例2:启动Spring Boot应用容器(关联MySQL+端口映射)
# 启动demo-app容器,关联MySQL,配置应用端口
docker run -d \
--name demo-app \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=Root@123456 \
--link mysql8:mysql8 \
--restart always \
demo-app:1.0.0
说明:
功能:查看容器的运行状态,包含容器ID、名称、端口映射、启动时间等信息。
实战示例:
# 查看正在运行的容器
docker ps
# 查看所有容器(包括停止的)
docker ps -a
# 查看容器的详细信息(JSON格式)
docker inspect demo-app
# 查看容器的日志(实时输出)
docker logs -f demo-app
# 查看容器的进程
docker top demo-app
功能:管理容器的生命周期(启动、停止、重启、删除)。
实战示例:
# 启动停止的容器
docker start demo-app
# 停止运行的容器
docker stop demo-app
# 重启容器
docker restart demo-app
# 强制停止容器(类似kill命令)
docker kill demo-app
# 删除停止的容器
docker rm demo-app
# 强制删除运行中的容器(慎用)
docker rm -f demo-app
# 批量删除所有停止的容器
docker container prune -f
数据卷是Docker中用于持久化数据的核心机制,解决了容器内数据易丢失的问题。数据卷分为三种类型:
实战示例:
# 创建命名卷
docker volume create mysql-data
# 查看所有数据卷
docker volume ls
# 查看数据卷的详细信息
docker volume inspect mysql-data
# 启动容器时使用命名卷
docker run -d \
--name mysql8-volume \
-p 3307:3306 \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
mysql:8.0.36
# 删除数据卷(需先删除使用该卷的容器)
docker volume rm mysql-data
# 批量删除未使用的数据卷
docker volume prune -f
容器网络是实现容器间通信和容器与外部通信的核心,Docker默认提供三种网络模式:
实战示例:
# 查看所有网络
docker network ls
# 创建自定义网络(桥接模式,推荐企业级使用,实现容器间安全通信)
docker network create demo-network
# 启动容器时加入自定义网络
docker run -d \
--name mysql8-network \
--network demo-network \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
mysql:8.0.36
docker run -d \
--name demo-app-network \
--network demo-network \
-p 8081:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8-network:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai \
demo-app:1.0.0
# 查看网络中的容器
docker network inspect demo-network
# 删除自定义网络(需先删除网络中的容器)
docker network rm demo-network
# 批量删除未使用的网络
docker network prune -f
说明:自定义网络中,容器可以通过容器名称直接通信(Docker内置DNS解析),无需使用--link参数,更安全、更易管理。
功能:在运行的容器内执行命令(如查看日志、调试应用、执行SQL等)。
实战示例:
# 进入容器的交互式终端(常用,类似ssh登录)
docker exec -it mysql8 /bin/bash
# 在容器内执行SQL命令(无需进入终端)
docker exec -it mysql8 mysql -u root -pRoot@123456 -e "SELECT DATABASE();"
# 查看容器内应用日志(Spring Boot应用)
docker exec -it demo-app tail -f /app/logs/app.log
# 复制容器内的文件到宿主机
docker cp demo-app:/app/app.jar /local/path/
# 复制宿主机的文件到容器内
docker cp /local/path/application.yml demo-app:/app/
以“Spring Boot应用容器 + MySQL容器”为例,实现容器间通信:
docker network create demo-network
docker run -d \
--name mysql8 \
--network demo-network \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
-e MYSQL_DATABASE=demo_db \
mysql:8.0.36
docker run -d \
--name demo-app \
--network demo-network \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=Root@123456 \
demo-app:1.0.0
# 进入demo-app容器,测试连接MySQL
docker exec -it demo-app /bin/bash
# 在容器内执行ping命令(需先安装ping工具)
apt-get update && apt-get install -y iputils-ping
ping mysql8 # 能ping通说明网络通信正常
# 测试MySQL连接
telnet mysql8 3306 # 能连接说明端口开放正常
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>demo-app</artifactId>
<version>1.0.0</version>
<name>demo-app</name>
<description>Docker容器化实战项目</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<fastjson2.version>2.0.47</fastjson2.version>
<springdoc.version>2.3.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MyBatis-Plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok依赖(@Slf4j) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- FastJSON2依赖 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- Springdoc OpenAPI(Swagger3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Spring工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Google集合工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring:
datasource:
url:jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username:root
password:Root@123456
driver-class-name:com.mysql.cj.jdbc.Driver
jackson:
date-format:yyyy-MM-ddHH:mm:ss
time-zone:Asia/Shanghai
mybatis-plus:
mapper-locations:classpath:mapper/*.xml
type-aliases-package:com.jam.demo.entity
configuration:
map-underscore-to-camel-case:true
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
server:
port:8080
servlet:
context-path:/demo
# Swagger3配置
springdoc:
api-docs:
path:/api-docs
swagger-ui:
path:/swagger-ui.html
operationsSorter:method
packages-to-scan:com.jam.demo.controller
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
*
* @author ken
* @date 2025-05-20
*/
@Data
@TableName("t_user")
publicclass User {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码(加密存储)
*/
private String password;
/**
* 手机号
*/
private String phone;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.springframework.stereotype.Repository;
/**
* 用户Mapper接口
*
* @author ken
* @date 2025-05-20
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
}
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.time.LocalDateTime;
/**
* 用户服务类
*
* @author ken
* @date 2025-05-20
*/
@Slf4j
@Service
publicclass UserService extends ServiceImpl<UserMapper, User> implements IService<User> {
/**
* 保存用户信息
*
* @param user 用户实体
* @return 保存结果(true:成功,false:失败)
*/
public boolean saveUser(User user) {
// 校验用户实体是否为空
if (ObjectUtils.isEmpty(user)) {
log.error("保存用户失败:用户实体为空");
returnfalse;
}
// 校验用户名是否为空
if (org.springframework.util.StringUtils.isEmpty(user.getUsername())) {
log.error("保存用户失败:用户名为空");
returnfalse;
}
// 设置创建时间和更新时间
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
log.info("保存用户信息:{}", com.alibaba.fastjson2.JSON.toJSONString(user));
return save(user);
}
}
package com.jam.demo.controller;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 用户控制器
*
* @author ken
* @date 2025-05-20
*/
@Slf4j
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理接口", description = "提供用户的增删改查操作")
publicclass UserController {
@Resource
private UserService userService;
/**
* 保存用户
*
* @param user 用户实体
* @return 保存结果
*/
@PostMapping("/save")
@Operation(summary = "保存用户", description = "新增或修改用户信息")
@ApiResponse(responseCode = "200", description = "操作成功", content = @Content(schema = @Schema(implementation = Boolean.class)))
public ResponseEntity<Boolean> saveUser(@RequestBody User user) {
log.info("接收保存用户请求:{}", JSON.toJSONString(user));
boolean result = userService.saveUser(user);
returnnew ResponseEntity<>(result, HttpStatus.OK);
}
/**
* 根据ID查询用户
*
* @param id 用户ID
* @return 用户实体
*/
@GetMapping("/{id}")
@Operation(summary = "根据ID查询用户", description = "通过用户ID获取用户详细信息")
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = User.class)))
public ResponseEntity<User> getUserById(
@Parameter(description = "用户ID", required = true) @PathVariable Long id) {
// 校验ID是否为空
if (ObjectUtils.isEmpty(id)) {
log.error("查询用户失败:ID为空");
returnnew ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
User user = userService.getById(id);
returnnew ResponseEntity<>(user, HttpStatus.OK);
}
/**
* 分页查询用户
*
* @param pageNum 页码
* @param pageSize 每页条数
* @return 分页用户列表
*/
@GetMapping("/page")
@Operation(summary = "分页查询用户", description = "分页获取用户列表")
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = IPage.class)))
public ResponseEntity<IPage<User>> getUserPage(
@Parameter(description = "页码", required = true, example = "1") @RequestParam Integer pageNum,
@Parameter(description = "每页条数", required = true, example = "10") @RequestParam Integer pageSize) {
// 校验参数
if (ObjectUtils.isEmpty(pageNum) || ObjectUtils.isEmpty(pageSize) || pageNum < 1 || pageSize < 1) {
log.error("分页查询用户失败:参数无效,pageNum={}, pageSize={}", pageNum, pageSize);
returnnew ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
IPage<User> page = new Page<>(pageNum, pageSize);
IPage<User> userPage = userService.page(page);
returnnew ResponseEntity<>(userPage, HttpStatus.OK);
}
/**
* 根据ID删除用户
*
* @param id 用户ID
* @return 删除结果
*/
@DeleteMapping("/{id}")
@Operation(summary = "根据ID删除用户", description = "通过用户ID删除用户信息")
@ApiResponse(responseCode = "200", description = "删除成功", content = @Content(schema = @Schema(implementation = Boolean.class)))
public ResponseEntity<Boolean> deleteUserById(
@Parameter(description = "用户ID", required = true) @PathVariable Long id) {
if (ObjectUtils.isEmpty(id)) {
log.error("删除用户失败:ID为空");
returnnew ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
boolean result = userService.removeById(id);
returnnew ResponseEntity<>(result, HttpStatus.OK);
}
}
package com.jam.demo;
import org.mybatisplus.annotation.DbType;
import org.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
* 应用启动类
*
* @author ken
*/
@SpringBootApplication
publicclass DemoAppApplication {
public static void main(String[] args) {
SpringApplication.run(DemoAppApplication.class, args);
}
/**
* 配置MyBatis-Plus分页插件
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
CREATE TABLE`t_user` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`username`varchar(50) NOTNULLCOMMENT'用户名',
`password`varchar(100) NOTNULLCOMMENT'密码(加密存储)',
`phone`varchar(20) DEFAULTNULLCOMMENT'手机号',
`create_time` datetime NOTNULLCOMMENT'创建时间',
`update_time` datetime NOTNULLCOMMENT'更新时间',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_username` (`username`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';
# 进入项目根目录
cd demo-app
# 编译打包(跳过测试)
mvn clean package -Dmaven.test.skip=true
docker build -t demo-app:1.0.0 .
docker run -d \
--name mysql8 \
--network demo-network \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=Root@123456 \
-e MYSQL_DATABASE=demo_db \
-e TZ=Asia/Shanghai \
--restart always \
mysql:8.0.36 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
# 进入MySQL容器
docker exec -it mysql8 /bin/bash
# 登录MySQL
mysql -u root -pRoot@123456
# 切换到demo_db数据库
use demo_db;
# 执行建表语句(复制上述SQL语句粘贴执行)
docker run -d \
--name demo-app \
--network demo-network \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=Root@123456 \
--restart always \
demo-app:1.0.0
# 查看容器日志
docker logs -f demo-app
# 访问Swagger3页面(验证应用启动成功)
curl http://localhost:8080/demo/swagger-ui.html
# 测试接口(新增用户)
curl -X POST -H "Content-Type: application/json" -d '{"username":"test","password":"123456","phone":"13800138000"}' http://localhost:8080/demo/user/save
仓库是用于集中存储和管理Docker镜像的服务,类似代码仓库Git,核心作用是实现镜像的共享与分发。
仓库与镜像的关系类似“文件夹与文件”:一个仓库可以包含多个镜像,每个镜像通过“标签(Tag)”区分版本。例如,nginx仓库下可以包含nginx:1.24、nginx:1.25等多个版本的镜像。
核心关联逻辑:
[仓库地址]/[仓库名称]:[标签](缺省仓库地址时,默认指向Docker Hub)registry.cn-hangzhou.aliyuncs.com/jam-demo/demo-app:1.0.0(阿里云私有仓库的demo-app镜像,版本1.0.0)nginx:1.24(默认Docker Hub的nginx仓库,版本1.24)通过流程图理解镜像与仓库的流转:

Docker Hub是最基础的公有仓库,适合存储开源镜像或个人学习用镜像。以下操作需先注册Docker Hub账号(https://hub.docker.com/)。
推送镜像到Docker Hub前,需确保镜像标签符合格式:[Docker Hub用户名]/[仓库名称]:[标签],否则无法推送。
实战示例:
# 假设Docker Hub用户名为jamdev,要推送的镜像名为demo-app,版本1.0.0
# 给本地镜像打标签(本地镜像名为demo-app:1.0.0)
docker tag demo-app:1.0.0 jamdev/demo-app:1.0.0
# 查看打标签后的镜像
docker images jamdev/demo-app:1.0.0
# 输入Docker Hub用户名和密码登录
docker login
# 登录成功提示:Login Succeeded
# 推送标签后的镜像
docker push jamdev/demo-app:1.0.0
# 推送成功后,可在Docker Hub网页端查看:https://hub.docker.com/repository/docker/jamdev/demo-app
# 其他用户无需登录即可拉取公有镜像
docker pull jamdev/demo-app:1.0.0
docker logout
阿里云ACR国内访问速度快,支持公有和私有仓库,企业级使用推荐。以下操作需先注册阿里云账号并开通ACR服务(https://cr.console.aliyun.com/)。
registry.cn-hangzhou.aliyuncs.com/[阿里云账号ID]/demo-app(可在仓库详情页查看)。镜像标签需符合格式:[阿里云ACR仓库地址]:[标签]
实战示例:
# 假设阿里云ACR仓库地址为registry.cn-hangzhou.aliyuncs.com/jam123456/demo-app(jam123456为阿里云账号ID)
# 给本地demo-app:1.0.0镜像打标签
docker tag demo-app:1.0.0 registry.cn-hangzhou.aliyuncs.com/jam123456/demo-app:1.0.0
阿里云ACR登录需使用“访问凭证”(推荐)或阿里云账号密码(不推荐)。访问凭证获取步骤:
登录命令:
# 格式:docker login [ACR仓库地址前缀] -u [阿里云账号ID] -p [访问凭证密码]
docker login registry.cn-hangzhou.aliyuncs.com -u jam123456 -p Jam@123456
# 推送打标签后的镜像
docker push registry.cn-hangzhou.aliyuncs.com/jam123456/demo-app:1.0.0
# 推送成功后,在ACR仓库详情页的“镜像版本”中可查看
# 先登录(私有仓库必须登录,公有仓库无需登录)
docker login registry.cn-hangzhou.aliyuncs.com -u jam123456 -p Jam@123456
# 拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/jam123456/demo-app:1.0.0
Harbor是企业内部自建私有仓库的首选,支持权限控制、镜像扫描、日志审计等企业级功能。以下实战基于Harbor最新稳定版(2.11.0),采用Docker Compose部署(需提前安装Docker和Docker Compose)。
# 下载Docker Compose(最新稳定版)
curl -L "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 赋予执行权限
chmod +x /usr/local/bin/docker-compose
# 验证安装
docker-compose --version
# 输出示例:docker-compose version v2.26.1, build 5db8d86f
# 下载Harbor离线安装包(推荐,无需在线依赖)
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
# 解压安装包
tar -zxvf harbor-offline-installer-v2.11.0.tgz -C /usr/local/
# 进入Harbor目录
cd /usr/local/harbor
# 复制配置模板
cp harbor.yml.tmpl harbor.yml
# 编辑配置文件(核心配置如下,其他配置保持默认)
vim harbor.yml
# 核心配置修改:
# 1. 改为Harbor服务器的IP或域名(如192.168.1.100,不要用localhost)
hostname: 192.168.1.100
# 2. 关闭HTTPS(测试环境可关闭,生产环境建议开启)
https:
port: 443
certificate: /your/certificate/path
private_key: /your/private/key/path
→ 注释掉整个https节点(在每行前加#)
# 3. 设置Harbor管理员密码(默认用户名admin)
harbor_admin_password: Harbor12345
# 4. 设置数据存储路径(默认/data/harbor,建议保留)
data_volume: /data/harbor
# 执行安装脚本(自动加载镜像并启动服务)
./install.sh
# 安装成功提示:Harbor has been installed and started successfully
# 查看Harbor相关容器
docker-compose ps
# 所有容器状态为Up即启动成功
# 格式:docker login [Harbor服务器IP] -u [用户名] -p [密码]
docker login 192.168.1.100 -u admin -p Harbor12345
# 格式:docker tag [本地镜像名:标签] [Harbor服务器IP]/[项目名称]/[镜像名:标签]
docker tag demo-app:1.0.0 192.168.1.100/demo-project/demo-app:1.0.0
docker push 192.168.1.100/demo-project/demo-app:1.0.0
# 推送成功后,在Harbor网页端→demo-project→镜像列表中可查看
docker login 192.168.1.100 -u admin -p Harbor12345
docker pull 192.168.1.100/demo-project/demo-app:1.0.0
# 进入Harbor安装目录
cd /usr/local/harbor
# 启动Harbor
docker-compose start
# 停止Harbor
docker-compose stop
# 重启Harbor
docker-compose restart
# 停止并删除Harbor容器(保留数据)
docker-compose down
# 停止并删除Harbor容器和数据(谨慎使用)
docker-compose down -v
混乱的标签会导致镜像管理混乱,企业级推荐采用“语义化版本+环境标识”的标签规范:
[主版本号].[次版本号].[修订号]-[环境标识]1.0.0-dev:开发环境版本1.0.0-test:测试环境版本1.0.0-prod:生产环境版本1.0.0:生产环境正式版本(无环境标识默认生产)latest标签(默认标签):latest标签会自动指向最新推送的镜像,容易导致不同环境部署版本不一致,生产环境必须使用固定版本标签。ecommerce-project,支付项目创建payment-project。docker image prune -a)。/data/harbor),避免仓库故障导致镜像丢失。tar -zcvf harbor-backup-$(date +%Y%m%d).tar.gz /data/harbor企业级仓库必须开启镜像安全扫描,避免推送带有漏洞的镜像到生产环境:
国内访问Docker Hub速度慢,推荐配置镜像加速器:
# 编辑Docker配置文件
vim /etc/docker/daemon.json
# 添加以下内容(替换为自己的加速地址)
{
"registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"]
}
# 重启Docker服务
systemctl daemon-reload
systemctl restart docker
# 验证配置是否生效
docker info | grep "Registry Mirrors"
# 输出包含配置的加速地址即生效
结合前面讲解的镜像、容器、仓库,梳理企业级开发部署的完整流程:

docker build -t demo-app:1.0.0-dev .。docker login 192.168.1.100 -u admin -p Harbor12345。docker tag demo-app:1.0.0-dev 192.168.1.100/demo-project/demo-app:1.0.0-dev。docker push 192.168.1.100/demo-project/demo-app:1.0.0-dev。docker pull 192.168.1.100/demo-project/demo-app:1.0.0-dev。docker run -d \
--name mysql8-test \
--network demo-network \
-p 3306:3306 \
-v mysql-data-test:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=Test@123456 \
-e MYSQL_DATABASE=demo_db \
--restart always \
mysql:8.0.36
docker run -d \
--name demo-app-test \
--network demo-network \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8-test:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=Test@123456 \
--restart always \
192.168.1.100/demo-project/demo-app:1.0.0-dev
docker build -t demo-app:1.0.0-prod .。docker tag demo-app:1.0.0-prod 192.168.1.100/demo-project/demo-app:1.0.0-prod
docker push 192.168.1.100/demo-project/demo-app:1.0.0-prod
docker pull 192.168.1.100/demo-project/demo-app:1.0.0-prod。docker run -d \
--name mysql8-prod \
--network demo-network \
-p 3306:3306 \
-v mysql-data-prod:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=Prod@123456 \
-e MYSQL_DATABASE=demo_db \
--memory=4g \
--cpus=2 \
--restart always \
mysql:8.0.36 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
docker run -d \
--name demo-app-prod \
--network demo-network \
-p 8080:8080 \
-v demo-app-logs:/app/logs \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql8-prod:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=Prod@123456 \
-e SPRING_PROFILES_ACTIVE=prod \
--memory=2g \
--cpus=1 \
--restart always \
192.168.1.100/demo-project/demo-app:1.0.0-prod
docker logs -f demo-app-prod)或第三方监控工具(如Prometheus+Grafana)监控应用运行状态。# 在构建阶段的FROM指令后添加以下内容
RUN mkdir -p /root/.m2 \
&& echo "<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd\"> \
<mirrors> \
<mirror> \
<id>aliyunmaven</id> \
<name>阿里云中央仓库</name> \
<url>https://maven.aliyun.com/repository/public</url> \
<mirrorOf>central</mirrorOf> \
</mirror> \
</mirrors> \
</settings>" > /root/.m2/settings.xml
netstat -tuln | grep 8080(8080为占用端口)。-p 8081:8080(将宿主机8081端口映射到容器8080端口)。docker stop 容器ID或kill -9 进程ID。docker network inspect 网络名称。systemctl restart docker。firewall-cmd --permanent --add-port=8080/tcp(CentOS)。docker volume create),便于管理。用户名/仓库名:标签)。docker login 仓库地址。docker login 仓库地址 -u 用户名 -p 密码。Docker的镜像、仓库、容器是容器化技术的核心三要素,三者相辅相成:
掌握三者的底层逻辑(如镜像的分层结构、容器的隔离技术、仓库的存储机制),并结合企业级实战技巧(如镜像分层构建、容器资源限制、仓库权限管理),才能真正发挥Docker的价值,实现应用的高效开发、快速部署和便捷运维。