Docker容器化入门教程

教程简介

零基础Docker容器化完整教程,涵盖Docker概念、镜像操作、容器操作、Dockerfile编写、数据卷、网络配置、Docker Compose等核心技能,配有完整Todo应用实战部署项目,适合DevOps初学者。

Docker 容器化入门教程

🐳 从零开始,一步步掌握 Docker 容器化技术


目录

  1. 什么是 Docker?
  2. 核心概念
  3. 安装 Docker
  4. 镜像操作
  5. 容器操作
  6. 编写 Dockerfile
  7. 数据卷与持久化
  8. Docker 网络
  9. Docker Compose
  10. 实战项目:部署一个完整的 Web 应用
  11. 常用命令速查表
  12. 常见问题与排错

1. 什么是 Docker?

1.1 一个生活中的比喻

想象你要搬家。传统方式是把所有家具一件件搬走,到了新家还要重新布置、接水电、调网络。而 Docker 就像是把整个房间连同家具、装修、水电网络一起"打包"搬走——到了新地方,直接"拆箱入住",一切和原来一模一样。

1.2 技术定义

Docker 是一个开源的容器化平台,它允许你将应用程序及其所有依赖(库、配置文件、运行时环境等)打包到一个标准化的单元——容器中,然后在任何支持 Docker 的机器上运行。

1.3 Docker vs 虚拟机

特性 Docker 容器 虚拟机
启动速度 秒级 分钟级
资源占用 极少(共享宿主内核) 较多(独立操作系统)
隔离级别 进程级隔离 完全隔离
镜像大小 MB 级别 GB 级别
性能 接近原生 有一定损耗

简单理解:虚拟机是在一台电脑里"模拟"出多台电脑;Docker 是在一台电脑里"隔离"出多个独立的工作空间。

1.4 为什么要学 Docker?

  • 环境一致性:"在我电脑上能跑"不再是借口,开发、测试、生产环境完全一致
  • 快速部署:一条命令启动一个完整的应用环境
  • 资源高效:比虚拟机轻量得多,一台服务器可以跑更多应用
  • 版本控制:镜像可以版本化管理,随时回滚
  • 微服务友好:每个服务一个容器,独立部署、独立扩展

2. 核心概念

在正式操作之前,先理解四个核心概念:

2.1 镜像(Image)

镜像是一个只读模板,包含了运行应用所需的一切:代码、运行时、库、环境变量、配置文件。

你可以把镜像理解为"安装光盘"——它包含了软件的完整安装包,但还没有安装到电脑上。

镜像采用分层存储,每一层只记录与上一层的差异,多个镜像可以共享相同的基础层,节省存储空间。

2.2 容器(Container)

容器是镜像的运行实例。如果镜像是"安装光盘",那容器就是"安装好的、正在运行的程序"。

  • 一个镜像可以创建多个容器(就像一张光盘可以安装到多台电脑)
  • 容器之间相互隔离,互不影响
  • 容器可以启动、停止、删除

2.3 仓库(Repository)

仓库是用来存储和分发镜像的服务,类似于代码的 GitHub。

  • Docker Hub(hub.docker.com)是最大的公共仓库,包含数十万个镜像
  • 企业通常使用私有仓库(如 Harbor)来存储内部镜像

2.4 Dockerfile

Dockerfile 是一个文本文件,包含了一系列构建镜像的指令。就像一份"菜谱",告诉 Docker 如何一步步把代码变成可运行的镜像。

2.5 它们的关系

Dockerfile  ──(docker build)──>  镜像(Image)  ──(docker run)──>  容器(Container)
                                      │
                                      ▼
                                  仓库(Registry)
                                      │
                                      ▼
                              docker pull / docker push

3. 安装 Docker

3.1 系统要求

  • Linux:Ubuntu 20.04+、Debian 11+、CentOS 7+、Fedora 35+ 等主流发行版
  • macOS:macOS 10.15 (Catalina) 或更高版本
  • Windows:Windows 10/11(需要启用 WSL2 或 Hyper-V)

3.2 在 Ubuntu/Debian 上安装

# 1. 更新包索引
sudo apt update

# 2. 安装必要的依赖
sudo apt install -y ca-certificates curl gnupg

# 3. 添加 Docker 官方 GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 4. 添加 Docker 软件源
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 5. 安装 Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 6. 将当前用户添加到 docker 组(免 sudo)
sudo usermod -aG docker $USER

