Docker 容器化入门教程
🐳 从零开始,一步步掌握 Docker 容器化技术
目录
- 什么是 Docker?
- 核心概念
- 安装 Docker
- 镜像操作
- 容器操作
- 编写 Dockerfile
- 数据卷与持久化
- Docker 网络
- Docker Compose
- 实战项目:部署一个完整的 Web 应用
- 常用命令速查表
- 常见问题与排错
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 中的 CMD 或 ENTRYPOINT 指向一个前台运行的进程,而不是后台进程。例如:
# ❌ 错误:nginx 默认是后台运行的
CMD ["nginx"]
# ✅ 正确:使用 daemon off 让 nginx 前台运行
CMD ["nginx", "-g", "daemon off;"]
Q4: 如何查看容器为什么启动失败?
# 查看日志
docker logs <容器名>
# 查看退出码
docker inspect <容器名> --format='{{.State.ExitCode}}'
Q5: Docker 镜像太大怎么办?
- 使用精简基础镜像:
python:3.11-slim或alpine - 使用多阶段构建
- 合并
RUN指令减少层数 - 使用
.dockerignore排除无用文件 - 清理构建缓存:
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 入门教程!让我们回顾一下所学的内容:
- Docker 是什么:轻量级容器化平台,让应用环境一致、部署简单
- 核心概念:镜像是模板,容器是实例,仓库是镜像商店,Dockerfile 是构建配方
- 镜像操作:拉取、查看、构建、删除
- 容器操作:创建、运行、停止、进入、查看日志
- Dockerfile:用指令定义镜像的构建步骤
- 数据卷:让容器数据持久化
- Docker 网络:让容器之间互相通信
- Docker Compose:用 YAML 文件编排多容器应用
下一步学习建议
- 练习:动手把本教程的所有命令跑一遍,眼过千遍不如手过一遍
- Docker Hub:浏览 hub.docker.com,了解常用的官方镜像
- 实战:尝试容器化你自己的项目
- 进阶:学习 Docker Swarm 或 Kubernetes 进行容器编排
- CI/CD:了解如何在 GitHub Actions / GitLab CI 中使用 Docker
💡 记住:Docker 的学习曲线并不陡峭,关键是多动手实践。从今天开始,把你的第一个应用容器化吧!