2.Docker 实战:博客容器化完整记录

2026年5月3日 Nginx_Docker 25 分钟阅读 14 次阅读
📖 文章摘要

以个人博客项目为例,完整记录从 Docker 安装、前后端 Dockerfile 编写到 docker-compose 多容器编排的全流程实战踩坑记录。

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

注意事项

  1. docker compose vs docker-compose:新版 Docker 用 docker compose(空格),旧版用 docker-compose(横杠)
  2. version: '3' 已过时:新版 docker-compose 不需要写 version 字段,写了会有警告
  3. -d 参数:detach 的缩写,让容器在后台运行。不加 -d 会占着终端看日志
  4. docker stop + docker rm:stop 只停止运行,容器还存在(占端口);rm 才是真正删除
  5. curl -I vs curl-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              # 删除所有未使用的数据(容器+镜像+卷)
文章创建于:2026年5月3日CC BY-NC-SA 4.0

评论

暂无评论,来写第一条吧

别老叽霸扫描爆破后台了,个人博客能存啥有价值的东西,有这时间不如去扫俩放片的网站