Docker太伟大了,将平台或者一个项目浓缩到一台容器里面,把其中牵扯到的众多复杂步骤,浓缩成一个开关,你只需要一行命令docker compose up/down就可以直接操控这些容器(我说Docker王朝了尼尔多龙马)。掉点书袋子:纸上得来终觉浅,觉知此事要躬行。因为看理论的时候真看睡着了,那篇笔记https://myblog.marsrains.top/?p=481也很久没动过,所以就搞了个完整项目——全栈任务管理系统(TaskFlow),并最终把它自托管到自己的服务器上,通过域名https://tasks.marsrains.top访问。

这次搭建的东西比较简单,主要是通过构建这个项目来了解容器之间网络的通信和Caddy代理,理解里面的报错处理方式和管理数据卷的方法,一些代码的编写和底层逻辑的运行就先暂时放下。
这个项目从规划到跑通,大概花了不到两天时间(中间踩了无数坑)。今天把整个过程、用到的技术栈、以及所有踩过的坑和槽点完整记录下来,后续也会把相关的代码上传github
老规矩,先看最终效果
项目最终效果
- 前端:React + Vite + Tailwind(简洁的任务 CRUD 界面)
- 后端:FastAPI + SQLModel + asyncpg
- 数据库:PostgreSQL
- 缓存:Redis
- 反向代理 + HTTPS:Caddy(和我的 WordPress 博客共用同一个 Caddy)
- 部署方式:全部整合进已有的博客 `docker-compose.yml`,统一管理
访问https://tasks.marsrains.top就能看到正在运行的系统(如果我没down掉的话)
项目架构概览
整个系统由 6 个容器组成:
- taskflow-frontend:React 静态页面(Nginx 托管)
- taskflow-backend:FastAPI 接口服务
- taskflow-db:PostgreSQL 数据库
- taskflow-redis:Redis 缓存
- blog_caddy:统一反向代理 + 自动 HTTPS
- 原有 WordPress 服务(共用同一个网络 blog_net)
所有服务都在同一个 Docker 网络中,通过服务名互相通信。
技术栈与目录结构
后端:Python 3.12 + FastAPI + SQLModel
前端:React 18 + Vite + Tailwind + Axios
部署:Docker + Docker Compose + Caddy
wordpress/
├── compose.yml
├── Caddyfile
├── .env
└── taskflow/
├── backend/
├── frontend/
└── .env
核心实现要点
1、后端实现(FastAPI+SQLModel)
后端采用 FastAPI 作为框架,结合 SQLModel(SQLAlchemy + Pydantic 的现代结合)来处理数据库操作。
其中牵扯的关键文件:
models.py:定义了Task模型,使用 Enum(TaskStatus和TaskPriority)来规范状态和优先级。database.py:异步数据库连接池 +init_db()函数,在应用启动时自动创建表。crud.py:封装了任务的增删改查操作。main.py:核心路由 + lifespan 管理(启动时初始化数据库)。
其中Enum类型的映射是最头疼的,因为它映射到PostgreSQL ENUM的时候,很容易出现重复创建导致InterityError的报错,即使删除数据卷后再重建还是相同的报错,这里暂时按下不表,后面再吐槽这个东西和绕过它的方式
2、前端实现(React+Vite)
前端使用 Vite 作为构建工具,搭配 React 18 + Tailwind CSS 快速搭建界面,目前的主要功能就是
- 任务列表展示
- 新建任务(标题、描述、优先级)
- 修改任务状态(待办 → 进行中 → 已完成)
- 删除任务
3、Docker 整合
因为我已经有了一个运行中的 WordPress + Caddy 项目,最终选择不单独维护两个 compose 文件,而是将 TaskFlow 的四个服务(db、redis、backend、frontend)直接整合进博客的 compose.yml 中。
里面的关键配置:
- 所有 TaskFlow 服务都加入同一个网络
blog_net - 使用正确的服务名(
taskflow-db、taskflow-redis)而不是原来的db、redis - backend 使用
env_file: ./taskflow/.env,并通过environment覆盖关键连接字符串 - frontend 不暴露端口,完全通过 Caddy 代理
4、Caddy 反向代理配置
这一部分也很简单,毕竟是跟Wordpress处在同一个目录下,直接在Caddyfile里面添加就可以:
tasks.marsrains.top {
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
encode gzip zstd
# 优先匹配 API 请求
handle /api/* {
reverse_proxy taskflow-backend:8000
}
# 其余所有请求转发给前端(支持 React Router)
handle {
reverse_proxy taskflow-frontend:80
}
log {
output stdout
}
}
坑点和槽点
上次折磨得我死去活来的还是搭harbor仓库,一个证书配置的问题困扰了一个下午,要么linux无法验证证书,要么就是浏览器无法验证上传的证书形成完整的证书链。但现在harbor升级了,这个问题不存在,具体可以看看汤的博客,他在这一方面很有造诣
1、端口冲突
起初,本来打算是另外搞一个单独文件夹来启动taskflow,毕竟独立出来后续维护和迁移都会比较方便,结果就是配置代理的时候,启动 frontend 时一直报 Bind for :::3000 failed: port is already allocated。查了半天发现是我的监控栈里 Grafana 已经占用了 3000 端口。所以我最终放弃独立暴露端口,全部走 Caddy 统一反向代理。这是第一次意识到“已有项目整合”比单独部署更现实。
所以我就直接把东西全部丢进Wordpress的文件夹底下,跟我的博客待在一块了。
2、服务名解析失败
依然相同的问题:把 TaskFlow 当作独立项目,用 docker network connect 把容器手动加入博客的 blog_net。结果 Caddy 一直报 no such host: taskflow-backend。
ping、curl 在 caddy 容器里都解析不到 TaskFlow 的服务名。我尝试了多种 network connect 方式,换了不同网络名,都不稳定。最终也是把 TaskFlow 的四个服务全部搬进博客的 compose.yml,统一使用 blog_net,服务名解析立刻稳定。扩展一下上面那句话:
“已有项目整合”比单独部署更现实,不同 compose 项目之间手动连接网络非常不可靠!
3、ENUM类型重复创建

报错:duplicate key value violates unique constraint "pg_type_typname_nsp_index",具体是 taskstatus 这个 ENUM 类型已存在。即使后续删了 volume(docker volume rm),重启后依然报错。
原因就是SQLModel 在 metadata.create_all() 时会尝试重新创建 PostgreSQL ENUM 类型,而 PostgreSQL 不允许同名 ENUM 重复存在。所以最后重新在compose.yml里面添加了新的脚本指令
command: >
sh -c "
echo 'Waiting for database to be ready...' &&
sleep 8 &&
echo 'Dropping existing enum types if they exist...' &&
PGPASSWORD=\769392740 psql -h taskflow-db -U taskflow -d taskflow -c 'DROP TYPE IF EXISTS taskstatus CASCADE;' || true &&
PGPASSWORD=\769392740 psql -h taskflow-db -U taskflow -d taskflow -c 'DROP TYPE IF EXISTS taskpriority CASCADE;' || true &&
echo 'Starting FastAPI server...' &&
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2
"

总结:使用Enum映射到数据库类型时,要特别注意初始化逻辑
4、DATABASE_URL 主机名写错
一定要记住同步.env的配置,让.env和compose.yml的环境保持一致,这个坑我踩了两次,一次是刚整合时,一次是改了 .env 后忘记同步。
5、volume 过滤和删除
docker volume ls | grep -E 'taskflow'
docker volume rom $(sudo docker vomlume ls -q | grep -E 'taskflow') 2>/dev/null || true

检查容器部署的数据卷的时候,一定要记住过滤和筛选,方便发现不同的前缀,然后再删除。
总结
这个容器部署起来很容易,也很简单。LLM也确实是个得力的助手,用得好的话,就能在一些地方一针见血地指出错误,这次也是通过LLM的辅助,成功搞定了这个项目。对于Docker的学习,不应该只是局限于单一容器,而是要学会多容器写作、生产级部署和问题排查。书要看,实验项目也要做,才好理解。
也欢迎去github看一下我上传的taskflow,如果可以的话麻烦star和fork,相当感谢!





