我把两个 Java 项目和一个 Python AI 服务部署到 2C2G:一次低成本 Docker 容器化实践

一次基于 Docker Compose、LangChain/LangGraph 的低成本容器化部署实践

最近我把自己的自动化抽奖系统和 AI 五子棋部署到了一台阿里云 2 核 2G 服务器上。

一开始,我以为这件事只是把两个 Java 项目打包,再放到服务器上运行。真正整理依赖后才发现,除了两个 Spring Boot 应用,还需要 MySQL、Redis、RabbitMQ、奖品图片存储,以及一个使用 FastAPI、LangChain 和 LangGraph 的 Python AI 服务。

服务器只有 2GB 内存,我又希望两个项目可以同时在线,因此这次部署的重点不是"会不会执行 docker compose up",而是怎样在成本、资源占用和维护复杂度之间做取舍。

本文主要记录容器编排、生产配置、资源控制和实际排障过程,不展开 AI 工作流本身的业务实现。

1. 先看最终架构:2C2G 上到底运行了什么?

#mermaid-svg-UjXdW6u6bwbKb4UW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UjXdW6u6bwbKb4UW .error-icon{fill:#552222;}#mermaid-svg-UjXdW6u6bwbKb4UW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UjXdW6u6bwbKb4UW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UjXdW6u6bwbKb4UW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UjXdW6u6bwbKb4UW .marker.cross{stroke:#333333;}#mermaid-svg-UjXdW6u6bwbKb4UW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UjXdW6u6bwbKb4UW p{margin:0;}#mermaid-svg-UjXdW6u6bwbKb4UW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster-label text{fill:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster-label span{color:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster-label span p{background-color:transparent;}#mermaid-svg-UjXdW6u6bwbKb4UW .label text,#mermaid-svg-UjXdW6u6bwbKb4UW span{fill:#333;color:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW .node rect,#mermaid-svg-UjXdW6u6bwbKb4UW .node circle,#mermaid-svg-UjXdW6u6bwbKb4UW .node ellipse,#mermaid-svg-UjXdW6u6bwbKb4UW .node polygon,#mermaid-svg-UjXdW6u6bwbKb4UW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UjXdW6u6bwbKb4UW .rough-node .label text,#mermaid-svg-UjXdW6u6bwbKb4UW .node .label text,#mermaid-svg-UjXdW6u6bwbKb4UW .image-shape .label,#mermaid-svg-UjXdW6u6bwbKb4UW .icon-shape .label{text-anchor:middle;}#mermaid-svg-UjXdW6u6bwbKb4UW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UjXdW6u6bwbKb4UW .rough-node .label,#mermaid-svg-UjXdW6u6bwbKb4UW .node .label,#mermaid-svg-UjXdW6u6bwbKb4UW .image-shape .label,#mermaid-svg-UjXdW6u6bwbKb4UW .icon-shape .label{text-align:center;}#mermaid-svg-UjXdW6u6bwbKb4UW .node.clickable{cursor:pointer;}#mermaid-svg-UjXdW6u6bwbKb4UW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UjXdW6u6bwbKb4UW .arrowheadPath{fill:#333333;}#mermaid-svg-UjXdW6u6bwbKb4UW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UjXdW6u6bwbKb4UW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UjXdW6u6bwbKb4UW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UjXdW6u6bwbKb4UW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UjXdW6u6bwbKb4UW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UjXdW6u6bwbKb4UW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster text{fill:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW .cluster span{color:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UjXdW6u6bwbKb4UW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UjXdW6u6bwbKb4UW rect.text{fill:none;stroke-width:0;}#mermaid-svg-UjXdW6u6bwbKb4UW .icon-shape,#mermaid-svg-UjXdW6u6bwbKb4UW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UjXdW6u6bwbKb4UW .icon-shape p,#mermaid-svg-UjXdW6u6bwbKb4UW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UjXdW6u6bwbKb4UW .icon-shape .label rect,#mermaid-svg-UjXdW6u6bwbKb4UW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UjXdW6u6bwbKb4UW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UjXdW6u6bwbKb4UW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UjXdW6u6bwbKb4UW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阿里云 ECS

