Docker 实战:博客容器化完整记录
一、核心概念(自己总结的版本)
三个东西的关系
Dockerfile(配方) → docker build → 镜像(光盘) → docker run → 容器(运行中的系统)
- Dockerfile:告诉 Docker 怎么打包应用的文本文件,每一行是一条指令
- 镜像(Image):用 Dockerfile 构建出来的只读模板,包含代码+依赖+运行环境
- 容器(Container):镜像的运行实例,一个隔离的小环境
卷(Volume)
容器删了数据就没了。用 -v 宿主机路径:容器路径 把数据存在宿主机上,容器删了数据还在。
Docker Compose
用一个 YAML 文件定义多个容器,一条命令全部启动。适合前端+后端+数据库这种多服务场景。
二、安装 Docker(Ubuntu)
踩坑过程
第一次尝试: sudo apt install docker.io docker-compose
- 报错:
containerd.io : Conflicts: containerd - 原因:Ubuntu 自带的
docker.io包跟已装的containerd.io冲突
第二次尝试: curl get.docker.com
- 报错:
Connection reset by peer - 原因:服务器连不上 Docker 官网
最终成功: 用国内镜像源手动添加 Docker 源
正确安装步骤
# 1. 安装依赖工具
sudo apt install -y ca-certificates curl gnupg
# 2. 添加 Docker 的 GPG 密钥(让 apt 信任 Docker 的软件包)
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://mirrors.tencentyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 3. 添加 Docker 的 apt 源(用国内镜像)
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.tencentyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 4. 更新索引并安装 Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 5. 把当前用户加入 docker 组(以后不用每次 sudo)
sudo usermod -aG docker $USER
newgrp docker
# 6. 验证安装
docker run hello-world
安装原理
| 命令 | 作用 |
|---|---|
apt update |
更新软件源索引(不是更新软件,是下载"有哪些软件可以装"的列表) |
apt install docker-ce |
安装 Docker 社区版引擎 |
usermod -aG docker $USER |
把当前用户加入 docker 组,以后不用 sudo |
newgrp docker |
让当前终端立即刷新权限,不用重新登录 |
docker run hello-world |
下载 hello-world 镜像 → 创建容器 → 运行 → 输出验证信息 |
三、配置 SSH 免密登录
为什么需要
GitHub 私有仓库需要认证,用 SSH key 可以免密码 clone。
步骤
# 1. 生成 SSH 密钥对(ed25519 是目前推荐的算法)
ssh-keygen -t ed25519
# 一路回车,不设密码
# 2. 查看公钥
cat ~/.ssh/id_ed25519.pub
# 3. 把公钥添加到 GitHub
# 打开 https://github.com/settings/keys
# 点 New SSH key → 粘贴公钥 → 保存
Clone 仓库
# 用 SSH 地址 clone(不是 HTTPS)
git clone git@github.com:你的用户名/你的仓库名.git
注意:必须用 git@github.com: 开头的地址,https://github.com/ 开头的需要输密码。
四、后端 Dockerfile
文件位置
项目目录/backend/Dockerfile
完整内容
# 基础镜像:Python 3.11 精简版(slim 比完整版小很多,减少镜像体积)
FROM python:3.11-slim
# 设置容器内的工作目录,后续命令都在 /app 下执行
# 自动创建 /app 目录(不存在则新建);
# 后面 COPY、RUN、CMD 所有命令默认在 /app 目录执行;
# 容器启动后,默认进入 /app 作为当前目录。
WORKDIR /app
# 先复制依赖文件(利用 Docker 分层缓存:只要 requirements.txt 没变,pip install 就不重新执行)
# COPY 宿主机路径 容器内路径
# 作用:只先拷贝依赖文件,核心目的是利用 Docker 分层缓存
COPY requirements.txt .
# RUN:构建镜像阶段执行命令,执行结果会打包进镜像层。
# pip install -r requirements.txt:读取依赖文件,安装所有第三方库(fastapi、uvicorn 等)。
# --no-cache-dir:关闭 pip 本地缓存,不保存下载的安装包,进一步缩小镜像体积。
# -i 清华源地址:替换国外默认 pip 源,国内下载依赖速度大幅提升。
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 再复制代码文件(代码经常改,放在最后可以最大限度利用缓存)
# 第一个 .:宿主机当前项目所有文件(main.py、配置、静态资源等);
# 第二个 .:容器内 /app 目录;
# 执行时机放在 pip 安装之后:
# 只有代码改动时,这一层会重新构建,但前面 pip 安装的缓存层会保留,不用重复装依赖,节省构建时间。
COPY . .
# 声明容器监听 8001 端口(只是声明,实际映射在 docker run -p 时做)
# EXPOSE:仅声明文档作用,告诉别人这个容器程序会占用 8001 端口。
# 关键点:不会自动对外开放端口!
# 真正端口映射要在启动命令写:docker run -p 宿主机端口:8001。
EXPOSE 8001
# 容器启动时执行的命令:启动 FastAPI 服务,监听所有网卡的 8001 端口
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
为什么先复制 requirements.txt 再复制代码?
Docker 会缓存每一层。如果先复制代码,每次代码改动都会重新安装依赖(很慢)。先复制 requirements.txt,只要依赖没变就用缓存,只重新复制代码。
构建并运行
cd ~/项目目录/backend
# 整行作用:进入后端项目文件夹
# 必须执行这一步:docker build 需要在包含 Dockerfile 的目录运行,不切目录会找不到构建文件,构建失败
# 构建镜像
# docker build:Docker 构建镜像的核心命令
# -t blog-api:-t 是 tag(标签),给生成的镜像起名字叫 blog-api,后续运行、删除镜像都靠这个名字识别
# 末尾的 .:代表当前目录,告诉 Docker 在当前文件夹寻找 Dockerfile、项目代码、依赖文件来打包镜像
# 执行逻辑:读取目录里的 Dockerfile,从上到下逐层构建,生成名为 blog-api 的本地镜像
docker build -t blog-api .
# 运行容器
# -d:后台守护进程运行(detach),容器在后台静默跑,不会霸占当前终端窗口;不加 -d 会把服务日志打印在前台,关终端容器就停止
# -p 8001:8001:端口映射,格式 宿主机端口:容器内部端口
# 后面 8001:容器内 FastAPI 服务监听的端口(Dockerfile 里写的 8001)
# 前面 8001:你本机 / 服务器对外开放访问的端口
# 作用:外部请求访问本机 8001,流量转发进容器 8001 服务
# --name blog-api-test:给启动的容器自定义名称为 blog-api-test
# 不指定会随机生成一串容器名,不方便后续管理(停止、重启、查看日志)
# 末尾 blog-api:指定用刚才 build 生成的镜像来创建容器
docker run -d -p 8001:8001 --name blog-api-test blog-api
# 验证
curl localhost:8001/api/articles
踩坑记录
端口被占用:
Bind for :::8001 failed: port is already allocated
原因:之前有个旧容器在跑,占了 8001 端口
解决:
# 查看哪个容器占了端口
sudo ss -tlnp | grep 8001
# 停掉并删除旧容器
docker stop blog-api
docker rm blog-api
# 重新启动新容器
docker run -d -p 8001:8001 --name blog-api-test blog-api
容器名冲突:
Conflict. The container name "/blog-api-test" is already in use
原因:之前失败的 docker run 也创建了容器
解决:
docker rm blog-api-test
docker run -d -p 8001:8001 --name blog-api-test blog-api
五、前端 Dockerfile
文件位置
项目目录/frontend/Dockerfile
完整内容
# 基础镜像:Node.js 20 精简版(前端构建需要 Node 环境)
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 先复制依赖文件(利用分层缓存:package.json 没变就不重新安装依赖)
COPY package.json package-lock.json ./
RUN npm install --registry=https://registry.npmmirror.com
# 复制前端代码
COPY . .
# 构建 Nuxt 项目(生成 .output 目录,包含生产环境的代码)
RUN npm run build
# 暴露 Nuxt SSR 的默认端口
EXPOSE 3000
# 容器启动命令:运行构建后的 Nuxt 应用
CMD ["node", ".output/server/index.mjs"]
逐行解释
| 指令 | 作用 |
|---|---|
FROM node:20-alpine |
用 Node.js 20 的 Alpine 版本(最小的 Linux 发行版,镜像更小) |
COPY package*.json ./ |
先复制依赖定义文件 |
RUN npm install |
安装 npm 依赖,用国内镜像加速 |
RUN npm run build |
构建 Nuxt 项目,生成生产环境代码 |
EXPOSE 3000 |
Nuxt SSR 默认跑在 3000 端口 |
CMD ["node", ".output/server/index.mjs"] |
用 Node 运行构建后的服务端代码 |
构建并运行
cd ~/项目目录/frontend
# 构建镜像
docker build -t blog-web .
# 运行容器
docker run -d -p 3000:3000 --name blog-web-test blog-web
# 验证
curl -I localhost:3000
验证结果
HTTP/1.1 200 OK
content-type: text/html;charset=utf-8
x-powered-by: Nuxt
返回 200 OK + Nuxt 标识,说明前端容器正常运行。
六、两个容器同时运行
查看运行状态
docker ps
输出:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bb3a9af40d2c blog-web "docker-entrypoint.s…" 23 seconds ago Up 22 seconds 0.0.0.0:3000->3000/tcp blog-web-test
1427d74152ab blog-api "uvicorn main:app --…" 8 minutes ago Up 8 minutes 0.0.0.0:8001->8001/tcp blog-api-test
端口映射解释
-p 8001:8001 的含义:
- 第一个 8001:宿主机端口(服务器对外暴露的端口)
- 第二个 8001:容器内端口(Dockerfile 里 EXPOSE 的端口)
- 流程:浏览器 → 服务器 8001 端口 → 映射到容器 8001 端口 → FastAPI 处理
测试两个服务
# 测试后端 API(注意:不能用 curl -I,因为 /api/articles 只支持 GET,不支持 HEAD)
curl localhost:8001/api/articles
# 测试前端页面(-I 只看响应头,不看内容)
curl -I localhost:3000
七、Docker Compose 多容器编排
为什么需要
手动启动两个容器要分别 docker run,还要记端口号、容器名。docker-compose 用一个 YAML 文件定义所有服务,一条命令全部启动。
docker-compose.yml 文件
位置:项目根目录 ~/项目目录/docker-compose.yml
services:
# 后端服务:FastAPI
backend:
build: ./backend # Dockerfile 所在目录
ports:
- "8001:8001" # 宿主机端口:容器端口
volumes:
- ./data:/app/data # 数据库和上传文件持久化
environment:
- DATABASE_URL=sqlite:////app/data/blog.db # 覆盖 config.py 的默认路径
restart: always # 挂了自动重启
# 前端服务:Nuxt
frontend:
build: ./frontend # Dockerfile 所在目录
ports:
- "3000:3000" # 宿主机端口:容器端口
depends_on:
- backend # 等后端先启动
restart: always # 挂了自动重启
逐行解释
| 配置 | 作用 |
|---|---|
services: |
定义服务列表(每个服务对应一个容器) |
backend: |
后端服务的名称 |
build: ./backend |
用 backend 目录的 Dockerfile 构建镜像 |
ports: "8001:8001" |
端口映射 |
volumes: ./data:/app/data |
把宿主机 ./data 挂载到容器 /app/data(数据库持久化) |
environment: DATABASE_URL |
覆盖默认数据库路径,解决容器内路径计算错误 |
restart: always |
容器挂了自动重启 |
frontend: |
前端服务的名称 |
depends_on: - backend |
前端依赖后端,先启动后端再启动前端 |
常用命令
| 命令 | 作用 |
|---|---|
docker compose up -d |
启动所有服务(后台运行) |
docker compose down |
停止并删除所有容器 |
docker compose restart |
重启所有服务 |
docker compose ps |
查看服务状态 |
docker compose logs -f |
实时看所有服务日志 |
docker compose logs -f backend |
只看后端日志 |
docker compose up -d --build |
重新构建镜像并启动 |
操作流程
# 1. 进入项目根目录
cd ~/项目目录
# 2. 停掉之前手动启动的容器(如果有的话)
docker stop blog-api-test blog-web-test
docker rm blog-api-test blog-web-test
# 3. 用 docker-compose 启动
docker compose up -d
# 4. 查看状态
docker compose ps
# 5. 测试
curl localhost:8001/api/articles
curl -I localhost:3000
注意事项
docker composevsdocker-compose:新版 Docker 用docker compose(空格),旧版用docker-compose(横杠)version: '3'已过时:新版 docker-compose 不需要写 version 字段,写了会有警告-d参数:detach 的缩写,让容器在后台运行。不加-d会占着终端看日志docker stop+docker rm:stop 只停止运行,容器还存在(占端口);rm 才是真正删除curl -Ivscurl:-I发 HEAD 请求,某些 API 不支持会返回 405。测试 API 用curl不加-I
八、Docker 命令缩写说明
| 命令 | 全称 | 含义 |
|---|---|---|
docker rm |
remove | 删除容器 |
docker rmi |
remove image | 删除镜像(i = image) |
docker ps |
process status | 查看容器进程状态 |
docker exec |
execute | 在运行中的容器里执行命令 |
docker logs |
- | 查看容器日志 |
-d |
detach | 后台运行 |
-it |
interactive + tty | 交互模式(打开终端) |
-f |
force / follow | 强制删除 / 实时跟踪日志 |
-p |
port | 端口映射 |
-v |
volume | 挂载卷 |
容器命名规则
- 用
--name指定名字:docker run --name blog-api ... - 不指定名字:Docker 自动生成随机名字(两个英文单词拼一起)
- docker-compose 创建的容器:自动加项目名前缀
九、常用 Docker 命令速查
镜像相关
docker images # 查看本地所有镜像
docker images | grep blog # 按关键词筛选
docker rmi 镜像名 # 删除镜像
docker rmi -f 镜像名 # 强制删除
容器相关
docker ps # 查看运行中的容器
docker ps -a # 查看所有容器(含停止的)
docker stop 容器名 # 停止容器
docker start 容器名 # 启动已停止的容器
docker rm 容器名 # 删除容器(必须先停止)
docker rm -f 容器名 # 强制删除运行中的容器
docker logs 容器名 # 查看容器日志
docker logs -f 容器名 # 实时跟踪日志
docker exec -it 容器名 bash # 进入容器内部
docker stats # 查看容器资源使用
构建相关
docker build -t 镜像名:标签 . # 从 Dockerfile 构建镜像
# -t 指定名称和标签
# . 表示 Dockerfile 在当前目录
卷相关
docker run -v 宿主机路径:容器路径 镜像名 # 挂载卷
# 例:docker run -v /data:/app/data blog-api
清理相关
docker container prune # 删除所有已停止的容器
docker image prune # 删除所有未使用的镜像
docker system prune # 删除所有未使用的数据(容器+镜像+卷)
