第一次听说 Docker 的时候,我以为是某种船的名字。

后来才知道是个装软件的东西。但你上网一搜,全是"容器化"“微服务"“镜像编排"这种词,看得人头大。我当初就是被这些概念劝退了好几次。

直到有一天,我需要在服务器上搭个东西,折腾了俩小时环境配置都没弄好,旁边同事五分钟就搞定了。我问他怎么弄的,他说:“Docker 啊,一条命令的事。”

那一刻我决定认真学一下。

先别管概念,你就把它想象成一个盒子

Docker 是干嘛的?说白了就是把一个软件和它需要的所有东西打包成一个盒子,你拿到任何电脑上都能直接用

以前装软件有多痛苦大家都经历过吧?装个 Python 要配环境变量,装个数据库要改一堆配置文件,换台电脑又得重来一遍。而且最恶心的是,你电脑上装的 A 软件需要 Python 3.8,B 软件需要 Python 3.11,两个版本打架,最后谁也跑不起来。

Docker 解决的就是这个问题。每个盒子(容器)里面自带一套完整的运行环境,互不干扰。你电脑上装的是 Python 3.11 没关系,盒子里面用的是 3.8,各玩各的。

镜像和容器的关系

这里要分清两个概念:镜像容器

镜像就像一个安装光盘,里面打包好了软件和环境。容器就像你用这个光盘装出来的软件实例。一个镜像可以装出很多个容器,就像一张光盘可以装很多台电脑。

你改了容器里的东西不会影响镜像,就像你装完软件后的操作不会改变安装光盘。但反过来,如果你想保存容器里的改动,可以把它"提交"成一个新的镜像。

用一张图来理解:

镜像 (Image)                    容器 (Container)
┌─────────────┐                ┌─────────────┐
│  Ubuntu 基础  │   docker run  │  Ubuntu 基础  │
│  + Python 3.8 │  ─────────→  │  + Python 3.8 │
│  + 你的代码   │               │  + 你的代码   │
│  + 依赖库    │               │  + 依赖库    │
│  (只读模板)   │               │  + 运行状态   │
└─────────────┘                │  (可读可写)   │
                               └─────────────┘
一个镜像可以创建多个容器

Docker 和虚拟机的区别

很多人会问:Docker 和虚拟机有什么区别?不都是隔离环境吗?

虚拟机是模拟一整台电脑,包括操作系统。所以虚拟机很重,启动要几分钟,占内存也多。

Docker 容器只隔离应用层,共用宿主机的操作系统内核。所以容器很轻,启动只要几秒钟,内存占用也小得多。

打个比方:虚拟机就像在一栋楼里隔出独立的房间,每个房间都要装自己的水电。Docker 就像在同一套房子里隔出不同的工位,水电是共用的,但各自的东西互不干扰。

虚拟机                              Docker 容器
┌──────┐ ┌──────┐ ┌──────┐       ┌──────┐ ┌──────┐ ┌──────┐
│ App1 │ │ App2 │ │ App3 │       │ App1 │ │ App2 │ │ App3 │
│      │ │      │ │      │       ├──────┤ ├──────┤ ├──────┤
│ Bins │ │ Bins │ │ Bins │       │ Bins │ │ Bins │ │ Bins │
│      │ │      │ │      │       └──────┘ └──────┘ └──────┘
│  OS  │ │  OS  │ │  OS  │       ┌──────────────────────────┐
├──────┤ ├──────┤ ├──────┤       │     Docker Engine        │
│Hypervisor    │                  │     (共享宿主机内核)      │
├──────────────┤                  ├──────────────────────────┤
│  宿主机 OS    │                  │      宿主机 OS           │
└──────────────┘                  └──────────────────────────┘
虚拟机Docker 容器
启动时间分钟级秒级
内存占用GB 级MB 级
镜像大小GB 级MB 级
隔离级别完全隔离(含内核)进程级隔离(共享内核)
性能有损耗接近原生

装 Docker

说实话装 Docker 这一步是最简单的。

Linux(Ubuntu/Debian)

# 一键安装脚本
curl -fsSL https://get.docker.com | sh

# 把当前用户加到 docker 组(不用每次都 sudo)
sudo usermod -aG docker $USER

然后重新登录一下终端(或者重启),让组权限生效。

CentOS/RHEL/Fedora

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
sudo systemctl enable docker

Windows 和 Mac

Docker 官网 下载 Docker Desktop,装好打开就行。跟装个微信差不多。

Windows 用户注意:需要开启 WSL2(Windows Subsystem for Linux)。Docker Desktop 安装过程中会引导你开启,跟着提示走就行。

验证安装

docker --version
# Docker version 24.x.x, build xxxxx

docker run hello-world

能看到版本号,跑 hello-world 能看到 “Hello from Docker!",就说明装好了。