Ubuntu 22.04 · 2C2G · 1GB Swap
基础设施
业务应用
配置注入
配置注入
配置注入
配置注入
配置注入
公网IP:8080
公网IP:8081
浏览器访问
DeepSeek API
Docker Compose 总编排
deployment/.env

数据库密码、MQ密码、DeepSeek配置
lottery-app

Spring Boot

公网端口 8080
gobang-app

Spring Boot

公网端口 8081
ai-planner

FastAPI + LangChain/LangGraph

内部端口 8090
MySQL 8

lottery_system

java_gobang
Redis
RabbitMQ
prize-images-init

一次性初始化任务
奖品图片持久化卷
MySQL / Redis / RabbitMQ

持久化卷

最终方案可以概括成一句话:

一台 ECS、一套总 Compose、两个独立 Java 应用容器和一个独立 Python AI 服务;按实际依赖共享基础设施,只把两个业务端口开放到公网。

当前长期运行的服务一共有六个:MySQL、Redis、RabbitMQ、AI Planner、抽奖系统和五子棋。除此之外还有一个 prize-images-init,它只在初始化时复制演示图片,完成后以 Exited (0) 正常退出。

2. 两个项目一起部署,比我最初想的复杂

两个项目的依赖并不相同。

抽奖系统包括:

  • Spring Boot 应用;
  • MySQL 数据库;
  • Redis 缓存;
  • RabbitMQ 消息队列;
  • 奖品图片存储;
  • Python AI Planner;
  • 可选的邮件和钉钉通知。

五子棋包括:

  • Spring Boot 应用;
  • MySQL 数据库;
  • 通过 Spring AI 调用 DeepSeek 的棋局复盘功能。

这意味着它们不适合只放在 GitHub Pages 或普通静态托管平台上,也不是把两个 Jar 上传服务器就结束。Java、Python和几个中间件都需要对应的运行环境,还要处理启动顺序、数据保存、端口以及生产配置。

如果把这些环境直接安装在服务器上,第一次可能也能跑起来,但以后迁移服务器、重装系统或者排查版本冲突都会比较麻烦。因此我最终使用 Docker 隔离运行环境,再用 Docker Compose 统一管理整套服务。

3. 为什么最后只选一台服务器?

这次部署的目标很明确:用于个人项目展示,访问量不会很大,希望尽量降低几个月的运行成本。

我考虑过几种方案:

方案 结果
两个项目分别使用一台服务器 隔离更直接,但成本和维护量都会增加
单独购买云数据库和消息队列 更省运维,但对个人演示项目来说费用偏高
在服务器上手动安装所有环境 初期简单,后续容易出现版本和配置冲突
使用 Kubernetes 能力很强,但明显超过当前规模和服务器资源
一台 ECS + 一套 Docker Compose 成本最低,隔离性和可维护性也足够

最终我选择了最后一种。

这个选择并不是说 Docker Compose 比其他方案都先进,而是它更符合当前约束。我的目标不是搭建高并发、高可用集群,而是让两套完整项目在低访问量场景下稳定在线,同时保留清晰的部署结构。

服务器最终选择了 Ubuntu 22.04、x86_64、2 核 2G、40GB 磁盘和公网 IPv4。Ubuntu 的 Docker 资料和排障经验比较丰富,也方便后续维护。由于内存只有 2GB,我又额外增加了 1GB Swap,给镜像构建和偶发内存峰值留出缓冲空间。

4. 为什么只保留一套 Docker Compose?

最开始,两个项目各自存在部署配置。如果各维护一套 Compose,就会出现几个问题:

  • 两边都需要配置 MySQL;
  • 容器名、端口和网络容易冲突;
  • 数据库账号和模型配置需要重复维护;
  • 启动整套环境时要进入两个目录分别操作;
  • 后续修改基础设施时容易漏掉其中一个项目。

因此,我把总编排入口放在抽奖项目的 deployment/docker-compose.yml 中,五子棋不再保留重复的 Compose。

