你有没有遇到过这种情况:本地代码跑得稳稳当当,接口一顿点全通,结果一到服务器上就各种翻车——数据库连不上、端口被占、图片打不开、接口404。我当时搭建个人技术空间的时候,就被这套流程折腾得够呛。后来我把从开发到部署的全过程重新梳理了一遍,发现绝大部分问题其实不是代码逻辑的问题,而是“开发”和“部署”之间那条天然的鸿沟没有提前填平。
这篇内容就是把我从零搭建一个个人技术空间(兼具博客、工具箱、后台管理功能)的完整过程记录下来,从需求拆分、技术选型、本地开发,到Docker化改造、服务器部署、域名解析、HTTPS配置,再到备份和监控,一条线全走完。适合那些已经会写代码、但还没有完整走通过一次部署流程的前端、后端或者全栈开发者,也适合想把自己的项目从“只在本地能跑”升级成“真正的线上服务”的人。
1. 个人技术空间到底在解决什么问题:先想清楚再做
1.1 我的触发场景:本地Demo没问题,一上服务器就崩
我最初的动机很简单,就是想搭一个属于自己的技术空间,把平时写的博客、收藏的工具、折腾过的项目整理到一起。但在动手之前,我先复刻了一次以前经常遇到的部署翻车现场。
本地跑一个Spring Boot项目,IDEA里一键启动,浏览器打开localhost:8080,数据正常。可是把打包好的jar包传到服务器上,java -jar启动之后,接口直接报数据库连接失败。查了半天发现是数据库的地址写的还是localhost:3306,但服务器上的MySQL根本还没装。再不然就是装了MySQL,密码不一致,权限没设置好。
前端项目也一样,本地npm run dev一切正常,npm run build之后把dist文件夹扔到nginx里,结果一刷新页面就404。原因是前端用了路由的history模式,nginx没有配置try_files回退。
这些坑单独拿出来都不难解决,但如果一股脑全堆在第一次部署时爆发,就会非常劝退。所以我决定把“个人技术空间”当成一个完整的全栈项目来做,包含后端API、前端界面、数据库、缓存、反向代理、HTTPS证书、备份脚本,一套流程全部走通。
1.2 需求拆分:先明确这个空间要承担什么角色
任何项目动手写代码之前,我都习惯先做需求拆分。个人技术空间听起来很宽泛,但拆开看,无非就是这么几块:
- 内容展示:博客文章的列表、详情、标签分类,这是最核心的部分。
- 身份认证:需要一个后台登录入口,只有自己能发文章、改配置,不能把后台裸奔在公网上。
- 数据存储:文章、用户、访问记录这些数据得持久化,数据库必不可少。
- 静态资源管理:文章里要配图,代码要上传截图,得有一个上传文件的处理方案。
- 运维基础能力:日志要能看,数据要能备份,升级部署要能平滑,这是普通个人项目最容易被忽略的。
1.3 边界划定:哪些功能初期砍掉
我也给自己划了边界。评论系统第一版不做,先用邮件反馈代替;多语言不做,只有中文;AI能力先不接入,等技术空间稳定运行之后,再考虑在服务层接入一个大模型API,做成智能搜索或者自动打标签的助手。
砍需求的目的不是偷懒,而是让核心链路早点跑通。一个项目只有部署上线了,才算真正迈出第一步。功能可以后面慢慢加,但如果一开始就铺得太大,大概率卡在半路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型的取舍:为什么开发框架和部署方式是一起决定的
2.1 三种常见组合对比
个人技术空间的技术选型,我对比了三种方案,各有各的适用场景。
| 方案 | 技术栈举例 | 优点 | 缺点 |
|---|---|---|---|
| 单体全栈框架 | Spring Boot + Thymeleaf / Django + Templates | 部署简单,一个应用搞定 | 前后端耦合,后续想拆API给其他端用比较麻烦 |
| 前后端分离 | Vue/React + Spring Boot/Go/Node API | 职责清晰,前端可单独部署,API可复用 | 部署链路变长,需要处理跨域、反向代理 |
| 纯静态站 | VuePress / Hexo / Astro | 部署最简单,随便一个静态托管都能跑 | 无法做动态功能,后台管理、数据存储基本别想 |
作为一个个人技术空间,我希望能有一定的动态能力,比如后台发布文章、访问统计、未来的交互功能,所以纯静态站直接被排除。单体全栈框架虽然部署简单,但后续如果我想把接口开放给小程序或者其他设备用,前后端耦合的架构会束手束脚。
2.2 我选择前后端分离加容器化的原因
最终我选了前后端分离,并且从一开始就用Docker来统一环境。原因有三个:
第一,前后端分离之后,后端只负责提供API,前端只负责界面展示,两者可以独立开发。我在本地调试的时候,前端用Vite代理转发API请求,后端专注改接口的逻辑。这种开发体验比单体模板渲染舒服得多。
第二,Docker容器化可以彻底解决“在我电脑上是好的”这个问题。本地开发用的是JDK 17、Node 18、MySQL 8.0,服务器上如果版本不一致,很容易出现各种莫名其妙的问题。用Docker把MySQL、Redis、后端服务、前端nginx都装进容器,环境的一致性就锁死了。
第三,容器化的部署和回滚非常方便。改完代码重新build镜像,容器重建一下就行。万一新版本有问题,把旧镜像重新跑起来,几十秒就能回滚。这对没有专业运维团队的个人开发者来说,是性价比最高的方案。
2.3 中间件选型:能不自己造轮子就不自己造
- 数据库:MySQL 8.0,稳定、资料多、生态成熟。个人项目用不到分布式数据库,单实例完全够。
- 缓存:Redis,用来做登录态缓存、接口限流计数、热点数据加速。
- 反向代理:Nginx,部署在前端容器里托管静态资源,同时把
/api路径的请求转发到后端容器。 - 密钥管理:数据库密码、JWT密钥、OSS密钥全走环境变量注入,不写进配置文件,更不进代码仓库。
这套选型没有什么花哨的东西,都是被无数项目验证过的成熟组件。个人项目最忌讳盲目追求新技术,稳定压倒一切。
3. 从零搭建本地开发环境:代码跑起来才算开始
3.1 环境准备清单
先说本地开发的版本清单,这是我实测跑通的组合:
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | 17 (LTS) | 后端开发语言运行环境 |
| Maven | 3.9+ | 后端依赖管理与构建 |
| Node.js | 18 LTS | 前端开发与构建 |
| pnpm | 8+ | 前端依赖管理 |
| Docker Desktop | 最新稳定版 | 本地运行MySQL/Redis,保证与线上环境一致 |
| IDEA / VS Code | 任意较新版本 | 编辑器 |
注意Node版本不要用太新的,有些依赖可能还没跟上。我一开始在Node 21上装某个前端依赖直接报错,切换到Node 18就一切正常了。如果机器上有多个Node版本,建议用nvm做版本管理。
3.2 初始化后端工程与数据库
后端我用Spring Boot,因为Java生态在Web API这块太成熟了,无论是参数校验、异常处理还是持久化,都有现成的方案。用Spring Initializr创建项目,勾选Spring Web、Spring Data JPA、MySQL Driver、Lombok、Validation这几个依赖。
数据库在本地直接用Docker起:
bash复制docker run -d \
--name mysql-dev \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root123456 \
-e MYSQL_DATABASE=tech_space \
mysql:8.0
这里有一个关键点:本地开发虽然用Docker跑MySQL,但端口映射到了宿主机的3306,所以本地的Spring Boot应用可以通过localhost:3306直连。到了服务器上,MySQL容器会跑在Docker内部网络里,后端容器通过服务名访问,这个区别后面部署章节会详细说。
写一个最简单的健康检查接口,确认工程能启动:
java复制@RestController
@RequestMapping("/api/health")
public class HealthController {
@GetMapping
public Map<String, String> health() {
return Map.of("status", "up");
}
}
启动应用后访问http://localhost:8080/api/health,能看到{"status":"up"},说明后端环境已经通了。
3.3 初始化前端工程与API联调
前端用Vue 3加Vite创建项目:
bash复制npm create vite@latest tech-space-web -- --template vue-ts
cd tech-space-web
pnpm install
创建完之后,在vite.config.ts里配置开发代理,把/api开头的请求转发到后端的8080端口:
ts复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
配好这个代理,前端页面里请求/api/health就不会有跨域问题了。Vite的代理在开发阶段非常好用,它把前后端的联调变成了一种“伪同源”的状态,你不用在代码里写死后端的完整URL,后续部署到服务器上也更灵活。
3.4 本地联调中的常见报错排查
这里记录两个我实际遇到的小问题。
第一个是MySQL连接时报时区错误:The server time zone value 'CST' is unrecognized。MySQL 8.0的连接URL必须要带时区参数。我的解决办法是在application.yml里明确指定时区:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/tech_space?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
第二个是前端依赖版本冲突。pnpm install的时候报了ERR_PNPM_PEER_DEPENDENCY_CONFLICT,这是因为某个插件要求的Vue版本和当前项目不匹配。我直接用pnpm add vue@latest把Vue升到了匹配的版本,问题解决。个人项目不用太纠结依赖的精确版本,只要不是生产环境的重大升级,顺手修掉就行。
4. 设计与实现阶段:把单机Demo扩展成可部署应用
4.1 多环境配置:本地和线上不能共用同一套参数
很多第一次部署的人会栽在配置文件上,因为本地连的数据库、用的密钥和线上完全不一样,硬编码在代码里迟早出事。正确的做法是拆分配置文件。
Spring Boot原生支持多Profile配置,我拆成这样:
application.yml:公共配置,比如应用名、JPA相关配置。application-dev.yml:开发环境配置,数据库地址是localhost:3306,日志级别是DEBUG,JWT密钥用本地测试值。application-prod.yml:生产环境配置,数据库地址是mysql(Docker服务名),日志级别是INFO,JWT密钥从环境变量JWT_SECRET注入。
启动时用spring.profiles.active指定环境。本地跑IDEA,在VM options里加-Dspring.profiles.active=dev;服务器上用Docker跑,在容器环境变量里设置SPRING_PROFILES_ACTIVE=prod。
4.2 用户认证与安全机制
个人技术空间的后台登录,不引入太复杂的权限框架,直接用JWT加拦截器实现。
流程是这样的:用户提交账号密码,后端验证通过后签发一个JWT令牌,有效期设置为24小时。前端把令牌存在localStorage,每次请求在Authorization头里带上。后端写一个拦截器,拦截/api/admin/**开头的所有请求,验证令牌有效性和过期时间。
密码存储必须用加盐哈希,我用的是BCrypt,Spring Security里自带的BCryptPasswordEncoder可以直接用。千万不要把密码明文存在数据库里,否则数据库一旦泄露,账号就全完了。
JWT的密钥强度也很重要,要足够长。我在生产环境的.env文件里设置了一个64位随机字符串,用openssl rand -hex 32生成,然后通过环境变量注入到容器里。
4.3 日志规范与统一错误处理
个人项目虽然不需要像大厂那样做全链路追踪,但日志至少要能帮助排查问题。
我用SLF4J加Logback,在application.yml里把日志按天滚动切割:
yaml复制logging:
file:
name: logs/tech-space.log
logback:
rollingpolicy:
max-history: 7
max-file-size: 100MB
统一错误处理用@RestControllerAdvice,每一种异常都返回固定的JSON结构,格式为:
json复制{
"code": 500,
"message": "服务器内部错误",
"data": null
}
前端拿到这个结构后统一提示,不需要关心后端到底抛了什么异常。同时要注意,生产环境不要把异常堆栈直接返回给前端,只记录到日志文件里。
4.4 前端路由与打包配置
前端用的是Vue Router的history模式,这种模式下访问/article/123这个路径是前端路由处理的,服务器上不存在这个物理文件。所以静态服务器必须要配置try_files回退到index.html,否则刷新就404。这个Nginx配置在部署章节会给出。
打包时前端请求的baseURL我设置成相对路径/api,这样不管部署在哪个域名下,都不用改代码。如果直接写死http://localhost:8080,到了线上就废了。
ts复制// 前端请求封装
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
5. 部署前的最后一公里:环境适配、配置抽离与安全检查
5.1 Docker化改造:Dockerfile与docker-compose
部署前最重头的工作是Docker化改造。后端写一个多阶段构建的Dockerfile:
dockerfile复制# 第一阶段:用Maven镜像编译
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:只保留运行环境
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/tech-space.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
前端用Node构建之后,直接用Nginx镜像托管:
dockerfile复制FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpm build
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
接着写docker-compose.yml,把MySQL、Redis、后端、前端四个容器编排到一起:
yaml复制version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: tech-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: tech_space
volumes:
- mysql-data:/var/lib/mysql
networks:
- tech-net
redis:
image: redis:7-alpine
container_name: tech-redis
restart: always
networks:
- tech-net
backend:
build: ./backend
container_name: tech-backend
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
DB_USERNAME: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
REDIS_HOST: redis
JWT_SECRET: ${JWT_SECRET}
ports:
- "8080:8080"
networks:
- tech-net
frontend:
build: ./frontend
container_name: tech-frontend
depends_on:
- backend
ports:
- "80:80"
networks:
- tech-net
volumes:
mysql-data:
networks:
tech-net:
driver: bridge
这里我特意把MySQL的3306端口只暴露在容器内部网络,不再映射到宿主机。这样从公网扫描是扫不到数据库端口的,能少了很多被爆破的风险。
5.2 静态资源与上传文件处理
文章里的配图、上传的附件,这些不能打进镜像里,否则容器一删就全丢了。我的做法是把服务器的/opt/tech-space/uploads目录挂载进后端容器,映射到应用的上传目录:
yaml复制services:
backend:
volumes:
- /opt/tech-space/uploads:/app/uploads
同时Nginx也要能访问到这个目录下的文件,所以前端容器也挂载同一个目录,并在nginx配置里加一个/uploads的静态路由,让图片直接由nginx返回,不经过后端应用,减轻后端压力。
5.3 数据库迁移与初始化
个人项目我用的是最简单可靠的方式:Spring Boot启用了spring.jpa.hibernate.ddl-auto=update,启动时自动根据实体类更新表结构。生产环境其实更好用Flyway做版本化迁移,但考虑到个人项目的表结构变动不频繁,用JPA的自动更新已经够用。
不过有一个坑需要注意:ddl-auto=update在生产环境自动执行DDL是有风险的,如果表结构变更涉及数据迁移,这种自动更新可能丢数据。我目前的阶段是控制表结构变更的频率和风险,等后续功能再复杂一些,会切换成Flyway。
5.4 安全检查清单
部署之前,我给自己列了一份安全检查清单,不是走形式,每一条都在服务器上核对过:
- 服务器安全组只放行22、80、443端口,其他全关。
- 生产环境数据库和Redis不映射宿主机端口,只在Docker网络内访问。
- 所有默认密码全部改掉,数据库密码用随机生成的长密码。
- 生产环境的JWT密钥长度至少64字符,不能和开发环境的一样。
- 后端接口加了一个简单的IP访问频率限制,用Redis计数器实现,单IP每分钟最多60次请求。
- SSH不开放密码登录,只允许密钥登录,并且禁止root直接登录。
6. 服务器部署实操:从手动操作到自动化脚本
6.1 服务器选型与系统初始化
个人技术空间对服务器配置的要求不高,我一开始用的是1核2G的机器,跑起来内存有点紧张,Java进程加上MySQL再加Nginx,内存经常在临界点徘徊。后来升到2核4G,一下子就从容了。建议直接上2核4G,一年也多不了多少钱,体验完全不一样。
系统我选的Ubuntu 22.04 LTS。拿到服务器之后第一件事是创建普通用户、配置SSH密钥登录、更新系统软件包:
bash复制sudo apt update && sudo apt upgrade -y
sudo useradd -m -s /bin/bash tech
sudo usermod -aG sudo tech
然后把本地的公钥写到服务器的~/.ssh/authorized_keys里,测试密钥登录成功后,修改/etc/ssh/sshd_config:
code复制PasswordAuthentication no
PermitRootLogin no
最后重启SSH服务。这一步能防住绝大多数针对密码的暴力破解扫描。
6.2 安装Docker并配置镜像加速
Ubuntu上安装Docker官方源里的社区版:
bash复制sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
拉取镜像的时候,我配置了云厂商提供的镜像加速地址,编辑/etc/docker/daemon.json:
json复制{
"registry-mirrors": ["https://你的加速地址"]
}
配置完重启Docker:sudo systemctl restart docker。
6.3 使用docker-compose部署前后端
把代码传到服务器上,我是直接在服务器上git clone自己的仓库,然后在项目根目录创建.env文件,把需要的密钥全部放进去:
bash复制MYSQL_ROOT_PASSWORD=一串随机生成的强密码
JWT_SECRET=另一串64位随机字符串
创建好之后,执行:
bash复制sudo docker compose build
sudo docker compose up -d
第一次构建会比较慢,因为要下载Maven依赖和npm依赖。构建完启动后,用sudo docker compose ps查看容器状态,再用sudo docker compose logs -f跟踪日志。看到后端日志里出现Started TechSpaceApplication,说明启动成功。
6.4 首次部署失败记录:端口占用与数据卷丢失
我的第一次部署并没有一次成功,踩了两个坑,值得记录下来。
第一个坑是端口占用。服务器上我之前用Python起过一个临时的http.server占着80端口,前端Nginx容器启动的时候直接报bind: address already in use。排查方式是sudo ss -tlnp | grep :80,找到进程PID,kill掉之后再重新docker compose up -d。这件事之后的教训是:部署前先检查服务器上所有常用端口有没有被占,别等容器启动失败才去排查。
第二个坑是MySQL数据卷丢了。第一版部署的时候,我在docker-compose里忘记配置volumes,结果后来为了调整镜像重新执行了docker compose down和docker compose up,MySQL容器重建之后数据全没了。虽然是部署测试阶段没造成实际损失,但这个教训让我把数据持久化这条规则刻在了脑子里:有状态服务必须挂载数据卷,否则容器一删,数据就没了。
7. SSL证书、域名解析与HTTPS强制跳转
7.1 域名购买与DNS解析
个人技术空间最好还是配一个域名,一方面方便记忆,另一方面后续申请SSL证书、配置HTTPS都离不开域名。
域名我在国内云服务商买的,买完之后在DNS解析控制台添加A记录,将tech.example.com解析到服务器的公网IP。等几分钟后,用dig tech.example.com验证解析是否生效。
7.2 使用certbot申请免费SSL证书
证书用的Let's Encrypt,完全免费,通过Certbot工具自动申请和续期。服务器上安装:
bash复制sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d tech.example.com
Certbot会自动检测Nginx配置,为域名申请证书,并帮你把HTTPS配置写好。证书会自动续期,但需要设置一个cron任务来确保,后面会写到。
7.3 Nginx配置反向代理与HTTPS强制跳转
申请完证书,我优化了一份Nginx配置,直接放在前端容器的nginx.conf里:
nginx复制server {
listen 80;
server_name tech.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name tech.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
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;
}
location /uploads/ {
alias /app/uploads/;
}
}
由于Nginx跑在Docker容器里,证书文件也通过数据卷挂载进去。location /的try_files就是解决前端history路由刷新404问题的关键配置。
7.4 证书自动续期与实测验证
Let's Encrypt证书有效期是90天,配置自动续期非常重要。用crontab:
bash复制sudo crontab -e
添加一行,每天凌晨执行两次续期检查:
code复制0 0,12 * * * certbot renew --quiet --deploy-hook "docker exec tech-frontend nginx -s reload"
这里的关键是续期完成后要重载Nginx容器,让新证书生效。配置完先手动测试一次:
bash复制sudo certbot renew --dry-run
看到Congratulations字样,说明自动续期配置成功。
实测HTTPS效果:浏览器访问tech.example.com会被强制跳转到https://,地址栏显示小锁图标。再用curl -I https://tech.example.com验证响应头,能确认返回的是Nginx的HTTPS响应。
8. 数据备份、日志监控与升级流程(踩坑总结)
8.1 数据库备份策略:每天自动导出一份SQL
个人技术空间虽然数据量不大,但文章写多了就是心血,丢了真的会崩溃。我在服务器上写了一个备份脚本,放在/opt/tech-space/backup.sh:
bash复制#!/bin/bash
TIMESTAMP=$(date +"%Y%m%d%H%M%S")
BACKUP_DIR="/opt/tech-space/backups"
MYSQL_CONTAINER="tech-mysql"
DB_NAME="tech_space"
DB_USER="root"
DB_PASS="$MYSQL_ROOT_PASSWORD"
mkdir -p $BACKUP_DIR
docker exec $MYSQL_CONTAINER sh -c \
"exec mysqldump -u$DB_USER -p$DB_PASS $DB_NAME" \
> $BACKUP_DIR/$DB_NAME-$TIMESTAMP.sql
# 只保留最近7天的备份
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
然后用crontab每天早上2点执行:
code复制0 2 * * * bash /opt/tech-space/backup.sh >> /opt/tech-space/backup.log 2>&1
设置好之后,我特意手动执行了一次脚本,确认SQL文件生成并可以正常导入。备份这件事,不能只在文档里写了就算完,必须要实测一次恢复流程。
8.2 日志收集与磁盘告警
Docker容器默认的日志驱动会在/var/lib/docker/containers下无限增长,如果不限制大小,磁盘迟早被撑爆。我在/etc/docker/daemon.json里加了全局日志限制:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
这样每个容器最多保留5个20MB的日志文件,不会无限膨胀。修改完重启Docker生效。
另外写了一个简单的磁盘空间告警脚本,每天检查根分区使用率,超过80%就发邮件通知自己:
bash复制#!/bin/bash
THRESHOLD=80
USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $USAGE -gt $THRESHOLD ]; then
echo "Disk usage is over ${USAGE}%" | mail -s "Disk Usage Warning" my-email@example.com
fi
8.3 平滑升级流程:一次前端资源缓存事故
升级部署这块我踩过最典型的一个坑,是前端资源缓存问题。
有一次我修改了前端页面,重新构建镜像、重启容器,满以为访问到的就是新版本。结果浏览器打开还是旧页面。排查了一下发现,Nginx默认对静态资源返回的身份标识是Last-Modified和ETag,但某些静态资源文件因为哈希没有变化,浏览器直接用了本地缓存。
这个问题的标准解法是前端构建时给文件名加上内容哈希。Vite默认就会在dist/assets目录下生成带哈希的文件名,比如index-abc123.css。所以实际上新构建的HTML里引用的资源文件路径已经变了,只要HTML文件不在浏览器缓存里,就能拿到最新的资源。
问题的根源在于Nginx对index.html也做了缓存配置。解决办法是给index.html设置不缓存:
nginx复制location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
这样HTML每次都回源校验,带哈希的资源文件永久缓存。改完这个配置之后,升级再没遇到过“页面还在旧版本”的情况。
8.4 个人开发者最容易忽略的5件事
最后把这一整套流程走下来,结合我自己的经历和踩过的坑,总结出个人开发者部署项目时最容易忽略的5件事:
-
密钥和账号密码直接写进代码仓库。有些人的项目是公开仓库,数据库密码、JWT密钥全在里面,等于把大门钥匙交给了所有人。必须用环境变量或密钥管理工具隔离。
-
不设置备份或者备份没有验证过。很多人的备份脚本只是写了,从来没跑过,等到真需要恢复的时候才发现脚本是坏的。备份必须定期做恢复演练。
-
数据库端口公网暴露。MySQL、Redis默认端口如果映射到宿主机,公网扫描工具很快就能扫到,然后开始暴力破解。有状态服务尽量只暴露给内网。
-
日志不轮转。不限制Docker日志大小,磁盘被日志文件写满之后服务直接挂掉。这类问题往往发生在你完全想不到的时候。
-
只部署不写部署文档。隔几个月再升级项目,发现自己都忘了当初是怎么部署的。把部署步骤、环境变量、备份恢复流程写进项目的README或者DEPLOY文档,花小钱省大时间。
我把这套流程完整跑通之后,最大的感受是:部署能力不是靠看书学会的,就是要用自己的项目一遍遍撞坑、排坑。个人技术空间从开发到部署的这条路走完,后面再做任何项目,我都会先按照这套框架想清楚环境、配置、安全、备份这些事,而不是写完代码就急着打包。现在我的技术空间已经稳定跑了好几个月了,后台发文章、上传图片、HTTPS访问、每日备份都在按部就班地工作。后面我还想给这个空间加一个基于大模型API的智能标签功能,把文章内容自动分类,这对个人知识库来说会是个很实用的增强。到时候可能又要重新走一遍开发、部署、升级的老路子,不过这次心里就有底了。
