前言
最近在折腾一个个人项目(Java后端 + 前端 + MySQL + Redis + Nginx),手里预算有限,只买了一台 2核2G 的轻量应用服务器。在这个"极限配置"下,我踩了不少坑,也总结出了一套非常稳的部署方案。同时,考虑到后续要上架小程序,涉及到备案、HTTPS等合规问题,索性一并记录下来。这篇博客既是我的备忘笔记,也希望能帮到同样在折腾全栈部署的你。
一、 服务器选购与安全初始化
1.1 轻量应用服务器 vs ECS
对于个人开发者,轻量应用服务器是性价比首选。
我的选择:79元/年(新人专享),2核2G,40G系统盘,Ubuntu 22.04 LTS,200M峰值带宽。
为什么不选ECS? 同配置的ECS经济型e实例大概99元/年,带宽只有3M。虽然ECS有完整的VPC、安全组,能连云RDS,但对我这种"单机部署全家桶"来说毫无影响。200M的峰值带宽上传JAR包和前端文件简直快得飞起。
但是享受完轻量应用服务器的新人折扣,不影响ECS的新人折扣,用完这一年的轻量级服务器后面再购买ECS的 QAQ。
弊端提示:轻量服务器无法通过内网连接云MySQL、云Redis,且不支持弹性扩展。所以我建议:在2G小机器上,数据库、缓存、对象存储全用Docker装在本机,不要去买云产品,避免网络隔离的坑。
镜像避坑:购买时千万别选默认的"OpenClaw"或"宝塔面板"应用镜像!一定要在"系统镜像"里选 Ubuntu 22.04 LTS,纯净且教程最多。
1.2 防火墙(安全组)策略:最小权限原则
买完服务器第一件事,就是配置防火墙。这里必须遵循"最小权限原则":把管理入口锁死,只把业务入口开放给所有人。
-
22端口(SSH管理):绝对不要对 0.0.0.0/0 开放! 互联网上每天有无数扫描器在尝试暴力破解22端口。建议将授权对象改为“我的IP”(家庭或公司固定IP),这样只有你能连进去。
-
80端口(HTTP)和443端口(HTTPS):必须保持 0.0.0.0/0 全放行。因为这是给普通用户和微信小程序访问的“正门”,一旦限制,用户就打不开网页。
-
3306(MySQL)和6379(Redis):绝对禁止放行!这两个服务只允许在 Docker 内部网络通信,暴露到公网会被黑客瞬间扫到并植入挖矿病毒。
-
8080(后端测试):测试期可以放行,正式上线后建议关掉,统一走 Nginx 反代到 80/443。

大坑警告:家庭宽带的公网 IP 通常是动态的,运营商重启光猫后 IP 就会变。如果你限制22端口只允许旧IP访问,改完可能连自己都被锁在服务器外面!
保命方法:提前记住阿里云控制台的“VNC远程连接”功能(不依赖SSH),一旦被锁进去改回配置即可。同时强烈建议在服务器上开启 SSH 密钥登录,并禁用密码登录,彻底杜绝爆破风险。
二、 系统初始化:2G内存的"保命符"——配置Swap
2G物理内存跑全家桶非常吃力,一旦内存飙升触发OOM Killer,我的Java进程会被直接杀掉。买完服务器第一件事,先配置2G的Swap虚拟内存(用硬盘空间换内存)。
连上SSH后,逐行执行:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
降低系统对Swap的依赖,尽量用物理内存
sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
让内核参数立即生效
sudo sysctl -p
验证,看到Swap有2.0Gi即成功
free -h
-
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab:将Swap挂载信息写入开机自动挂载文件。tee -a表示追加写入,不会覆盖原配置。不写这行,服务器重启后Swap就会失效。 -
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf:Linux默认会积极使用Swap(默认值60),设为10是告诉系统“尽量用物理内存,实在撑不住再用Swap”,避免频繁的磁盘交换导致服务器卡顿。


三、 Docker安装与核心概念
3.1 在 Ubuntu 24.04 上安装 Docker(国内源方案)
Ubuntu 24.04 (noble) 用户请特别注意:Docker 官方源对 noble 的支持尚不完善,直接手动添加官方源很可能失败(详见 3.2 节)。同时,get.docker.com 在国内网络环境下也经常连接重置。因此,最稳妥的方式是使用国内镜像源手动配置。
以下方案使用中科大镜像源,并显式指定系统代号 noble,已在 Ubuntu 24.04 上实测通过。
# 1. 清理可能残留的旧配置
sudo rm -f /etc/apt/sources.list.d/docker.list
sudo rm -f /etc/apt/keyrings/docker.gpg
sudo apt update
2. 安装基础依赖
sudo apt install -y ca-certificates curl gnupg
3. 创建密钥目录并下载中科大 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
4. 添加中科大 Docker 源(显式指定 noble)
echo
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg]
https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu
noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
5. 更新并安装 Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
6. 启动 Docker 并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
7. 验证
docker -v
docker compose version
执行结果如下:


为什么必须添加Docker官方GPG密钥?
Linux的 apt 包管理器在安装软件时,会检查软件包的数字签名。如果没有导入官方密钥,apt 会报错 NO_PUBKEY 并拒绝安装,因为它无法验证软件包是否来自官方且未被黑客篡改。导入密钥就是告诉系统:“我信任Docker官方源,请用它来安装”。
如果中科大源下载慢或失败,可以替换为清华源:
# 清华源地址(替换上面第3、4步中的 URL)
# GPG: https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg
# 源: https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu
备用方案:使用 Ubuntu 原生源(最简,版本略旧但够用)
sudo apt update
sudo apt install -y docker.io docker-compose-v2
sudo systemctl start docker
sudo systemctl enable docker
docker -v
docker compose version
注意:Ubuntu 24.04 原生源提供的是
docker-compose-v2,安装后命令是docker compose(中间有空格),和官方版一致。
配置镜像加速器和日志轮转(拉取镜像慢、磁盘易爆必做):
方便复制:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.1ms.run"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
解释:
# ============================================================
# Docker 镜像加速器配置 - /etc/docker/daemon.json
# ============================================================
创建配置目录
sudo mkdir -p /etc/docker
写入 daemon.json 配置文件
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
# ---- DaoCloud(上海道客,国内老牌云原生公司) ----
# 免费、免登录、口碑最稳
"https://docker.m.daocloud.io"
---- 毫秒镜像 ----
运营方:木雷坞公司
收费模式:免费层 + 付费流量包
说明:基础服务永久免费,个人可按需购买流量。
免费版走 Cloudflare CDN,日常开发够用。
官方提示:引用时请标注来源"毫秒镜像"
"https://docker.1ms.run",
],
"log-driver": "json-file", // 全局默认日志驱动:把容器日志写成 JSON 文件
"log-opts": { // 传给上面这个日志驱动的参数
"max-size": "10m", // 单个日志文件最大 10MB
"max-file": "3" // 最多保留 3 个日志文件
}
}
EOF
重新加载 systemd 配置(通常非必须,但无害)
sudo systemctl daemon-reload
重启 Docker 服务,使配置生效
sudo systemctl restart docker
⚠️
max-size: 10m和max-file: 3限制了每个容器日志最多占 30M,防止日志把 40G 磁盘撑爆。提醒:上述
#注释仅供阅读参考。直接复制此代码块写入/etc/docker/daemon.json会导致 Docker 解析失败,因为 JSON 标准不支持注释。实际使用时,请将注释行删除,只保留合法 JSON 内容。
免费替换源推荐
如果 docker.xuanyuan.me 触发限流,或想增加备用源,可替换/追加以下免费地址:
| 镜像源 | 地址 | 运营方 | 均速参考 | 特点 |
|---|---|---|---|---|
| DaoCloud | https://docker.m.daocloud.io | DaoCloud | ~9.5 MB/s | 老牌服务,兼容性好,适合兜底 |
| RatDev | https://hub.rat.dev | RatDev 社区 | ~9.7 MB/s | 社区共建,P2P 辅助分发 |
| 原子镜像 | https://atomhub.openatom.cn | 开放原子开源基金会 | ~8.2 MB/s | 仅收录可信基础镜像,安全合规场景专用 |
| 1Panel | https://docker.1panel.live/ | 1Panel | — | 限制只能中国地区访问 |
| Docker Proxy | dockerproxy.net | 社区 | — | 可直接作为备用源 |
配置建议:在 registry-mirrors 数组中按优先级排列,Docker 会按顺序尝试,前面失败时自动回退到后面的源。建议至少保留 2~3 个有效源。
补充:阿里云免费个人版
阿里云容器镜像服务提供个人免费版,包含 5GB 存储 + 1000 次/日拉取额度,地址格式为 https://<你的ID>.mirror.aliyuncs.com,需要在阿里云控制台开通后获取专属地址
我的执行结果如下:


3.2 Docker安装常见报错与解决方案
在实际安装过程中,我遇到了以下报错,这里记录完整的排查过程和解决方案。
报错现象一:找不到包(no installation candidate)
Package docker-ce is not available, but is referred to by another package.
E: Package 'docker-ce' has no installation candidate
E: Unable to locate package containerd.io
E: Unable to locate package docker-compose-plugin
同时,在执行 curl 下载 GPG 密钥时,可能会遇到:
curl: (35) Recv failure: Connection reset by peer
gpg: no valid OpenPGP data found.
原因分析:
这本质上是网络问题导致的连锁反应。
-
curl请求download.docker.com或某些国内镜像源时被重置(Connection reset),导致 GPG 密钥文件下载失败(no valid OpenPGP data found)。 -
GPG 密钥文件内容为空或格式错误,导致
/etc/apt/sources.list.d/docker.list中的软件源无法通过apt校验。 -
apt update更新时,自动跳过了无效的 Docker 源,所以安装时自然提示no installation candidate(找不到安装候选)。
解决方案:直接使用 3.1 节中的中科大镜像源方案,并显式指定 noble 代号。这是目前在 Ubuntu 24.04 上最稳妥的方式。
报错现象二:一键脚本也无法下载
即使使用了 --mirror Aliyun 参数,执行以下命令时仍然报错(之前尝试的这种方式):
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
错误信息:
curl: (35) Recv failure: Connection reset by peer
原因:--mirror Aliyun 参数只影响脚本内部下载软件包时使用的镜像,但脚本本身仍然是从 get.docker.com 下载的。只要该域名被干扰,命令就会在第一步就失败。
解决方案:放弃一键脚本,改用 3.1 节的手动配置国内源方案。
报错现象三:apt 锁被自动更新进程占用
当 Docker 源配置成功、执行安装时,可能会遇到以下报错:
E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 38451 (unattended-upgr)
N: Be aware that removing the lock file is not a solution and may break your system.
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?
原因:Ubuntu 系统默认开启了 unattended-upgrades 自动更新服务,它会在后台自动检查并安装安全更新,运行时会独占 apt 的锁文件。任何并发的 apt install 命令都会因为获取不到锁而失败。
⚠️ 警告:报错信息中特别提示
removing the lock file is not a solution。绝对不要手动删除/var/lib/dpkg/lock-frontend或/var/lib/dpkg/lock,这样会破坏 apt 的完整性,导致后续所有软件管理操作异常。
解决方案:
# 1. 查看占用锁的进程
ps aux | grep unattended
2. 停止自动更新服务
sudo systemctl stop unattended-upgrades
3. 强制结束相关进程(把 PID 换成你实际的进程号)
sudo kill -9 38451 60323
4. 确认锁已释放(无输出代表已释放)
sudo fuser /var/lib/dpkg/lock-frontend
5. 重新执行 Docker 安装命令
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
报错现象四:2G 小机器被自动更新进程吃光内存
在执行 ps aux | grep unattended 排查锁冲突时,可能看到这样的输出:
root 38451 37.8 13.3 456372 219704 ? Sl 23:47 3:14 /usr/bin/python3 /usr/bin/unattended-upgrade
root 60323 13.5 12.6 533652 208984 ? R 23:56 0:00 /usr/bin/python3 /usr/bin/unattended-upgrade
问题:两个 unattended-upgrade 进程同时运行,各占约 200MB+ 内存,在 2G 的机器上瞬间吃掉近 430MB,导致服务器内存告急、Docker 无法安装。
解决方案:
# 1. 停止自动更新服务
sudo systemctl stop unattended-upgrades
2. 强制干掉正在运行的进程(把 PID 换成你实际的)
sudo kill -9 38451 60323
3. 确认锁已释放(无输出代表已释放)
sudo fuser /var/lib/dpkg/lock-frontend
4. 安装 Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
2G 小机器的长期建议:不要重新开启 unattended-upgrades。它会在后台不定时启动,每次吃掉几百 MB 内存。推荐改用手动更新(每周执行一次 sudo apt update && sudo apt upgrade -y),把宝贵的内存留给 MySQL 和 Java。
如果必须保留自动更新,可以改为“只更新列表,不自动安装”:
sudo nano /etc/apt/apt.conf.d/20auto-upgrades
# 改成:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "0";
永久禁用自动更新:
sudo systemctl disable unattended-upgrades
sudo systemctl mask unattended-upgrades
3.3 Docker核心概念
| 概念 | 比喻 | 说明 |
|---|---|---|
| 镜像 (Image) | 安装光盘 | 只读模板,包含应用运行所需的一切 |
| 容器 (Container) | 运行中的程序 | 镜像的运行实例,可读可写,互相隔离 |
| Dockerfile | 菜谱 | 描述如何构建镜像的文本文件 |
| 数据卷 (Volume) | 外接硬盘 | 持久化存储,容器删了数据还在 |
| Docker Compose | 指挥家 | 用一个YAML文件编排多个容器,一键启动 |
3.4 Docker常用命令速查
镜像管理:
docker images # 查看本地所有镜像
docker pull nginx:alpine # 拉取镜像
docker rmi nginx:alpine # 删除镜像
docker build -t myapp:v1 . # 根据Dockerfile构建镜像
测试了下也是没问题:

容器操作:
docker ps # 查看运行中的容器
docker ps -a # 查看所有容器(包括已停止的)
docker run -d --name myapp -p 8080:8080 myapp:v1 # 后台运行
docker stop myapp # 停止容器
docker logs -f myapp # 实时查看日志
docker exec -it myapp /bin/sh # 进入容器内部执行命令
docker rm myapp # 删除容器
资源监控与清理:
docker stats # 实时监控CPU/内存占用
docker system df # 查看Docker磁盘占用
docker system prune -a -f # 清理无用镜像和缓存(数据卷安全)