服务器上的目录固定为:

text 复制代码
/opt/lottery-stack/
├── lottery-system/
└── java_gobang/

两个仓库保持同级,是因为总 Compose 会使用相对路径找到五子棋项目:

yaml 复制代码
gobang-app:
  build:
    context: ../../java_gobang
    dockerfile: Dockerfile

这样做之后,我只需要在一个目录执行:

bash 复制代码
docker compose build
docker compose up -d
docker compose ps

就能统一构建、启动和检查全部服务。

5. 为什么共享 MySQL,却不让两个项目混在一起?

2GB 内存下再启动第二个 MySQL 容器没有太大必要,因此两个项目共用一个 MySQL 8 容器。但"共享 MySQL 容器"不等于让两个项目共用数据库和账号。

实际结构是:

text 复制代码
MySQL 8
├── lottery_system
│   └── lottery_user
└── java_gobang
    └── gobang_user

共用的是 MySQL 进程和服务器资源,隔离的是数据库、业务账号和密码。

全新数据卷第一次启动时,MySQL 会按照顺序执行挂载到 /docker-entrypoint-initdb.d 的初始化脚本:

text 复制代码
01-init-databases.sh
02-lottery-schema.sql
03-gobang-schema.sql
04-lottery-demo-prizes.sql
05-lottery-demo-users.sql

这些脚本会创建两个数据库、两个业务账号、项目表结构和演示数据。初始化只在空数据卷第一次启动时执行,普通更新不会重复覆盖已有数据。

Redis 和 RabbitMQ 则只由抽奖系统使用,五子棋没有为了"共享"而增加不需要的依赖。

6. 本地能运行,为什么放进 Docker 不一定能运行?

整理五子棋部署配置时,我遇到过一个很容易忽略的问题:本地 IntelliJ 能启动,不代表服务器从 Git 克隆后就能按相同方式运行。

本地和生产环境使用的连接地址完全不同:

text 复制代码
本地运行
application-local.yml
→ localhost:3306

Docker运行
application-prod.yml
→ mysql:3306

在容器内部,localhost 指向的是当前容器自身,而不是 MySQL 容器。所以生产环境必须使用 Compose 中的服务名 mysql

五子棋最终拆分为:

text 复制代码
application.yml       公共配置和AI配置结构
application-local.yml 本地数据库与本地端口
application-prod.yml  生产数据库变量占位符

Compose 启动五子棋时设置:

yaml 复制代码
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/java_gobang
SPRING_DATASOURCE_USERNAME: ${GOBANG_DB_USERNAME:-gobang_user}
SPRING_DATASOURCE_PASSWORD: ${GOBANG_DB_PASSWORD}

Spring Boot 最终加载的是公共配置、application-prod.yml,以及优先级更高的容器环境变量。application-local.yml 只服务本地开发,不会参与生产容器运行。

为了避免本地数据库配置进入生产构建上下文,五子棋的 .dockerignore 还排除了:

text 复制代码
.git
.idea
target
.env
src/test
src/main/resources/application-local.yml

7. 密码和 DeepSeek Key 到底应该放在哪里?

生产配置中最容易混淆的是"配置结构"和"真实配置值"。

我的最终划分是:

text 复制代码
Git仓库
└── 保存变量名、默认值和配置结构

服务器 deployment/.env
└── 保存数据库密码、RabbitMQ密码、DeepSeek Key等真实值

Docker Compose
└── 把 .env 中的值传入对应容器

Spring Boot / FastAPI
└── 从容器环境变量读取配置

服务器部署时,先复制示例文件:

bash 复制代码
cd /opt/lottery-stack/lottery-system/deployment
cp .env.example .env

然后在服务器上填写真实值。数据库和 RabbitMQ 密码使用随机生成的新密码,DeepSeek Key 也只写入服务器 .env,不会提交到 Git。

AI Planner 和五子棋共用同一个 DEEPSEEK_API_KEYDEEPSEEK_BASE_URLDEEPSEEK_MODEL_NAME,但调用路径不同:抽奖 Java 应用通过 Docker 内部网络请求 AI Planner,五子棋则直接通过 Spring AI 请求 DeepSeek。