配置镜像加速(国内用户必做)

国内直接拉 Docker Hub 的镜像很慢,需要配置镜像加速器:

sudo tee /etc/docker/daemon.json << 'EOF'
{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.xuanyuan.me"
  ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

镜像操作:你的软件仓库

搜索镜像

想找什么软件,先搜一下:

docker search nginx

不过说实话,我一般直接去 Docker Hub 网站上搜,能看到详细说明和下载量,比命令行方便。

拉取镜像

下载一个镜像到本地:

docker pull nginx          # 默认拉最新版
docker pull nginx:1.24     # 拉指定版本
docker pull mysql:8.0      # 拉 MySQL 8.0

版本号不写的话默认是 latest。建议在生产环境里明确指定版本,不然哪天自动更新了可能会出问题。

查看本地镜像

docker images

会列出你本地所有镜像,包括仓库名、版本、大小、创建时间等。

删除镜像

docker rmi nginx:latest    # 删除指定镜像
docker image prune         # 清理没用的镜像(悬空镜像)
docker image prune -a      # 清理所有没被容器使用的镜像

容器操作:核心中的核心

运行容器

docker run -d -p 8080:80 --name my-nginx nginx

这条命令拆开看:

  • docker run:创建并启动一个容器
  • -d:后台运行(detach),不占着你的终端
  • -p 8080:80:端口映射,把电脑的 8080 端口映射到容器里的 80 端口
  • --name my-nginx:给容器起个名字,方便后面操作
  • nginx:用哪个镜像

跑完之后打开浏览器访问 http://localhost:8080,你会看到 nginx 的欢迎页面。

查看运行中的容器

docker ps

能看到容器的 ID、用的什么镜像、端口映射、运行了多久等等。

如果想看所有容器(包括已经停了的):

docker ps -a

停止、启动、重启容器

docker stop my-nginx       # 停止
docker start my-nginx      # 启动
docker restart my-nginx    # 重启

删除容器

docker rm my-nginx         # 删除已停止的容器
docker rm -f my-nginx      # 强制删除(即使还在运行)

查看容器日志

出问题了第一件事就是看日志:

docker logs my-nginx           # 查看全部日志
docker logs -f my-nginx        # 实时跟踪日志(类似 tail -f)
docker logs --tail 100 my-nginx  # 只看最后 100 行

进入容器内部

有时候你需要进到容器里面看看:

docker exec -it my-nginx /bin/bash    # 进入容器的 bash
docker exec -it my-nginx sh           # 如果没有 bash,用 sh

进去之后就像登录了一台新的 Linux 一样,可以执行各种命令。输入 exit 退出。

查看容器资源占用

docker stats

实时显示每个容器的 CPU、内存、网络、磁盘使用情况。

数据持久化:Volume

这是新手最容易踩的坑:容器里的数据默认不保存,容器一删数据就没了。

为什么需要 Volume

容器的设计理念是"用完即弃”。你可以随时删除容器再用同一个镜像创建一个新的。但数据库里的数据、用户上传的文件这些不能丢啊。

Volume 就是把容器里的某个目录映射到宿主机上,这样即使容器删了,数据还在宿主机上。

使用 Volume

# 命名卷(Docker 管理)
docker run -d -p 3306:3306 \
  -v mysql_data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=123456 \
  --name my-mysql \
  mysql:8.0

# 绑定挂载(指定宿主机目录)
docker run -d -p 8080:80 \
  -v /home/xyf/website:/usr/share/nginx/html \
  --name my-nginx \
  nginx

命名卷由 Docker 管理,存在 Docker 的数据目录里。绑定挂载是你自己指定宿主机的哪个目录。

管理 Volume

docker volume ls              # 列出所有卷
docker volume inspect my_vol  # 查看卷详情
docker volume rm my_vol       # 删除卷
docker volume prune           # 清理没用的卷

网络:容器之间怎么通信

端口映射

前面已经说了,-p 参数做端口映射:

-p 8080:80       # 宿主机 8080 → 容器 80
-p 80:80         # 宿主机 80 → 容器 80
-p 127.0.0.1:8080:80  # 只允许本机访问

容器间通信

同一个 Docker 网络里的容器可以用容器名互相访问:

# 创建网络
docker network create mynet

# 在同一个网络里启动两个容器
docker run -d --name web --network mynet nginx
docker run -d --name db --network mynet mysql:8.0

这样 web 容器里可以直接用 db 作为主机名访问 MySQL 容器,不需要知道 IP 地址。

Dockerfile:构建自己的镜像

你可能会问,那些镜像是谁做的?大部分是软件官方做的。但有时候你需要把自己的代码打包成镜像。

基本结构

创建一个叫 Dockerfile 的文件(没有后缀名):

# 基础镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY . .

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["python", "app.py"]

构建镜像

docker build -t myapp:1.0 .
  • -t myapp:1.0:给镜像起名字和版本号
  • .:Dockerfile 所在的目录(当前目录)

常用指令

FROM python:3.11      # 基础镜像
WORKDIR /app          # 设置工作目录
COPY . .              # 复制文件到容器里
RUN pip install -r requirements.txt  # 构建时执行的命令
ENV APP_ENV=production  # 设置环境变量
EXPOSE 8000           # 声明端口(文档作用,不实际开放)
CMD ["python", "app.py"]  # 容器启动时执行的命令
ENTRYPOINT ["python"]     # 入口点,CMD 作为参数追加

.dockerignore 文件

.gitignore 类似,告诉 Docker 哪些文件不要复制到镜像里:

.git
node_modules
__pycache__
*.pyc
.env

多阶段构建:让镜像更小

# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

# 运行阶段
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]

