Docker——自托管任务管理系统

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-dbtaskflow-redis)而不是原来的 dbredis
  • 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,相当感谢!

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