部署过程中,我还遇到过"配置文件里明明写了邮件配置,服务器为什么仍然发不出邮件"的问题。后来检查容器环境才发现,服务器 .env 中的邮件账号、SMTP 授权码和钉钉 Webhook 仍然为空。

这让我真正区分了两件事:

配置文件中存在环境变量占位符,只代表程序支持读取它;只有运行容器中拿到了非空值,配置才算真正生效。

补齐变量后,需要重新创建抽奖应用容器,新的环境变量才会进入进程。之前已经产生的中奖记录也不会因为后来补配置而自动重发,所以验证时需要重新制造一次中奖事件。

8. 2C2G 真的能跑下这么多服务吗?

可以用于低访问量演示,但前提是主动限制资源,而不是让所有服务使用默认配置。

增加 1GB Swap

服务器最初只有 2GB 物理内存,没有 Swap。我增加了 1GB Swap,主要用于缓冲构建阶段和偶发峰值。

Swap 不能替代物理内存,也不会让服务器变快。它的作用是降低内存短时不足时直接触发 OOM 的概率。

串行构建镜像

两个 Java 项目都使用 Maven 多阶段构建,Python 服务也需要下载依赖。如果并行构建,2GB 内存很容易同时承受多个编译进程。

因此实际构建时使用:

bash 复制代码
COMPOSE_PARALLEL_LIMIT=1 docker compose build

让镜像按顺序构建。第一次构建需要下载 Maven、Python 和基础镜像依赖,在低配服务器上等待十几分钟是正常的。

限制 JVM 和容器内存

两个 Java 应用都限制最大堆为 256MB:

text 复制代码
-Xms96m
-Xmx256m
-XX:MaxMetaspaceSize=96m
-XX:+UseSerialGC

Compose 中,抽奖应用的容器上限为 576MB,五子棋为 512MB。堆内存之外还要为元空间、线程栈和本地库留出空间,所以容器上限不能直接等于 -Xmx

其他服务同样做了约束:

  • MySQL 调低 Buffer Pool、最大连接数并关闭 Performance Schema;
  • Redis 最大内存限制为 64MB,使用淘汰策略;
  • 抽奖系统的异步线程池按 2 核服务器缩小;
  • Docker 日志按单文件 10MB、最多 3 个文件轮转。

这些处理不是为了把 2C2G 变成高性能服务器,而是控制低访问量演示环境中的资源峰值。它仍然不适合高并发生产流量。

9. Docker Compose 如何避免服务乱序启动?

单纯配置 depends_on 只能说明容器启动顺序,不能证明数据库或消息队列已经可以连接。因此我给长期服务配置了健康检查,并使用健康状态作为依赖条件。

抽奖系统的启动关系大致是:

text 复制代码
MySQL健康
Redis健康
RabbitMQ健康
AI Planner就绪
奖品图片初始化完成
        ↓
启动 lottery-app

五子棋则等待 MySQL 健康后再启动。

AI Planner 的 /ready 接口只检查运行模式和必要配置是否完整,不会为了健康检查不断调用 DeepSeek、消耗模型额度。

长期运行的容器还配置了:

yaml 复制代码
restart: unless-stopped

服务器或 Docker 重启后,容器会自动尝试恢复。健康检查解决的是"服务是否真正可用",自动重启解决的是"进程退出后是否恢复",两者作用并不相同。

10. 从两份代码到六个健康服务

实际部署过程大致分成以下几步。

第一步:准备服务器

创建 Ubuntu 22.04 ECS,确认 Docker、Docker Compose、磁盘和系统架构,然后增加 1GB Swap。

安全组最终只开放管理端口和两个业务端口:

text 复制代码
22    SSH / Workbench
8080  抽奖系统
8081  五子棋

MySQL、Redis、RabbitMQ、AI Planner 和 RabbitMQ 管理端口没有对公网开放。

第二步:克隆两个仓库