这样最终镜像只有几 MB,而不是几百 MB。

Docker Compose:多容器编排

单个容器跑起来不难,但实际用的时候一般需要好几个服务配合。比如搭个网站可能需要:Web 服务器 + 数据库 + 缓存 + 后端 API。

一个一个手动启动太麻烦了,Docker Compose 就是干这个的——用一个配置文件把所有容器的启动方式写好,一条命令全部启动

安装

Docker Desktop 自带 Docker Compose。Linux 上需要单独安装:

sudo apt install docker-compose-plugin

验证:

docker compose version

配置文件

创建一个 docker-compose.yml 文件:

services:
  web:
    image: nginx:1.24
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=mysql://root:123456@db:3306/myapp
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

常用命令

# 启动所有服务(后台运行)
docker compose up -d

# 查看状态
docker compose ps

# 查看日志
docker compose logs -f

# 停止所有服务
docker compose down

# 停止并删除卷(数据会丢!)
docker compose down -v

# 重新构建并启动
docker compose up -d --build

# 进入某个容器
docker compose exec api bash

实际案例:一键搭建 WordPress

services:
  wordpress:
    image: wordpress:latest
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: 123456
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: wordpress
    volumes:
      - db_data:/var/lib/mysql

volumes:
  wp_data:
  db_data:

docker compose up -d,打开 http://localhost:8080,就能看到 WordPress 的安装界面了。

常见问题和踩坑记录

1. 容器启动就退出了

docker ps -a    # 看看退出码
docker logs 容器名  # 看日志找原因

最常见的原因是主进程结束了。Docker 容器的生命和主进程绑定,主进程一结束容器就停了。确保你的应用在前台运行,不要用 & 后台执行。

2. 端口被占用

Error: bind: address already in use

说明宿主机上已经有东西占了这个端口。换个端口映射就行,或者先停掉占用端口的服务。

3. 容器内无法访问外网

可能是 DNS 问题。试试:

docker run --dns 223.5.5.5 alpine ping google.com

4. 镜像太大

用多阶段构建减小镜像体积(见上面 Dockerfile 章节)。

5. 数据丢失

再说一遍:容器删了数据就没了。 重要的数据一定要用 Volume 挂载出来。

6. 时区问题

容器默认是 UTC 时区。设置中国时区:

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime

或者在 docker-compose.yml 里:

environment:
  - TZ=Asia/Shanghai

7. 权限问题

挂载的目录可能会有权限问题:

# 查看容器内用户
docker exec -it my-nginx id

# 修改挂载目录权限
chmod -R 755 /home/xyf/website

一些实用技巧

清理空间

Docker 用久了会占用很多磁盘空间:

docker system df            # 查看占用
docker system prune         # 清理停止的容器、没用的网络、悬空镜像
docker system prune -a      # 清理所有没被使用的镜像(慎用)

导出和导入镜像

没有网络的环境下很有用:

docker save myapp:1.0 > myapp.tar    # 导出
docker load < myapp.tar              # 导入

限制容器资源

docker run -d --memory=512m --cpus=1.5 nginx

限制容器最多用 512MB 内存和 1.5 个 CPU 核心。在共享服务器上很有用,防止某个容器把资源吃光。

查看容器的详细信息

docker inspect my-nginx

返回 JSON 格式的详细信息,包括网络配置、挂载点、环境变量等。

最后

Docker 这东西,入门真的不难。你不需要把所有概念都搞懂才能用,先跑起来,遇到问题再查。

我现在的习惯是,能用 Docker 跑的东西尽量用 Docker。换电脑、换服务器的时候直接把 docker-compose.yml 一拷,环境就全好了,再也不用从零开始配环境了。

如果你也被环境配置折磨过,试试 Docker 吧,真的能省不少事。


写给和我一样被环境配置搞崩溃过的朋友们。