LiaoWL123头像
关注
部署实战(一):2核2G轻量服务器Docker部署Java全栈及小程序上线避坑指南封面图

部署实战(一):2核2G轻量服务器Docker部署Java全栈及小程序上线避坑指南

前言

最近在折腾一个个人项目(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 触发限流,或想增加备用源,可替换/追加以下免费地址:

镜像源地址运营方均速参考特点
DaoCloudhttps://docker.m.daocloud.ioDaoCloud~9.5 MB/s老牌服务,兼容性好,适合兜底
RatDevhttps://hub.rat.devRatDev 社区~9.7 MB/s社区共建,P2P 辅助分发
原子镜像https://atomhub.openatom.cn开放原子开源基金会~8.2 MB/s仅收录可信基础镜像,安全合规场景专用
1Panelhttps://docker.1panel.live/1Panel—限制只能中国地区访问
Docker Proxydockerproxy.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.

原因分析:
这本质上是网络问题导致的连锁反应。

  1. curl 请求 download.docker.com 或某些国内镜像源时被重置(Connection reset),导致 GPG 密钥文件下载失败(no valid OpenPGP data found)。

  2. GPG 密钥文件内容为空或格式错误,导致 /etc/apt/sources.list.d/docker.list 中的软件源无法通过 apt 校验。

  3. 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 镜像注释行,说明下面使用的基础镜像类型。不会被执行。
2FROM eclipse-temurin:21-jre-alpine指定基础镜像。使用 Eclipse Temurin 提供的 OpenJDK 21 JRE,底层是 Alpine Linux。jre 表示只有 Java 运行时,不包含 javac 等开发工具,镜像体积更小。
3WORKDIR /app设置容器内的工作目录为 /app。如果目录不存在,Docker 会自动创建。后续的 COPY、ENTRYPOINT 等都会默认在这个目录下执行。
4COPY target/*.jar app.jar把宿主机 target 目录下构建好的 jar 包复制到容器内,并重命名为 app.jar。注意:如果 target 下同时存在多个 .jar 文件,这条命令会报错,因为目标 app.jar 不是目录。
5空行仅用于分隔内容,无实际作用。
6# 核心:限制JVM堆内存,同时开启 JDK 21 虚拟线程支持注释行,说明下面 ENV 的作用。
7ENV JAVA_OPTS="..."设置环境变量 JAVA_OPTS,容器启动时会被后面的 ENTRYPOINT 引用。具体参数含义见下一节。
8ENTRYPOINT ["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"]

设置容器启动入口。使用 sh -c 是为了让 shell 解析 $JAVA_OPTS 环境变量。最终实际执行的命令是:
java -XX:+UseG1GC -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -Dspring.threads.virtual.enabled=true -jar app.jar。

为什么 ENTRYPOINT 里要加 exec?

容器内 PID 1 的进程是接收停止信号的进程。当执行 docker stop 时,Docker 会向 PID 1 发送 SIGTERM 信号,等待一段时间后若进程未退出则发送 SIGKILL 强杀。

  • 不加 exec:PID 1 是 sh,Java 是子进程。sh 收到 SIGTERM 后自己退出,但不会转发信号给 Java,导致 Java 被 SIGKILL 暴力杀死,无法优雅关闭(正在处理的请求被中断、数据库连接未释放、日志未刷盘)。

  • 加 exec:exec 会用 Java 进程替换 sh 进程,使 Java 成为 PID 1。此时 SIGTERM 直接发给 Java,Spring Boot 会触发优雅关闭流程。

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功能,。

  1. 配置SSH终端:底部 Terminal → 新建 SSH Session。

  2. 配置Deployment:Tools → Deployment → Configuration,配置SFTP。

  3. 警告:在 Mappings 里,部署路径绝对不要填 /,会覆盖服务器系统文件! 填具体路径,如 /home/xxx/project。

  4. 关闭自动上传:Tools → Deployment → 取消勾选 Automatic Upload。防止本地编译的 target 垃圾覆盖服务器文件。

  5. 手动上传:右键文件 → Deployment → Upload to default server(快捷键 Ctrl+Alt+Shift+X)。

  6. 文件管理:使用 Tools → Deployment → Browse Remote Host,右侧弹出的面板可以直接拖拽上传前端dist或修改配置文件。

六、 后续扩展与迁移考量

这套Docker架构的迁移性极强。如果未来项目做大,要升级ECS或换云厂商,只需在新服务器上:

  1. 配好Swap、装好Docker。

  2. 把代码 git clone 下来。

  3. 把MySQL用 mysqldump 导出并在新机器导入。

  4. docker compose up -d。

改一下DNS解析,半小时就能完成平滑迁移。所有的IP变化都不需要改代码,因为Docker内部靠服务名(mysql、redis)通信。

七、 小程序上线合规清单

  1. 微信认证:企业主体需完成,300元/年,这是小程序备案的前提。

  2. ICP备案(双重):

    • 域名ICP备案:后端API域名必须备案,在云厂商控制台申请,10-20个工作日。

    • 小程序ICP备案:在微信公众平台单独提交,与域名备案独立,不能替代。

  3. HTTPS证书与续期:

    • 微信要求后端必须HTTPS,且TLS版本不低于1.2。

    • 免费证书(如Let's Encrypt)有效期只有90天,必须配置自动续期(推荐用 acme.sh 脚本配合DNS API自动续期,或使用云厂商的证书托管服务),否则证书过期小程序直接白屏连不上后端。

  4. 公安联网备案:小程序上线后30日内完成。

  5. 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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--