bash 复制代码
mkdir -p /opt/lottery-stack
cd /opt/lottery-stack
git clone <lottery-system仓库地址>
git clone <java_gobang仓库地址>

部署前必须确认本地需要上线的代码已经提交到远程仓库。五子棋第一次检查时,新增的生产配置仍然只在本地,服务器直接克隆只能拿到旧版本,所以我先完成了配置整理、测试和提交。

第三步:准备服务器私有配置

bash 复制代码
cd /opt/lottery-stack/lottery-system/deployment
cp .env.example .env

随后生成独立的数据库和 RabbitMQ 密码,并在服务器终端中填写 DeepSeek Key。整个过程中不把真实值发到聊天、截图或 Git 仓库。

第四步:先校验 Compose

bash 复制代码
docker compose config --quiet

这一步会提前发现缺少必填环境变量或 Compose 语法错误。如果配置解析都没有通过,就没有必要直接开始耗时构建。

第五步:串行构建三个业务镜像

bash 复制代码
COMPOSE_PARALLEL_LIMIT=1 docker compose build

需要构建的业务镜像包括:

  • 抽奖 Spring Boot 应用;
  • 五子棋 Spring Boot 应用;
  • Python AI Planner。

MySQL、Redis、RabbitMQ 和 Alpine 初始化任务使用官方镜像,不需要自行编译。

第六步:启动并检查

bash 复制代码
docker compose up -d
docker compose ps

最终六个长期服务均进入健康状态,两个项目分别通过下面的地址访问:

text 复制代码
http://服务器公网IP:8080
http://服务器公网IP:8081

当前使用的是公网 IP 加端口,没有部署 Nginx、域名和 HTTPS。因此本文不会把这些未实现的能力写成部署成果。

11. 这次部署真正踩过的坑

五子棋本地改完了,服务器为什么还是旧配置?

因为服务器只会克隆远程仓库,不会读取我电脑上尚未提交的文件。application-prod.yml 当时仍然是本地新增文件,如果直接上服务器,Compose 虽然能传环境变量,远程代码却没有完整的生产配置。

最后我先检查和测试本地改动,再将部署配置准确提交到远程仓库,然后才继续部署。

AI Planner 为什么卡在 Python 依赖下载?

第一次构建 AI Planner 时,从 files.pythonhosted.org 下载依赖发生超时。

我没有马上更换依赖版本或修改项目,而是先分别检查宿主机和 Docker 容器访问 PyPI 的网络情况。后续请求恢复正常,说明是临时网络波动。单独重试 AI Planner 构建后成功,因此没有为了偶发问题引入额外镜像源配置。

为什么不同时构建三个镜像?

2GB 内存下,同时运行两个 Maven 构建和一个 Python 依赖安装没有必要。串行构建虽然总耗时不一定更短,但内存峰值更可控,也更容易判断是哪一个镜像失败。

页面能打开,为什么还是登录失败?

当时页面已经可以通过公网打开,但登录提示"登录方式不存在"。最后发现不是服务器、容器或数据库问题,而是输入的手机号位数不完整。

这个小问题提醒我:页面能打开以后,后续错误仍然要按浏览器输入、业务校验、应用日志、数据库连接分层排查,不能把所有异常都归因于部署。

本地能发通知,服务器为什么不能?

本地 IntelliJ 启动配置中存在邮件和钉钉变量,并不代表云服务器容器会自动继承。检查运行容器后,才确认服务器的相关变量为空。

补齐服务器 .env 并重新创建抽奖容器后,邮件账号、SMTP 授权码和钉钉 Webhook 才真正进入运行环境。

.env 改了,为什么还要重建容器?

Docker Compose 的 .env 主要用于解析 Compose 变量。正在运行的进程不会自动读取后来修改的内容,因此修改后需要重新创建对应容器,而不是只编辑文件就认为配置已经生效。

12. 最终结果:部署成功不只是页面能打开