大概执行了下,后面装Mysql,Redis再使用具体的命令。
四、 2核2G的Docker极限部署方案
4.1 后端Dockerfile(适配 JDK 21)
由于本地使用的是 JDK 21 (Dragonwell 21),Docker 镜像的基础环境也需要同步升级到 JDK 21。
在2G的机器上,千万不要在服务器上跑Maven构建,会瞬间吃掉1G+内存导致卡死。采用“本地打包,只传JAR”的策略:
# 使用 JDK 21 的轻量级 JRE 镜像
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
核心:限制JVM堆内存,同时开启 JDK 21 虚拟线程支持
ENV JAVA_OPTS="-XX:+UseG1GC -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -Dspring.threads.virtual.enabled=true"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"]
| 行号 | 代码 | 说明 |
|---|---|---|
| 1 | # 使用 JDK 21 的轻量级 JRE 镜像 | 注释行,说明下面使用的基础镜像类型。不会被执行。 |
| 2 | FROM eclipse-temurin:21-jre-alpine | 指定基础镜像。使用 Eclipse Temurin 提供的 OpenJDK 21 JRE,底层是 Alpine Linux。jre 表示只有 Java 运行时,不包含 javac 等开发工具,镜像体积更小。 |
| 3 | WORKDIR /app | 设置容器内的工作目录为 /app。如果目录不存在,Docker 会自动创建。后续的 COPY、ENTRYPOINT 等都会默认在这个目录下执行。 |
| 4 | COPY target/*.jar app.jar | 把宿主机 target 目录下构建好的 jar 包复制到容器内,并重命名为 app.jar。注意:如果 target 下同时存在多个 .jar 文件,这条命令会报错,因为目标 app.jar 不是目录。 |
| 5 | 空行 | 仅用于分隔内容,无实际作用。 |
| 6 | # 核心:限制JVM堆内存,同时开启 JDK 21 虚拟线程支持 | 注释行,说明下面 ENV 的作用。 |
| 7 | ENV JAVA_OPTS="..." | 设置环境变量 JAVA_OPTS,容器启动时会被后面的 ENTRYPOINT 引用。具体参数含义见下一节。 |
| 8 | ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] | 容器启动入口。使用 sh -c 是为了让 shell 解析 $JAVA_OPTS 变量。最终执行命令是:java $JAVA_OPTS -jar app.jar。 |
JAVA_OPTS 参数详解
| 参数 | 作用 | 备注 |
|---|---|---|
-XX:+UseG1GC | 使用 G1 垃圾收集器。 | JDK 21 默认就是 G1,可以保留,也可以省略。 |
-Xms256m | 设置 JVM 初始堆内存为 256MB。 | 启动时分配,避免堆频繁伸缩。 |
-Xmx512m | 设置 JVM 最大堆内存为 512MB。 | 限制堆上限,防止容器内存被无限占用。 |
-Duser.timezone=Asia/Shanghai | 设置 JVM 时区为上海。 | 避免容器默认使用 UTC,导致时间相差 8 小时。 |
-Dspring.threads.virtual.enabled=true | 开启 Spring Boot 虚拟线程支持。 | 需要 Spring Boot 3.2+ 和 JDK 21。注意:这不是 JVM 开关,而是 Spring Boot 配置项,通过 -D 以系统属性方式传入。仅对 Spring Boot 应用生效。 |
💡 JDK 21 专属彩蛋:
-Dspring.threads.virtual.enabled=true可以开启 Spring Boot 3.2+ 的虚拟线程支持(Project Loom)。在 2G 小机器上,虚拟线程能极大提升 IO 密集型任务的吞吐量,是 JDK 21 带给我们的最大红利。
如果你希望构建过程也在Docker内完成,可以使用多阶段构建(同样适配 JDK 21):
# 第一阶段:构建(使用 Maven + JDK 21)
FROM maven:3.9.6-eclipse-temurin-21 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
第二阶段:运行
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
ENV JAVA_OPTS="-XX:+UseG1GC -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -Dspring.threads.virtual.enabled=true"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"]
第一阶段:构建阶段
| 代码 | 说明 |
|---|---|
FROM maven:3.9.6-eclipse-temurin-21 AS builder | 指定构建阶段的基础镜像。该镜像包含 Maven 3.9.6 和 Eclipse Temurin JDK 21。AS builder 给这个阶段命名为 builder,方便后续阶段通过 --from=builder 引用它。 |
WORKDIR /build | 设置构建阶段容器内的工作目录为 /build。后续的 COPY、RUN 都在这个目录下执行。 |
COPY pom.xml . | 把宿主机当前目录下的 pom.xml 复制到容器内的 /build 目录。 |
RUN mvn dependency:go-offline -B | 在容器内执行 Maven 命令,提前下载 pom.xml 中声明的所有依赖。-B 表示批处理模式,减少交互输出。这样做的目的是利用 Docker 层缓存:只要 pom.xml 不变,后续重新构建时这层缓存可以复用,不必重复下载依赖。 |
COPY src ./src | 把宿主机当前目录下的 src 源码目录复制到容器内的 /build/src。 |
RUN mvn package -DskipTests -B | 执行 Maven 打包,生成可执行的 jar 文件。-DskipTests 表示跳过测试,-B 表示批处理模式。打包结果默认在 /build/target 目录下。 |
第二阶段:运行阶段
| 代码 | 说明 |
|---|---|
FROM eclipse-temurin:21-jre-alpine | 开始一个新的运行阶段,基础镜像为 Eclipse Temurin OpenJDK 21 的 JRE,底层是 Alpine Linux。只包含 Java 运行时,不包含 Maven、JDK 编译工具和源码,因此最终镜像体积更小。 |
WORKDIR /app | 设置运行阶段容器内的工作目录为 /app。 |
COPY --from=builder /build/target/*.jar app.jar | 从名为 builder 的构建阶段中,把 /build/target/ 目录下的 jar 文件复制到当前阶段的 /app 目录,并重命名为 app.jar。注意:如果 target 下有多个 jar,这条命令会因为目标不是目录而报错。 |
ENV JAVA_OPTS="-XX:+UseG1GC -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -Dspring.threads.virtual.enabled=true" | 设置容器内的环境变量 JAVA_OPTS,供启动命令使用。参数含义:• -XX:+UseG1GC:使用 G1 垃圾收集器。• -Xms256m:JVM 初始堆内存 256MB。• -Xmx512m:JVM 最大堆内存 512MB。• -Duser.timezone=Asia/Shanghai:设置 JVM 时区为上海。• -Dspring.threads.virtual.enabled=true:开启 Spring Boot 虚拟线程支持,需要 Spring Boot 3.2+ 和 JDK 21。 |
EXPOSE 8080 | 声明容器对外暴露 8080 端口。这只是一个文档性质的声明,不会自动把端口映射到宿主机。实际运行时仍需用 docker run -p 8080:8080 或 Compose 的 ports 来映射。 |
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] |
设置容器启动入口。使用 为什么 ENTRYPOINT 里要加 容器内 PID 1 的进程是接收停止信号的进程。当执行
|
4.2 Docker Compose核心语法详解
-
service(服务):一个容器就是一个服务,如
mysql、redis。 -
network(网络):服务之间通过自定义网络互相通信,用服务名当主机名。
-
volume(数据卷):把容器内数据映射到宿主机,容器删了数据不丢。
常用配置项速查:
| 配置项 | 作用 | 示例 |
|---|---|---|
image | 使用的镜像 | mysql:5.7 |
build | 从Dockerfile构建 | build: . |
ports | 端口映射 | "8080:8080" |
volumes | 数据卷挂载 | ./data:/app/data |
environment | 环境变量 | SPRING_PROFILES_ACTIVE=docker |
depends_on | 依赖关系 | - mysql |
restart | 重启策略 | always |
deploy.resources.limits | 资源限制 | memory: 512M |
关于depends_on和健康检查的坑:depends_on 默认只保证启动顺序,不保证服务就绪。必须配合 healthcheck 使用:
mysql:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # MySQL启动慢,前30秒失败不算数
backend:
depends_on:
mysql:
condition: service_healthy # 等MySQL健康检查通过才启动
| 配置项 | 说明 |
|---|---|
mysql: | 定义一个名为 mysql 的服务。Docker Compose 会为这个服务创建容器。 |
healthcheck: | 定义该服务的健康检查规则。Docker 会按规则定期执行命令,判断容器是否健康。 |
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] | 健康检查执行的命令。这里使用 mysqladmin ping 来检测 MySQL 是否响应。CMD 表示直接执行命令,不经过 shell。-h localhost 指定连接本机。数组展开后等价于在容器内执行:mysqladmin ping -h localhost。mysqladmin 是 MySQL 自带的管理工具,官方 MySQL 镜像已预装,无需自己定义。如果命令返回 0,表示健康;返回非 0,表示不健康。 |
interval: 10s | 健康检查的执行间隔。每 10 秒检查一次。 |
timeout: 5s | 单次健康检查的超时时间。如果命令 5 秒内没有返回,就视为本次检查失败。 |
retries: 5 | 连续失败多少次后,才把容器标记为 unhealthy。这里是连续失败 5 次。 |
start_period: 30s | 容器启动后的宽限期。在这 30 秒内,健康检查失败不会被计入 retries,也不会把容器标记为不健康。适合 MySQL 这种启动较慢的服务。 |
backend: | 定义另一个名为 backend 的服务。 |
depends_on: | 定义服务之间的启动依赖关系。 |
mysql: | 指定 backend 依赖的服务名为 mysql。 |
condition: service_healthy | 依赖条件:只有当 mysql 的健康状态变为 healthy 时,才会启动 backend。如果 mysql 一直不健康,backend 就不会启动。 |
4.3 完整的docker-compose.yml(2核2G优化版)
version: "3.8"
services:
mysql:
image: mysql:5.7
container_name: mini-toolbox-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: your_root_password
MYSQL_DATABASE: mini_toolbox
MYSQL_USER: your_name
MYSQL_PASSWORD: your_password
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
deploy:
resources:
limits:
memory: 400M
command:
- --performance_schema=OFF
- --innodb_buffer_pool_size=128M
- --max_connections=50
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- app-network
redis:
image: redis:7-alpine
container_name: mini-toolbox-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
deploy:
resources:
limits:
memory: 128M
command: redis-server --maxmemory 96mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- app-network
backend:
build: .
container_name: mini-toolbox-backend
restart: always
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mini_toolbox?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: your_name
SPRING_DATASOURCE_PASSWORD: your_password
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
deploy:
resources:
limits:
memory: 600M
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
nginx:
image: nginx:alpine
container_name: mini-toolbox-nginx
restart: always
ports:
- "80:80"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ./frontend/dist:/usr/share/nginx/html
deploy:
resources:
limits:
memory: 64M
depends_on:
- backend
networks:
- app-network
volumes:
mysql-data:
redis-data:
networks:
app-network:
driver: bridge
1. MySQL 服务
| 配置项 | 说明 |
|---|---|
image: mysql:5.7 | 使用 MySQL 5.7 官方镜像。注释说“5.7 比 8.0 省一半内存”,在低配环境下确实更轻量。 |
container_name: mini-toolbox-mysql | 指定容器名称为 mini-toolbox-mysql,方便管理。 |
restart: always | 容器退出时自动重启,适合生产环境。 |
environment | 设置 MySQL 初始化环境变量: • MYSQL_ROOT_PASSWORD:root 用户密码。• MYSQL_DATABASE:启动时自动创建数据库 mini-toolbox。• MYSQL_USER:创建普通用户.• MYSQL_PASSWORD:普通用户密码。 |
ports: - "3306:3306" | 把容器 3306 端口映射到宿主机 3306,外部可以直接访问 MySQL。生产环境建议不要暴露,或只绑定内网 IP。 |
volumes: - mysql-data:/var/lib/mysql | 把命名卷 mysql-data 挂载到 MySQL 数据目录,实现数据持久化。 |
deploy.resources.limits.memory: 400M | 限制容器内存为 400MB。注意:在非 Swarm 模式下,deploy 的资源限制可能不生效,建议改用 mem_limit: 400m。 |
command | 覆盖默认启动命令,追加 MySQL 参数: • --performance_schema=OFF:关闭性能模式,节省内存。• --innodb_buffer_pool_size=128M:设置 InnoDB 缓冲池为 128MB。• --max_connections=50:最大连接数 50。 |
healthcheck | 健康检查: • test:执行 mysqladmin ping -h localhost,检查 MySQL 是否存活。• interval: 10s:每 10 秒检查一次。• timeout: 5s:单次超时 5 秒。• retries: 5:连续失败 5 次标记为不健康。• start_period: 30s:启动后 30 秒内失败不计入重试。 |
networks: - app-network | 加入自定义网络 app-network,使其他服务可以通过服务名 mysql 访问它。 |
关于 MySQL 健康检查
mysqladmin ping -h localhost 探测的是 MySQL 进程是否存活,不涉及认证,无需加密码。如果未来你希望更严谨,也可以使用带密码的版本:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
注意 $$ 是转义写法,-p 与密码之间不能有空格。
2. Redis 服务
| 配置项 | 说明 |
|---|---|
image: redis:7-alpine | 使用 Redis 7 的 Alpine 版本,体积小。 |
container_name: mini-toolbox-redis | 容器名称。 |
restart: always | 自动重启。 |
ports: - "6379:6379" | 映射 Redis 默认端口到宿主机。生产环境同样建议不要暴露。 |
volumes: - redis-data:/data | 持久化 Redis 数据到命名卷 redis-data。 |
deploy.resources.limits.memory: 128M | 限制内存 128MB。 |
command: redis-server --maxmemory 96mb --maxmemory-policy allkeys-lru | 启动 Redis 并设置最大内存 96MB,淘汰策略为 allkeys-lru(所有键中按最近最少使用淘汰)。留出约 32MB 给系统开销。 |
healthcheck | 健康检查:执行 redis-cli ping,返回 PONG 表示健康。间隔 10 秒,超时 3 秒,重试 5 次。 |
networks: - app-network | 加入同一网络。 |
3. Backend 服务
| 配置项 | 说明 |
|---|---|
build: . | 使用当前目录下的 Dockerfile 构建镜像。 |
container_name: mini-toolbox-backend | 容器名称。 |
restart: always | 自动重启。 |
ports: - "8080:8080" | 暴露后端服务端口。 |
environment | 设置 Spring Boot 环境变量: • SPRING_PROFILES_ACTIVE: docker:激活 docker 配置文件。• SPRING_DATASOURCE_URL:JDBC 连接串,主机名为 mysql(依赖 Compose 网络的服务名解析)。• SPRING_DATASOURCE_USERNAME: username:数据库用户名,注意与 MySQL 服务中设置的用户名不一致。• SPRING_DATASOURCE_PASSWORD: your_password:数据库密码。• SPRING_REDIS_HOST: redis:Redis 主机名。• SPRING_REDIS_PORT: 6379:Redis 端口。 |
deploy.resources.limits.memory: 600M | 限制后端内存 600MB。 |
depends_on | 依赖 mysql 和 redis,并且指定 condition: service_healthy,表示必须等这两个服务的健康检查通过后才启动。 |
networks: - app-network | 加入同一网络。 |
4. Nginx 服务
| 配置项 | 说明 |
|---|---|
image: nginx:alpine | 使用 Nginx Alpine 镜像。 |
container_name: mini-toolbox-nginx | 容器名称。 |
restart: always | 自动重启。 |
ports: - "80:80" | 把宿主机 80 端口映射到容器 80 端口。 |
volumes | 挂载两个路径: • ./docker/nginx/nginx.conf → /etc/nginx/conf.d/default.conf:自定义 Nginx 配置。• ./frontend/dist → /usr/share/nginx/html:前端构建产物。 |
deploy.resources.limits.memory: 64M | 限制 Nginx 内存 64MB。 |
depends_on: - backend | 依赖后端服务,但只是简单列表,只保证启动顺序,不等待后端健康。 |
networks: - app-network | 加入同一网络。 |
5. 数据卷与网络
| 配置项 | 说明 |
|---|---|
volumes: mysql-data: | 声明命名卷 mysql-data,由 Docker 管理,用于 MySQL 数据持久化。 |
volumes: redis-data: | 声明命名卷 redis-data,用于 Redis 数据持久化。 |
networks: app-network: driver: bridge | 创建自定义 bridge 网络。所有服务加入后,可以通过服务名互相访问,例如 backend 连接 mysql:3306、redis:6379。 |
核心配置说明:
-
deploy.resources.limits.memory:严格限制每个容器的内存上限,防止某个容器吃光所有内存。 -
depends_on + condition: service_healthy:确保MySQL和Redis完全就绪后才启动后端。 -
networks: app-network:所有服务在同一自定义网络内,通过服务名互相访问。 -
SPRING_DATASOURCE_URL中使用mysql:3306作为主机名,Docker内部DNS自动解析,换服务器后不需改任何IP。
4.4 Compose常用操作命令
docker compose up -d --build # 构建并后台启动所有服务
docker compose up -d --build backend # 只重新构建后端
docker compose ps # 查看所有服务状态
docker compose logs -f backend # 查看后端实时日志
docker compose down # 停止所有服务(保留数据卷)
docker compose down -v # 停止并删除数据卷(⚠️会清空数据库)
docker compose restart backend # 重启单个服务
4.5 前端部署策略
绝对不要在2G服务器上 npm run build! Node.js会瞬间吃掉1G内存。正确做法:在本地电脑执行 npm run build,生成 dist 文件夹,通过IDEA的 Remote Host 面板拖拽上传到服务器,挂载给Nginx。
Nginx配置示例:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
| 配置行 | 说明 |
|---|---|
server { | 定义一个虚拟主机配置块。Nginx 可以配置多个 server,每个对应一个站点。 |
listen 80; | 监听 80 端口。容器内 Nginx 监听 80,宿主机通过 ports: - "80:80" 映射。 |
server_name localhost; | 指定域名。localhost 表示本机访问。生产环境可以改成你的域名,例如 example.com。 |
root /usr/share/nginx/html; | 设置静态文件根目录。前端构建产物挂载到容器的这个路径。 |
index index.html; | 指定默认首页文件。访问 / 时返回 index.html。 |
location / { | 匹配所有以 / 开头的请求,也就是前端页面和静态资源。 |
try_files $uri $uri/ /index.html; | 按顺序尝试: 1. $uri:直接找请求路径对应的文件。2. $uri/:找对应目录。3. /index.html:都找不到时返回 index.html。这是 Vue Router、React Router 使用 history 模式时必须的配置,否则刷新子路由会 404。 |
location /api/ { | 匹配所有以 /api/ 开头的请求,例如 /api/user/login。 |
proxy_pass http://backend:8080/; | 把请求转发到后端。backend 是 Docker Compose 服务名,8080 是后端容器内部端口。末尾的 / 很关键,它会把 location 匹配到的 /api/ 前缀替换掉。例如 /api/user 转发到后端时变成 /user。 |
proxy_set_header Host $host; | 把客户端请求的 Host 头传给后端。后端可以通过 request.getHeader("Host") 获取。 |
proxy_set_header X-Real-IP $remote_addr; | 把客户端真实 IP 传给后端。后端获取 X-Real-IP 即可拿到用户 IP,而不是 Nginx 容器 IP。 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | 追加代理链路上的 IP 列表。如果前面还有负载均衡或 CDN,这个头会保留完整链路。 |
proxy_set_header X-Forwarded-Proto $scheme; | 告诉后端原始请求是 http 还是 https。后端生成重定向链接时会用到。 |
proxy_read_timeout 60s; | 设置 Nginx 等待后端响应的超时时间为 60 秒。如果后端处理时间较长,可以调大。 |
} | 结束 location /api/ 块。 |
} | 结束 server 块。 |
4.6 运行情况
需要将compose文件中的配置根据自己的服务修改,同时相关文件和jar包均已上传到服务器指定位置:

通过以下语法检测无误后
docker compose config --quiet # 自查语法,无输出=OK
运行如下命令构建服务拉取镜像启动容器
docker compose up -d --build # 构建镜像 + 全栈启动(首次要拉基础镜像)

等待资源下载。

服务启动很顺利

后端服务一直在重试是因为没有先导入sql,导致启动报错,这个先不管了,后面导入初始化sql后再重启即可。
然后打包前端上传到指定目录下
run build:prod


上传服务器即可:


接下来执行初始化sql,文件我已经上传服务器。


sql导入成功,最后面那个初始化数据sql执行失败是因为创建人字段我将字符串改成了存用户id,这个系统的id是数字,这个不管了,后面再处理。
直接访问服务器80端口,成功访问

接口登录也都正常:

五、 IDEA 高效工作流配置
如果暂时不想使用Xshell和FinalShell等工具,可以使用IDEA自带的SSH和SFTP功能,。
-
配置SSH终端:底部 Terminal → 新建 SSH Session。
-
配置Deployment:
Tools → Deployment → Configuration,配置SFTP。 -
警告:在
Mappings里,部署路径绝对不要填/,会覆盖服务器系统文件! 填具体路径,如/home/xxx/project。 -
关闭自动上传:
Tools → Deployment → 取消勾选 Automatic Upload。防止本地编译的 target 垃圾覆盖服务器文件。 -
手动上传:右键文件 →
Deployment → Upload to default server(快捷键Ctrl+Alt+Shift+X)。 -
文件管理:使用
Tools → Deployment → Browse Remote Host,右侧弹出的面板可以直接拖拽上传前端dist或修改配置文件。
六、 后续扩展与迁移考量
这套Docker架构的迁移性极强。如果未来项目做大,要升级ECS或换云厂商,只需在新服务器上:
-
配好Swap、装好Docker。
-
把代码
git clone下来。 -
把MySQL用
mysqldump导出并在新机器导入。 -
docker compose up -d。
改一下DNS解析,半小时就能完成平滑迁移。所有的IP变化都不需要改代码,因为Docker内部靠服务名(mysql、redis)通信。
七、 小程序上线合规清单
-
微信认证:企业主体需完成,300元/年,这是小程序备案的前提。
-
ICP备案(双重):
-
域名ICP备案:后端API域名必须备案,在云厂商控制台申请,10-20个工作日。
-
小程序ICP备案:在微信公众平台单独提交,与域名备案独立,不能替代。
-
-
HTTPS证书与续期:
-
微信要求后端必须HTTPS,且TLS版本不低于1.2。
-
免费证书(如Let's Encrypt)有效期只有90天,必须配置自动续期(推荐用
acme.sh脚本配合DNS API自动续期,或使用云厂商的证书托管服务),否则证书过期小程序直接白屏连不上后端。
-
-
公安联网备案:小程序上线后30日内完成。
-
ICP许可证:纯展示或自营非交易类无需办理。涉及电商、付费会员等经营性业务才需要申请。
八、 总结
2核2G的机器虽然配置低,但只要做好Swap兜底、Docker内存限制、本地打包部署这三步,完全可以稳如老狗地跑起一整套Java微服务架构。
预期内存水位参考: Java约500M,MySQL约350M,Redis约50M,Nginx约10M,宿主机系统约200M。总计约1.1G-1.3G物理内存,剩余由2G Swap兜底。
如果 docker stats 显示内存稳定、服务不卡顿,说明部署成功!如果服务器频繁卡死,说明2G物理内存确实扛不住全家桶,此时果断把MySQL和Redis从Docker移出,直接在Ubuntu宿主机上用 apt 原生安装(不在Docker里跑),能再省出100-200M内存。
开发者的路就是不断踩坑、填坑的过程,希望这篇博客能帮你少走弯路!
目前前后端已部署到服务器,基础服务仍在调试中。后续的微信小程序申请、域名申请与备案、公安备案等,就留到下期再聊。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_44648243/article/details/167284162