# 7. 重新登录或执行以下命令使组权限生效
newgrp docker

3.3 在 CentOS/RHEL 上安装

# 1. 安装必要的依赖
sudo yum install -y yum-utils

# 2. 添加 Docker 软件源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 3. 安装 Docker Engine
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 4. 启动 Docker 并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker

# 5. 将当前用户添加到 docker 组
sudo usermod -aG docker $USER
newgrp docker

3.4 macOS / Windows

macOS 和 Windows 用户请下载安装 Docker Desktop,它提供了图形化界面,安装过程非常简单,一路"下一步"即可。

3.5 验证安装

# 查看 Docker 版本
docker --version

输出示例:

Docker version 24.0.7, build afdd53b
# 运行测试容器
docker run hello-world

输出示例:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete
Digest: sha256:...
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

看到 Hello from Docker! 就说明安装成功了!

# 查看 Docker 系统信息
docker info

输出示例(节选):

Client: Docker Engine - Community
 Version:    24.0.7

Server: Docker Engine - Community
 Containers: 1
  Running: 0
  Paused: 0
  Stopped: 1
 Images: 1
 Server Version: 24.0.7
 Storage Driver: overlay2
 ...

4. 镜像操作

4.1 搜索镜像

# 从 Docker Hub 搜索镜像
docker search nginx

输出示例:

NAME                       DESCRIPTION                      STARS   OFFICIAL
nginx                      Official build of Nginx.         18000   [OK]
bitnami/nginx              Bitnami nginx Docker Image       150
nginxinc/nginx-unprivileged   Unprivileged NGINX Dockerfiles  100
...

4.2 拉取镜像

# 拉取最新版本的 nginx 镜像
docker pull nginx

输出示例:

Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
186b1aaa4aa6: Pull complete
b4df32aa5a72: Pull complete
a0bcbecc962e: Pull complete
Digest: sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
# 拉取指定版本
docker pull nginx:1.24

# 拉取 Python 镜像
docker pull python:3.11-slim

# 拉取 MySQL 镜像
docker pull mysql:8.0

4.3 查看本地镜像

docker images

输出示例:

REPOSITORY   TAG          IMAGE ID       CREATED        SIZE
nginx        latest       605c77e624dd   2 weeks ago    141MB
nginx        1.24         2abcd3f0e8e1   3 weeks ago    142MB
python       3.11-slim    a3dc1a2c02b1   1 month ago    130MB
mysql        8.0          3218b38490ce   2 weeks ago    516MB
hello-world  latest       d2c94e258dcb   6 months ago   13.3kB
# 只显示镜像 ID
docker images -q

# 显示包括中间层在内的所有镜像
docker images -a

4.4 给镜像打标签

# 标签类似于"别名",常用于推送到私有仓库
docker tag nginx:latest myregistry.com/myapp/nginx:v1.0

4.5 删除镜像

# 删除指定镜像
docker rmi nginx:1.24

输出示例:

Untagged: nginx:1.24
Untagged: nginx@sha256:...
Deleted: sha256:2abcd3f0e8e1...
# 强制删除(即使有容器在使用)
docker rmi -f nginx:latest

# 删除所有未使用的镜像(清理空间)
docker image prune

# 删除所有镜像(慎用!)
docker rmi $(docker images -q)

5. 容器操作

5.1 创建并启动容器

# 最基本的启动方式
docker run nginx

这会在前台运行 nginx,按 Ctrl+C 停止。

# 后台运行(-d = detached)
docker run -d nginx

输出示例:

a3f5b7c9d1e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f2a4

这个长串是容器的 ID。

# 后台运行,并给容器起个名字
docker run -d --name my-nginx nginx
# 端口映射(-p 宿主机端口:容器端口)
docker run -d --name web -p 8080:80 nginx

现在访问 http://localhost:8080 就能看到 nginx 的欢迎页面了!

# 交互式运行(-it = interactive + tty)
docker run -it ubuntu:22.04 /bin/bash

你会进入容器的 bash 终端,就像登录了一台新机器:

root@a1b2c3d4e5f6:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@a1b2c3d4e5f6:/# cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
...
root@a1b2c3d4e5f6:/# exit
# 运行完毕自动删除容器(--rm)
docker run --rm ubuntu:22.04 cat /etc/os-release

输出示例:

PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
...

5.2 查看容器

# 查看正在运行的容器
docker ps

输出示例:

CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                  NAMES
a3f5b7c9d1e2   nginx     "/docker-entrypoint.…"   5 minutes ago    Up 5 minutes    0.0.0.0:8080->80/tcp   web
d4e6f8a0b2c4   nginx     "/docker-entrypoint.…"   10 minutes ago   Up 10 minutes   80/tcp                 my-nginx
# 查看所有容器(包括已停止的)
docker ps -a

# 只显示容器 ID
docker ps -q

# 显示容器大小
docker ps -s

5.3 停止、启动、重启容器

# 停止容器
docker stop web

# 启动已停止的容器
docker start web

# 重启容器
docker restart web

# 强制停止(类似拔电源)
docker kill web

5.4 进入运行中的容器

# 进入容器(交互式)
docker exec -it my-nginx /bin/bash
root@a3f5b7c9d1e2:/# ls /usr/share/nginx/html/
50x.html  index.html
root@a3f5b7c9d1e2:/# exit
# 在容器中执行单条命令(不进入)
docker exec my-nginx cat /etc/nginx/nginx.conf

5.5 查看容器日志

# 查看日志
docker logs web

输出示例:

172.17.0.1 - - [28/May/2026:00:30:00 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0" "-"
172.17.0.1 - - [28/May/2026:00:30:01 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "-" "Mozilla/5.0" "-"
# 实时跟踪日志(类似 tail -f)
docker logs -f web

# 显示最后 100 行
docker logs --tail 100 web

# 显示时间戳
docker logs -t web

5.6 容器与宿主机之间的文件复制

# 从宿主机复制到容器
echo "<h1>Hello from Docker!</h1>" > /tmp/index.html
docker cp /tmp/index.html web:/usr/share/nginx/html/index.html

# 从容器复制到宿主机
docker cp web:/etc/nginx/nginx.conf ./nginx.conf

5.7 查看容器详细信息

# 查看容器完整信息(JSON 格式)
docker inspect web

# 只查看 IP 地址
docker inspect --format='{{.NetworkSettings.IPAddress}}' web

输出示例:

172.17.0.2

5.8 删除容器

# 删除已停止的容器
docker rm my-nginx

# 强制删除运行中的容器
docker rm -f web

# 删除所有已停止的容器
docker container prune

6. 编写 Dockerfile

6.1 Dockerfile 基础指令

Dockerfile 是构建镜像的"配方",由一系列指令组成。每条指令都会创建一个新的镜像层

指令 作用 示例
FROM 指定基础镜像 FROM python:3.11-slim
WORKDIR 设置工作目录 WORKDIR /app
COPY 复制文件到镜像 COPY . .
ADD 复制文件(支持URL和解压) ADD app.tar.gz /app/
RUN 执行命令 RUN pip install -r requirements.txt
ENV 设置环境变量 ENV PORT=8000
EXPOSE 声明容器端口 EXPOSE 8000
CMD 容器启动时的默认命令 CMD ["python", "app.py"]
ENTRYPOINT 容器的入口点 ENTRYPOINT ["python"]
ARG 构建时的变量 ARG VERSION=1.0

6.2 编写一个 Python 应用的 Dockerfile

让我们来构建一个简单的 Python Web 应用。

项目结构

my-python-app/
├── app.py
├── requirements.txt
└── Dockerfile

app.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return '<h1>🐳 Hello from Docker!</h1><p>This app is running inside a Docker container.</p>'

@app.route('/health')
def health():
    return {'status': 'ok'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

requirements.txt

flask==3.0.0

Dockerfile

# 1. 使用 Python 3.11 精简版作为基础镜像
FROM python:3.11-slim

# 2. 设置元数据(可选但推荐)
LABEL maintainer="your-email@example.com"
LABEL description="A simple Flask web application"

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

# 4. 先复制依赖文件(利用 Docker 缓存机制)
COPY requirements.txt .

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

# 6. 复制应用代码
COPY . .

# 7. 声明容器使用的端口
EXPOSE 5000

# 8. 设置环境变量
ENV FLASK_APP=app.py

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

6.3 构建镜像

# 在 Dockerfile 所在目录执行
docker build -t my-python-app:v1.0 .

输出示例:

[+] Building 25.3s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                     0.0s
 => [internal] load .dockerignore                                        0.0s
 => [internal] load metadata for docker.io/library/python:3.11-slim      1.2s
 => [1/5] FROM docker.io/library/python:3.11-slim@sha256:...             5.3s
 => [2/5] WORKDIR /app                                                   0.1s
 => [3/5] COPY requirements.txt .                                        0.0s
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt            15.7s
 => [5/5] COPY . .                                                       0.0s
 => exporting to image                                                    2.8s
 => => naming to docker.io/library/my-python-app:v1.0                    0.0s
# 运行构建好的镜像
docker run -d --name python-app -p 5000:5000 my-python-app:v1.0

访问 http://localhost:5000 即可看到效果。

6.4 .dockerignore 文件

就像 .gitignore 一样,.dockerignore 可以排除不需要复制到镜像中的文件:

.git
.gitignore
__pycache__
*.pyc
.env
node_modules
.DS_Store
*.md

6.5 多阶段构建(进阶)

多阶段构建可以显著减小镜像体积:

# ---- 第一阶段:构建 ----
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

# ---- 第二阶段:运行 ----
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

第一阶段编译代码,第二阶段只复制编译好的二进制文件到一个精简镜像中。最终镜像可能只有几十 MB,而不是几百 MB。


7. 数据卷与持久化

容器是临时的——容器删除后,里面的数据就没了。数据卷(Volume) 解决了这个问题,它让数据持久化存储在宿主机上。

7.1 数据卷的三种方式

方式一:命名数据卷(推荐)

# 创建数据卷
docker volume create my-data

输出示例:

my-data
# 查看所有数据卷
docker volume ls

输出示例:

DRIVER    VOLUME NAME
local     my-data
# 使用数据卷启动容器
docker run -d --name mysql-test \
  -v my-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=123456 \
  mysql:8.0

数据会保存在 my-data 卷中,即使容器删除,数据依然存在。新建容器挂载同一个卷,数据就回来了。

# 查看数据卷详情
docker volume inspect my-data

输出示例:

[
    {
        "CreatedAt": "2026-05-28T08:30:00+08:00",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/my-data/_data",
        "Name": "my-data",
        "Options": {},
        "Scope": "local"
    }
]
# 删除数据卷
docker volume rm my-data

# 删除所有未使用的数据卷(慎用!)
docker volume prune

方式二:绑定挂载(Bind Mount)

将宿主机的目录直接挂载到容器中:

# 将当前目录挂载到容器的 /app
docker run -d --name dev-app \
  -v $(pwd):/app \
  -p 5000:5000 \
  my-python-app:v1.0

这样你在宿主机上修改代码,容器里会立即生效,非常适合开发环境。

方式三:tmpfs 挂载

数据存储在内存中,容器停止后数据消失,适合临时数据:

docker run -d --name tmp-app \
  --tmpfs /app/temp \
  nginx

7.2 数据卷实际操作演示

# 1. 创建一个命名卷
docker volume create web-content

# 2. 启动 nginx 并挂载卷
docker run -d --name nginx-vol \
  -v web-content:/usr/share/nginx/html \
  -p 8081:80 \
  nginx

# 3. 在卷中创建文件
docker exec nginx-vol bash -c 'echo "<h1>持久化内容</h1>" > /usr/share/nginx/html/index.html'

# 4. 验证
curl http://localhost:8081

输出:

<h1>持久化内容</h1>
# 5. 删除容器
docker rm -f nginx-vol

# 6. 用同一个卷启动新容器
docker run -d --name nginx-vol2 \
  -v web-content:/usr/share/nginx/html \
  -p 8081:80 \
  nginx

# 7. 数据还在!
curl http://localhost:8081

输出:

<h1>持久化内容</h1>

8. Docker 网络

8.1 网络类型

Docker 提供了几种内置的网络驱动:

网络类型 说明
bridge 默认网络,容器之间通过虚拟网桥通信
host 容器直接使用宿主机网络(无隔离)
none 无网络,容器完全断网
overlay 跨主机通信,用于 Docker Swarm
macvlan 容器拥有独立的 MAC 地址

8.2 查看网络

docker network ls

输出示例:

NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   bridge    bridge    local
f6e5d4c3b2a1   host      host      local
1a2b3c4d5e6f   none      null      local

8.3 创建自定义网络

# 创建自定义桥接网络
docker network create my-network

输出示例:

b7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8
# 创建指定子网的网络
docker network create --subnet=172.20.0.0/16 --gateway=172.20.0.1 custom-net

8.4 容器间通信

同一个自定义网络中的容器,可以直接通过容器名互相访问:

# 1. 创建网络
docker network create app-net

# 2. 启动一个 Redis 容器
docker run -d --name redis --network app-net redis:7

# 3. 启动一个应用容器
docker run -d --name myapp --network app-net my-python-app:v1.0

# 4. 在 myapp 容器中可以直接通过 "redis" 这个名字访问 Redis
docker exec myapp python -c "import socket; print(socket.getaddrinfo('redis', 6379))"

输出示例:

[(<AddressFamily.AF_INET: 2>, <SocketType.SOCK_STREAM: 1>, 6, '', ('172.20.0.2', 6379))]

8.5 将已有容器连接到网络

# 将容器连接到指定网络
docker network connect my-network myapp

# 断开网络连接
docker network disconnect my-network myapp

8.6 查看网络详情

docker network inspect app-net

输出(节选):

[
    {
        "Name": "app-net",
        "Driver": "bridge",
        "IPAM": {
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                }
            ]
        },
        "Containers": {
            "abc123...": {
                "Name": "redis",
                "IPv4Address": "172.20.0.2/16"
            },
            "def456...": {
                "Name": "myapp",
                "IPv4Address": "172.20.0.3/16"
            }
        }
    }
]

9. Docker Compose

当你的应用由多个服务组成(比如 Web 应用 + 数据库 + 缓存),一个个手动 docker run 就太麻烦了。Docker Compose 允许你用一个 YAML 文件定义和管理多容器应用。

9.1 docker-compose.yml 基础结构

version: '3.8'

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

  api:
    build: ./api
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    depends_on:
      - db

  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb

volumes:
  db-data:

9.2 Docker Compose 常用命令

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

输出示例:

[+] Running 4/4
 ✔ Network myapp_default    Created                             0.1s
 ✔ Container myapp-db-1     Started                             0.5s
 ✔ Container myapp-api-1    Started                             0.8s
 ✔ Container myapp-web-1    Started                             1.0s
# 查看服务状态
docker compose ps

输出示例:

NAME             IMAGE          COMMAND                  SERVICE   CREATED          STATUS          PORTS
myapp-api-1      myapp-api      "python app.py"          api       30 seconds ago   Up 28 seconds   0.0.0.0:5000->5000/tcp
myapp-db-1       postgres:15    "docker-entrypoint.s…"   db        30 seconds ago   Up 29 seconds   5432/tcp
myapp-web-1      nginx:latest   "/docker-entrypoint.…"   web       28 seconds ago   Up 27 seconds   0.0.0.0:80->80/tcp
# 查看日志
docker compose logs

# 实时跟踪所有服务日志
docker compose logs -f

# 只看某个服务的日志
docker compose logs -f api

# 停止所有服务
docker compose stop

# 停止并删除容器、网络
docker compose down

# 停止并删除容器、网络、数据卷
docker compose down -v

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

# 只重启某个服务
docker compose restart api

# 进入某个服务的容器
docker compose exec api /bin/bash

# 查看某个服务的容器日志
docker compose logs --tail=50 db

9.3 Compose 文件详解

version: '3.8'

services:
  app:
    # 使用的镜像(二选一)
    image: nginx:latest
    # 或者从 Dockerfile 构建
    # build:
    #   context: .
    #   dockerfile: Dockerfile
    #   args:
    #     - VERSION=1.0

    # 容器名称
    container_name: my-app

    # 端口映射
    ports:
      - "8080:80"        # 宿主机:容器
      - "443:443"

    # 数据卷
    volumes:
      - ./html:/usr/share/nginx/html    # 绑定挂载
      - db-data:/var/lib/data            # 命名卷

    # 环境变量
    environment:
      - NODE_ENV=production
      - DB_HOST=db
    # 或者从文件读取
    # env_file:
    #   - .env

    # 依赖关系
    depends_on:
      - db
      - redis

    # 重启策略
    restart: unless-stopped

    # 网络
    networks:
      - frontend
      - backend

    # 资源限制
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

    # 健康检查
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    networks:
      - backend

  redis:
    image: redis:7-alpine
    networks:
      - backend

# 数据卷定义
volumes:
  db-data:

# 网络定义
networks:
  frontend:
  backend:

10. 实战项目:部署一个完整的 Web 应用

让我们用一个完整的小项目来巩固所学知识。我们将部署一个待办事项(Todo)应用,包含前端、后端 API 和数据库。

10.1 项目结构

todo-app/
├── frontend/
│   ├── Dockerfile
│   └── index.html
├── backend/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── app.py
├── docker-compose.yml
└── .dockerignore

10.2 后端代码

backend/app.py

from flask import Flask, request, jsonify
from flask_cors import CORS
import psycopg2
import os

app = Flask(__name__)
CORS(app)

def get_db():
    return psycopg2.connect(
        host=os.environ.get('DB_HOST', 'db'),
        database=os.environ.get('DB_NAME', 'todos'),
        user=os.environ.get('DB_USER', 'postgres'),
        password=os.environ.get('DB_PASSWORD', 'postgres')
    )

def init_db():
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        CREATE TABLE IF NOT EXISTS todos (
            id SERIAL PRIMARY KEY,
            title VARCHAR(200) NOT NULL,
            completed BOOLEAN DEFAULT FALSE
        )
    ''')
    conn.commit()
    cur.close()
    conn.close()

@app.route('/api/todos', methods=['GET'])
def get_todos():
    conn = get_db()
    cur = conn.cursor()
    cur.execute('SELECT id, title, completed FROM todos ORDER BY id')
    todos = [{'id': row[0], 'title': row[1], 'completed': row[2]} for row in cur.fetchall()]
    cur.close()
    conn.close()
    return jsonify(todos)

@app.route('/api/todos', methods=['POST'])
def add_todo():
    data = request.get_json()
    conn = get_db()
    cur = conn.cursor()
    cur.execute('INSERT INTO todos (title) VALUES (%s) RETURNING id, title, completed',
                (data['title'],))
    todo = cur.fetchone()
    conn.commit()
    cur.close()
    conn.close()
    return jsonify({'id': todo[0], 'title': todo[1], 'completed': todo[2]}), 201

@app.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute('DELETE FROM todos WHERE id = %s', (todo_id,))
    conn.commit()
    cur.close()
    conn.close()
    return '', 204

@app.route('/api/todos/<int:todo_id>/toggle', methods=['PUT'])
def toggle_todo(todo_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute('UPDATE todos SET completed = NOT completed WHERE id = %s RETURNING id, title, completed',
                (todo_id,))
    todo = cur.fetchone()
    conn.commit()
    cur.close()
    conn.close()
    return jsonify({'id': todo[0], 'title': todo[1], 'completed': todo[2]})

@app.route('/api/health')
def health():
    return {'status': 'ok'}

if __name__ == '__main__':
    init_db()
    app.run(host='0.0.0.0', port=5000)

backend/requirements.txt

flask==3.0.0
flask-cors==4.0.0
psycopg2-binary==2.9.9

backend/Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

10.3 前端代码

frontend/index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Docker Todo App</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, sans-serif; background: #f0f2f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 20px; }
        .input-group { display: flex; gap: 10px; margin-bottom: 20px; }
        input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; }
        button { padding: 12px 24px; background: #1a73e8; color: white; border: none; border-radius: 8px;
                 cursor: pointer; font-size: 16px; }
        button:hover { background: #1557b0; }
        .todo-item { display: flex; align-items: center; padding: 12px; background: white;
                     border-radius: 8px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
        .todo-item.done { opacity: 0.6; text-decoration: line-through; }
        .todo-item span { flex: 1; margin-left: 10px; }
        .delete-btn { background: #e74c3c; padding: 6px 12px; font-size: 14px; }
        .footer { text-align: center; margin-top: 20px; color: #888; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🐳 Docker Todo App</h1>
        <div class="input-group">
            <input type="text" id="todoInput" placeholder="添加新的待办事项..." />
            <button onclick="addTodo()">添加</button>
        </div>
        <div id="todoList"></div>
        <div class="footer">Powered by Docker Compose</div>
    </div>
    <script>
        const API = '/api/todos';
        async function loadTodos() {
            const res = await fetch(API);
            const todos = await res.json();
            document.getElementById('todoList').innerHTML = todos.map(t =>
                `<div class="todo-item ${t.completed ? 'done' : ''}">
                    <input type="checkbox" ${t.completed ? 'checked' : ''} onchange="toggleTodo(${t.id})" />
                    <span>${t.title}</span>
                    <button class="delete-btn" onclick="deleteTodo(${t.id})">删除</button>
                </div>`
            ).join('');
        }
        async function addTodo() {
            const input = document.getElementById('todoInput');
            if (!input.value.trim()) return;
            await fetch(API, { method: 'POST', headers: {'Content-Type': 'application/json'},
                               body: JSON.stringify({title: input.value}) });
            input.value = '';
            loadTodos();
        }
        async function toggleTodo(id) {
            await fetch(`${API}/${id}/toggle`, { method: 'PUT' });
            loadTodos();
        }
        async function deleteTodo(id) {
            await fetch(`${API}/${id}`, { method: 'DELETE' });
            loadTodos();
        }
        document.getElementById('todoInput').addEventListener('keypress', e => { if(e.key==='Enter') addTodo(); });
        loadTodos();
    </script>
</body>
</html>

frontend/Dockerfile

FROM nginx:alpine

COPY index.html /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

frontend/nginx.conf

server {
    listen 80;
    server_name localhost;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://backend:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

10.4 Docker Compose 编排

docker-compose.yml

version: '3.8'

services:
  # ---- 前端 ----
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - app-net
    restart: unless-stopped

  # ---- 后端 API ----
  backend:
    build: ./backend
    environment:
      - DB_HOST=db
      - DB_NAME=todos
      - DB_USER=postgres
      - DB_PASSWORD=postgres123
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-net
    restart: unless-stopped

  # ---- 数据库 ----
  db:
    image: postgres:15-alpine
    volumes:
      - pg-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=todos
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres123
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-net
    restart: unless-stopped

volumes:
  pg-data:

networks:
  app-net:
    driver: bridge

10.5 一键部署!

# 1. 创建项目目录并进入
mkdir todo-app && cd todo-app

# 2. 创建所有上述文件后,一条命令启动整个应用
docker compose up -d --build

输出示例:

[+] Building 35.2s (18/18) FINISHED
[+] Running 4/4
 ✔ Network todo-app_app-net   Created        0.1s
 ✔ Container todo-app-db-1    Healthy        5.3s
 ✔ Container todo-app-backend-1 Started       5.6s
 ✔ Container todo-app-frontend-1 Started      5.9s
# 3. 查看运行状态
docker compose ps

输出示例:

NAME                    IMAGE               SERVICE    STATUS          PORTS
todo-app-backend-1      todo-app-backend     backend    running         5000/tcp
todo-app-db-1           postgres:15-alpine   db         running (healthy) 5432/tcp
todo-app-frontend-1     todo-app-frontend    frontend   running         0.0.0.0:80->80/tcp
# 4. 测试 API
curl http://localhost/api/todos
# 输出: []

curl -X POST http://localhost/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "学习 Docker"}'
# 输出: {"completed":false,"id":1,"title":"学习 Docker"}

curl -X POST http://localhost/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "写一个 Docker Compose 项目"}'
# 输出: {"completed":false,"id":2,"title":"写一个 Docker Compose 项目"}

curl http://localhost/api/todos
# 输出: [{"completed":false,"id":1,"title":"学习 Docker"},{"completed":false,"id":2,"title":"写一个 Docker Compose 项目"}]

现在打开浏览器访问 http://localhost,你就能看到一个完整的待办事项应用了!

10.6 管理应用

# 查看日志
docker compose logs -f

# 重启某个服务
docker compose restart backend

# 停止整个应用
docker compose down

# 停止并删除所有数据(包括数据库)
docker compose down -v

11. 常用命令速查表

镜像操作

命令 说明
docker pull <镜像> 拉取镜像
docker images 列出本地镜像
docker build -t <名称> . 构建镜像
docker rmi <镜像> 删除镜像
docker image prune 清理未使用的镜像
docker tag <源> <目标> 给镜像打标签
docker save -o file.tar <镜像> 导出镜像为文件
docker load -i file.tar 从文件导入镜像

容器操作

命令 说明
docker run -d --name <名称> <镜像> 后台运行容器
docker run -it <镜像> /bin/bash 交互式运行
docker ps 查看运行中的容器
docker ps -a 查看所有容器
docker stop <容器> 停止容器
docker start <容器> 启动容器
docker rm <容器> 删除容器
docker exec -it <容器> /bin/bash 进入容器
docker logs -f <容器> 查看容器日志
docker inspect <容器> 查看容器详情
docker cp <容器>:<路径> <本地路径> 复制文件

数据卷操作

命令 说明
docker volume create <名称> 创建数据卷
docker volume ls 列出数据卷
docker volume inspect <名称> 查看数据卷详情
docker volume rm <名称> 删除数据卷
docker volume prune 清理未使用的数据卷

网络操作

命令 说明
docker network ls 列出网络
docker network create <名称> 创建网络
docker network inspect <名称> 查看网络详情
docker network connect <网络> <容器> 连接容器到网络
docker network disconnect <网络> <容器> 断开网络连接

Docker Compose

命令 说明
docker compose up -d 后台启动所有服务
docker compose down 停止并删除容器和网络
docker compose down -v 同上,同时删除数据卷
docker compose ps 查看服务状态
docker compose logs -f 实时查看日志
docker compose build 构建/重新构建镜像
docker compose restart 重启所有服务
docker compose exec <服务> <命令> 在服务中执行命令

12. 常见问题与排错

Q1: permission denied while trying to connect to the Docker daemon socket

原因:当前用户没有 Docker 权限。

解决

sudo usermod -aG docker $USER
newgrp docker

Q2: port is already allocated

原因:端口被占用。

解决

# 查看哪个进程占用了端口
sudo lsof -i :8080
# 或者换一个端口
docker run -d -p 9090:80 nginx

Q3: 容器启动后立即退出

原因:容器的主进程结束了。

解决:确保 Dockerfile 中的 CMDENTRYPOINT 指向一个前台运行的进程,而不是后台进程。例如:

# ❌ 错误:nginx 默认是后台运行的
CMD ["nginx"]

# ✅ 正确:使用 daemon off 让 nginx 前台运行
CMD ["nginx", "-g", "daemon off;"]

Q4: 如何查看容器为什么启动失败?

# 查看日志
docker logs <容器名>

# 查看退出码
docker inspect <容器名> --format='{{.State.ExitCode}}'

Q5: Docker 镜像太大怎么办?

  1. 使用精简基础镜像:python:3.11-slimalpine
  2. 使用多阶段构建
  3. 合并 RUN 指令减少层数
  4. 使用 .dockerignore 排除无用文件
  5. 清理构建缓存:RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Q6: 如何清理 Docker 占用的磁盘空间?

# 查看 Docker 磁盘使用情况
docker system df

# 清理所有未使用的资源(镜像、容器、网络、数据卷)
docker system prune -a --volumes

输出示例:

WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all volumes not used by at least one container
  - all images without at least one container associated to them
  - all build cache

Are you sure you want to continue? [y/N] y
Deleted Containers:
...
Deleted Images:
...
Total reclaimed space: 2.5GB

Q7: 如何在 Docker 中使用国内镜像加速?

编辑 /etc/docker/daemon.json(没有就新建):

{
  "registry-mirrors": [
    "https://mirror.ccs.tencentyun.com",
    "https://docker.m.daocloud.io"
  ]
}

然后重启 Docker:

sudo systemctl daemon-reload
sudo systemctl restart docker

总结

恭喜你完成了 Docker 入门教程!让我们回顾一下所学的内容:

  1. Docker 是什么:轻量级容器化平台,让应用环境一致、部署简单
  2. 核心概念:镜像是模板,容器是实例,仓库是镜像商店,Dockerfile 是构建配方
  3. 镜像操作:拉取、查看、构建、删除
  4. 容器操作:创建、运行、停止、进入、查看日志
  5. Dockerfile:用指令定义镜像的构建步骤
  6. 数据卷:让容器数据持久化
  7. Docker 网络:让容器之间互相通信
  8. Docker Compose:用 YAML 文件编排多容器应用

下一步学习建议

  • 练习:动手把本教程的所有命令跑一遍,眼过千遍不如手过一遍
  • Docker Hub:浏览 hub.docker.com,了解常用的官方镜像
  • 实战:尝试容器化你自己的项目
  • 进阶:学习 Docker Swarm 或 Kubernetes 进行容器编排
  • CI/CD:了解如何在 GitHub Actions / GitLab CI 中使用 Docker

💡 记住:Docker 的学习曲线并不陡峭,关键是多动手实践。从今天开始,把你的第一个应用容器化吧!

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录