最终完成并验证的内容包括:

  • MySQL、Redis、RabbitMQ、AI Planner、抽奖系统和五子棋均健康运行;
  • prize-images-init 初始化任务成功完成;
  • 两个项目可以通过公网 IP 和独立端口访问;
  • 一个 MySQL 容器中成功初始化两个独立数据库;
  • 抽奖演示用户、奖品数据和奖品图片完成初始化;
  • MySQL、Redis、RabbitMQ 和奖品图片使用数据卷持久化;
  • 抽奖系统可以通过 Docker 内部网络调用 AI Planner;
  • 五子棋可以读取生产环境中的 DeepSeek 配置;
  • 邮件和钉钉配置补充后成功进入抽奖应用容器;
  • 数据库、缓存、消息队列和 AI Planner 均未直接暴露到公网。

这里的"部署成功"包括配置、依赖、数据、AI 服务和业务访问,而不只是看到某个容器处于 Up 状态。

13. 这套方案适合什么,又不适合什么?

这套方案适合:

  • 个人项目和简历演示;
  • 低访问量;
  • 预算有限;
  • 多个项目共享一台服务器;
  • 希望环境可重复构建和迁移。

它不适合:

  • 高并发业务;
  • 高可用集群;
  • 自动扩缩容;
  • 零停机发布;
  • 对数据库容灾有严格要求的正式生产系统。

单机部署意味着服务器本身仍然是单点。容器内存限制和自动重启可以减少部分故障影响,但不能替代多节点、高可用数据库或完整容灾方案。

所以更准确的定位是:

面向低访问量个人项目展示的一次多应用容器化部署实践。

14. 如果以后长期运行,我还会补什么?

这次部署完成后,我认为有三个很自然的改进方向。

第一,减少在 2C2G 服务器上重复构建镜像。当前项目更新频率不高,所以直接在服务器构建仍然可以接受。如果以后更新频繁,我会考虑提前构建好镜像,让服务器主要负责拉取和启动,降低对服务器内存和外部依赖网络的要求。

第二,增加更整洁的访问入口。当前使用公网 IP 加 8080/8081,适合短期演示。如果以后购买域名并长期维护,再考虑统一入口和 HTTPS。由于这次没有真正部署这些内容,因此它们只属于后续计划。

第三,增加定期数据库备份。Docker Volume 可以避免普通容器重建导致数据丢失,但数据卷本身不等于备份。长期运行时仍然需要把两个数据库定期导出,并把备份放到与当前服务器故障域不同的位置。

总结

这次部署让我感受最深的一点是:部署不是把本地项目换一个地方启动。

真正需要处理的是运行环境、服务依赖、配置边界、敏感信息、数据持久化、资源限制和故障定位。对于这次的个人项目,我没有追求更复杂的技术栈,而是在一台 2C2G 服务器上,用一套 Docker Compose 管理两个 Java 应用、一个 Python AI 服务及其基础设施。

它不是面向高并发生产系统的最终架构,但在低成本、低访问量和可维护性之间达到了我当前需要的平衡。更重要的是,这套方案已经从配置文件真正走到了服务器上,并在遇到网络、内存和环境变量问题后完成了实际验证。

相关推荐
AI分享猿1 小时前
百智云智能PPT 实测:一句话生成整套材料,还能上传自己的模板
人工智能·powerpoint
知几蜗牛1 小时前
同一套GPU多服务2.5倍用户,关键不是换模型
人工智能
快乐非自愿1 小时前
低代码落地实战:场景拆解+企业数字化转型完整框架
人工智能·低代码·架构
lisw051 小时前
代理型人工智能与网络安全:目前的进展状况
人工智能·安全·web安全
QiHY1 小时前
SpringAI+DeepSeek+HTMX实现AI Agent
人工智能·spring·ai·agent·deepseek·spring-ai
ChampaignWolf1 小时前
Joule Unit Test 深度集成:ABAP 单元测试的 AI 六件套全解析
人工智能·单元测试·sap·abap·joule·单元测试ai
星子yu1 小时前
【Java数据结构】二叉树 好像就这样?
java·数据结构
IT_陈寒1 小时前
我TM竟然被Java的空指针坑了第三次!
前端·人工智能·后端
Irene19911 小时前
Flink 学习需要具备的 Java 基础知识总结
java·flink