很多开发者第一次做 Web 项目时,真正卡住的并不是页面或接口,而是项目写完之后不知道下一步该做什么。
本地开发时,前端运行在:
http://localhost:5173
后端运行在:
http://localhost:8080
数据库可能是自己电脑里的 MySQL 或 PostgreSQL。只要打开开发工具,执行几条启动命令,项目就能正常使用。
但当你希望其他人通过浏览器访问这个项目时,问题会突然变多:
项目代码应该放在哪里;
Linux 服务器为什么需要公网 IP;
域名怎样指向服务器;
前端构建完成后放在哪里;
后端进程退出后怎样自动恢复;
Nginx 为什么能够把请求转发给后端;
HTTPS 证书怎样配置和续期;
Docker 容器删除后数据库为什么可能丢失;
用户上传的图片应该放在服务器磁盘还是 OSS;
日志写在哪里,服务器出错后怎样排查;
新版本怎样发布,发布失败后怎样回滚;
服务器重启后,所有服务能否自动恢复。
这说明“部署上线”并不是把源代码复制到服务器,然后执行一次启动命令。
一个真正可对外提供服务的 Web 系统,至少需要解决下面六件事:
项目可以构建
→ 程序可以持续运行
→ 用户可以通过域名访问
→ 请求可以正确转发
→ 数据和文件不会随进程消失
→ 出现故障时能够发现、排查和恢复
本文会以一个常见项目作为参考:
前端:React、Vue 或其他 SPA 项目
后端:Spring Boot、FastAPI、Django、Express、NestJS 等
数据库:PostgreSQL 或 MySQL
缓存:Redis,可选
文件:阿里云 OSS 或本地持久化目录
服务器:Ubuntu Linux
入口:Nginx 或 Caddy
部署:systemd 或 Docker Compose
具体框架可以替换,但整个部署数据流基本不会改变。
一、先理解一个 Web 项目上线后的完整结构
用户访问一个已经上线的网站时,请求通常会经过下面这条链路:
用户浏览器
↓
域名 DNS 解析
↓
服务器公网 IP
↓
云服务器安全组
↓
Linux 防火墙
↓
Nginx / Caddy
├── 返回前端静态文件
└── 转发 /api 请求
↓
后端应用
├── PostgreSQL / MySQL
├── Redis
├── OSS
└── 第三方服务
例如用户访问:
https://example.com/products/100
DNS 首先把 example.com 解析为服务器公网 IP。浏览器与服务器的 443 端口建立 HTTPS 连接,Nginx 接收请求并返回前端页面。
页面继续请求:
https://example.com/api/products/100
Nginx 发现路径以 /api/ 开头,于是把请求转发给服务器内部的后端服务:
http://127.0.0.1:8080/api/products/100
后端查询数据库,得到商品信息,再通过 Nginx 把响应返回给浏览器。
在这套结构中,每一个组件都有明确职责:
| 功能模块 | 解决的问题 | 常见选择 | 入门项目建议 |
|---|---|---|---|
| 代码管理 | 保存版本、协作、回滚 | GitHub、GitLab、Gitee | GitHub 或 GitLab |
| 云服务器 | 提供公网计算资源 | 阿里云 ECS、轻量服务器、腾讯云 CVM、普通 VPS | Ubuntu 云服务器 |
| 应用运行环境 | 提供 Java、Python、Node.js 等运行时 | JDK、Python venv、Node.js、Docker | 优先 Docker |
| 进程管理 | 进程退出后自动重启 | systemd、PM2、Supervisor、Docker Compose | systemd 或 Docker Compose |
| 反向代理 | 接收公网请求并转发到应用 | Nginx、Caddy、Traefik、云负载均衡 | Nginx |
| 域名解析 | 把域名映射到服务器 IP | 阿里云 DNS、Cloudflare DNS、腾讯云 DNSPod | 任意可靠 DNS 服务 |
| HTTPS | 加密浏览器与服务器通信 | Certbot、Caddy、云证书服务 | Nginx + Certbot |
| 关系型数据库 | 保存用户、订单等业务数据 | PostgreSQL、MySQL、MariaDB、云数据库 RDS | PostgreSQL 或 MySQL |
| 缓存 | 保存临时数据、会话和热点结果 | Redis、Valkey | 业务确实需要时再加入 |
| 文件存储 | 保存图片、视频和附件 | 本地目录、Docker Volume、NFS、OSS | 私有 OSS |
| 日志 | 保存程序运行和错误信息 | journald、Docker Logs、Loki、ELK、云日志服务 | stdout + journald/Docker Logs |
| 监控 | 发现服务中断和性能异常 | Prometheus、Grafana、Uptime Kuma、云监控 | Uptime 检查 + 云监控 |
| 自动部署 | 自动测试、构建和发布 | GitHub Actions、GitLab CI、Jenkins | GitHub Actions |
| 备份 | 防止数据库和文件永久丢失 | pg_dump、mysqldump、磁盘快照、OSS | 定时数据库备份到异机或 OSS |
这张表不意味着每个项目都需要安装所有产品。一个个人项目的最小结构可能只有:
Ubuntu
+ Nginx
+ 后端程序
+ PostgreSQL
+ 域名
+ HTTPS
而一个比较完整的单服务器生产环境可能是:
Ubuntu
+ Nginx
+ Docker Compose
+ 前端静态文件
+ 后端容器
+ PostgreSQL
+ Redis
+ OSS
+ 日志收集
+ 监控告警
+ 自动备份
+ GitHub Actions
技术选型不应该从“哪个工具最先进”出发,而应该先回答:当前系统有哪些进程、哪些数据必须保存、哪些端口需要暴露,以及出现故障后怎样恢复。
二、项目部署之前,代码需要先具备“可部署性”
很多项目无法顺利部署,并不是服务器配置有问题,而是代码从一开始就只考虑了本地开发。
一个可部署项目应该形成下面的构建模型:
源代码
→ 安装锁定版本的依赖
→ 执行测试
→ 构建可发布产物
→ 注入运行环境配置
→ 启动程序
→ 健康检查
区分源代码、构建产物和运行环境
不同类型项目最终发布的内容不同。
前端项目通常执行:
npm ci
npm run build
生成:
dist/
或者:
build/
浏览器并不直接运行 React 或 Vue 的源代码。生产环境真正需要的是构建后的 HTML、CSS 和 JavaScript 文件,这些文件一般交给 Nginx、Caddy、CDN 或对象存储提供访问。
Spring Boot 项目通常生成:
app.jar
Python 项目一般发布源代码和锁定的依赖文件:
requirements.txt
poetry.lock
uv.lock
Node.js 后端则可能发布源代码、编译后的 dist 目录和依赖描述文件。
需要避免把整个本地开发目录原样复制到服务器。下面这些内容通常不应该进入生产发布包:
node_modules/
.idea/
.vscode/
本地日志
测试数据库
临时上传文件
开发环境 .env
构建缓存
配置和代码必须分离
数据库密码、JWT 密钥、OSS AccessKey 和第三方服务 Token 不应该写死在源代码中。
错误示例:
String password = "123456";
String accessKeySecret = "abc...";
更合理的方式是让应用读取环境变量:
APP_ENV=production
SERVER_PORT=8080
DB_HOST=postgres
DB_PORT=5432
DB_NAME=example
DB_USERNAME=example_app
DB_PASSWORD=...
REDIS_HOST=redis
REDIS_PORT=6379
OSS_ENDPOINT=...
OSS_BUCKET=...
OSS_ACCESS_KEY_ID=...
OSS_ACCESS_KEY_SECRET=...
JWT_SECRET=...
环境变量解决的是“同一份代码如何运行在不同环境中”的问题。
开发环境:连接本地数据库
测试环境:连接测试数据库
生产环境:连接生产数据库
代码仓库中可以保留一个不含真实密钥的模板:
.env.example
真实生产配置则放在服务器上的受限文件、CI/CD Secret、云平台密钥管理服务或容器 Secret 中。
让应用成为无状态进程
理想情况下,后端进程本身不应该保存需要长期存在的数据。
例如下面的设计存在问题:
用户上传图片
→ 后端保存到项目目录 ./uploads
如果应用使用 Docker,重新创建容器后,这些文件可能随容器可写层一起消失。如果后续启动两个后端实例,一个实例保存的文件,另一个实例也无法直接读取。
更合理的划分是:
结构化业务数据 → PostgreSQL / MySQL
缓存和临时状态 → Redis
图片和附件 → OSS
应用程序 → 随时可以停止和重新创建
这就是“无状态应用”的核心含义:应用实例可以替换,真正需要保存的数据由专门的持久化系统管理。
提供健康检查接口
应用至少应该提供一个简单的健康检查接口:
GET /health
返回:
{
"status": "UP"
}
进一步可以区分两个接口:
/health/live
表示进程仍然存活;
/health/ready
表示数据库、必要配置和关键依赖已经准备好,可以接收真实流量。
健康检查不能只判断端口是否打开。一个进程可能仍然占用 8080 端口,但数据库连接池已经耗尽,所有业务请求都在失败。
设计数据库迁移
数据库表结构不应该依赖开发者手工登录服务器执行 SQL。
不同技术栈可以选择:
| 技术栈 | 常见迁移工具 |
|---|---|
| Java | Flyway、Liquibase |
| Python | Alembic、Django Migrations |
| Node.js | Prisma Migrate、TypeORM Migration、Knex |
| Ruby | Active Record Migrations |
一个正常发布过程应该明确包含:
备份数据库
→ 执行迁移
→ 启动新版本
→ 验证功能
数据库迁移需要考虑回滚。特别是删除字段、修改字段类型和大规模更新数据时,不应该假设应用回滚后,数据库也能自动恢复。
比较安全的方式是先执行兼容性迁移:
第一版:新增字段,但保留旧字段
→ 新旧代码都可以运行
→ 完成数据迁移
→ 所有实例更新
→ 后续版本再删除旧字段
三、服务器、域名、反向代理和 HTTPS 怎样连接起来
代码准备完成后,下一步是让公网用户能够找到服务器,并把请求送到正确的应用进程。
准备 Linux 服务器
个人项目可以选择 Ubuntu、Debian 或 Rocky Linux。对初学者来说,Ubuntu Server 的资料比较丰富,常见软件安装和排障也更直接。
服务器至少需要:
公网 IP
CPU 和内存
系统磁盘
公网带宽
Linux 操作系统
SSH 登录能力
服务器购买完成后,应先完成基础安全配置:
创建普通管理用户
配置 SSH 公钥
禁止或限制密码登录
避免长期直接使用 root
更新系统软件包
配置时区
启用防火墙
只开放必要端口
Ubuntu 常用 UFW 管理主机防火墙。云服务器通常还存在一层“安全组”,因此一条公网请求可能需要同时通过云安全组和 Linux 防火墙。
通常只应公开:
22 SSH
80 HTTP
443 HTTPS
SSH 的 22 端口最好只允许可信 IP,或者修改访问方式。数据库端口不应该直接向整个公网开放:
5432 PostgreSQL
3306 MySQL
6379 Redis
这些端口应只允许本机、Docker 内部网络、私有网络或指定应用服务器访问。
域名和 DNS 的作用
域名本身并不会自动知道服务器在哪里。需要在 DNS 控制台中添加解析记录。
例如:
类型:A
主机记录:@
值:服务器公网 IPv4
这表示:
example.com → 服务器公网 IPv4
如果还需要:
www.example.com
可以添加另一个 A 记录,或者使用 CNAME 指向主域名。
DNS 的基本职责,就是把人类可读的域名转换为服务器 IP 地址。
如果服务器位于中国内地并使用域名对外提供网站服务,通常需要先完成 ICP 备案;中国香港及其他非中国内地节点不走相同的工信部备案流程。具体要求应以服务器接入商和主管部门的最新规则为准。
反向代理究竟在代理什么
用户提到的“方向代理”,通常指的是反向代理。
正向代理站在客户端一侧:
用户
→ 正向代理
→ 目标网站
反向代理站在服务器一侧:
用户
→ 反向代理
→ 一个或多个内部应用
Nginx 常常是用户访问服务器后接触到的第一个应用层服务。它可以提供静态文件、反向代理、缓存、负载均衡以及 TCP/UDP 代理等能力。
使用反向代理后,后端可以只监听:
127.0.0.1:8080
而不是直接暴露给公网。公网只看到:
https://example.com
反向代理主要解决:
使用一个域名访问前端和后端;
把 HTTPS 处理集中在入口层;
隐藏后端真实端口;
提供前端静态资源;
根据域名和路径转发请求;
设置上传大小、超时和限流;
转发客户端真实 IP;
后续扩展多个后端实例。
一个常见 Nginx 配置如下:
server {
listen 80;
server_name example.com www.example.com;
root /srv/example/frontend;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1: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_connect_timeout 5s;
proxy_read_timeout 60s;
}
}
这里存在两条完全不同的数据流。
前端文件请求:
GET /assets/index.js
→ Nginx
→ /srv/example/frontend/assets/index.js
→ 浏览器
接口请求:
GET /api/users/me
→ Nginx
→ 127.0.0.1:8080/api/users/me
→ 后端应用
try_files $uri $uri/ /index.html 对 React Router、Vue Router 等前端 History 路由非常重要。否则用户直接访问:
https://example.com/products/100
Nginx 会尝试寻找真实文件:
/srv/example/frontend/products/100
最终返回 404。回退到 index.html 后,路径才会交给前端路由处理。
X-Forwarded-Proto 告诉后端,用户最初使用的是 HTTP 还是 HTTPS。如果没有正确处理代理头,后端可能生成错误的跳转地址,或者认为安全请求来自 HTTP,进而影响 Cookie 的 Secure 属性和 OAuth 回调地址。
如果项目使用 WebSocket,还需要配置 Upgrade 请求头;如果使用 SSE 或长时间任务,需要调整代理缓冲和读取超时。不能只看到普通 REST 接口能够访问,就认为反向代理已经完全正确。
Nginx、Caddy 和 Traefik怎样选择
Nginx适合希望理解反向代理、静态文件、路径转发和 TLS 配置的开发者。它的生态成熟,生产环境使用广泛,但配置需要自己维护。
Caddy的配置通常更短,提供域名后默认可以自动管理 HTTPS 证书和 HTTP 到 HTTPS 的跳转,也可以直接配置反向代理。
例如:
example.com {
root * /srv/example/frontend
handle /api/* {
reverse_proxy 127.0.0.1:8080
}
try_files {path} /index.html
file_server
}
Traefik更适合容器和动态服务环境。它可以根据容器标签或编排平台配置自动发现服务,但对于只有一个前端和一个后端的项目,可能增加不必要的理解成本。
一个实用选择是:
第一次系统学习部署:Nginx
希望快速管理 HTTPS:Caddy
容器服务数量较多:Traefik
多服务器和大规模流量:云负载均衡
配置 HTTPS
Nginx 常与 Certbot 配合申请和自动续期证书。Certbot 可以读取现有 Nginx 配置,为站点安装证书,并把 HTTP 请求切换到 HTTPS。
典型流程是:
域名已经解析到服务器
→ 80 端口可以正常访问
→ 安装 Certbot
→ 为 Nginx 申请证书
→ 开放 443 端口
→ 测试自动续期
不应该只确认“今天 HTTPS 可以访问”,还需要检查证书是否能自动续期。否则几个月后证书过期,浏览器会阻止用户正常访问。
四、应用怎样在 Linux 上持续运行
在服务器终端执行:
java -jar app.jar
或者:
python main.py
只能证明应用能够启动。关闭 SSH 连接、程序异常退出或服务器重启后,应用可能停止。
因此需要一个进程管理工具负责:
启动
停止
重启
开机自启
异常恢复
查看状态
收集输出
方案一:直接运行加 systemd
systemd 是 Linux 常见的系统和服务管理器,.service 单元可以描述一个需要被启动和监督的进程。
Spring Boot 服务可以配置为:
[Unit]
Description=Example Web Backend
After=network.target
[Service]
Type=simple
User=webapp
Group=webapp
WorkingDirectory=/srv/example/backend
EnvironmentFile=/etc/example/backend.env
ExecStart=/usr/bin/java -jar /srv/example/backend/app.jar
Restart=on-failure
RestartSec=5
SuccessExitStatus=143
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
保存为:
/etc/systemd/system/example-backend.service
然后执行:
sudo systemctl daemon-reload
sudo systemctl enable example-backend
sudo systemctl start example-backend
sudo systemctl status example-backend
查看日志:
journalctl -u example-backend -f
这种方式适合:
只有一两个服务;
不希望引入 Docker;
已经熟悉 Linux 文件、用户和权限;
应用运行环境相对固定。
它的主要问题是服务器容易积累大量手工配置。JDK、Python 依赖、Node.js 版本和系统库都安装在宿主机中,时间久了可能很难准确复现环境。
Node.js 项目还可以使用 PM2。PM2 是面向 Node.js 应用的进程管理工具,可以保持应用运行并提供状态和资源查看能力。
Python 项目也常见 Supervisor。Supervisor 通过配置文件定义需要运行和管理的程序,并提供 supervisorctl 进行启动、停止和状态管理。
不过在现代 Linux 单机部署中,systemd 通常已经能满足通用进程管理需求。没有必要同时让 systemd、Supervisor 和 PM2 管理同一个进程。
方案二:使用 Docker Compose
Docker 将应用和运行依赖打包成镜像。Docker Compose 则使用一个 YAML 文件描述多个服务、网络和数据卷,并统一管理它们的生命周期。官方文档也将单主机生产部署列为 Compose 的适用场景。
一个常见的单服务器结构是:
宿主机
├── Nginx
├── Docker
│ ├── backend
│ ├── postgres
│ └── redis
├── /srv/example/frontend
└── /etc/example/*.env
对应的 Compose 文件可以表达为:
name: example-web
services:
backend:
image: registry.example.com/example-backend:${
APP_VERSION}
restart: unless-stopped
env_file:
- /etc/example/backend.env
ports:
- "127.0.0.1:8080:8080"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
postgres:
image: postgres:18
restart: unless-stopped
env_file:
- /etc/example/postgres.env
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- pg_isready -U "{
mathJaxContainer[0]}{
POSTGRES_DB}"
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:8
restart: unless-stopped
command:
- redis-server
- --appendonly
- "yes"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
后端端口使用:
"127.0.0.1:8080:8080"
表示宿主机只能通过本机地址访问 8080,公网不能直接连接。Nginx 位于同一台宿主机,因此仍然可以把请求转发到:
127.0.0.1:8080
PostgreSQL 和 Redis 没有配置 ports,因此不会直接暴露到宿主机和公网。后端可以通过 Docker 网络中的服务名连接:
postgres:5432
redis:6379
这里经常出现一个错误:
后端容器中的 DB_HOST=localhost
容器里的 localhost 指向后端容器自己,而不是 PostgreSQL 容器。容器之间应该使用 Compose 服务名通信。
Docker Compose 适合:
单台服务器;
前后端和数据库包含多个进程;
希望开发、测试和生产环境尽量一致;
希望按镜像版本发布和回滚;
暂时不需要 Kubernetes。
它并不自动解决:
数据备份;
多服务器调度;
数据库高可用;
日志长期保存;
安全更新;
零停机发布;
服务器本身故障。
Docker 能够提高部署环境的一致性,但不能代替完整运维设计。
什么时候才需要 Kubernetes
Kubernetes 用于管理容器化工作负载和服务,提供声明式配置、调度、服务发现和自动化能力。
它适合:
多台服务器
多个服务
多个副本
频繁发布
弹性扩缩容
高可用要求
团队具备平台运维能力
对于一个部署在单台服务器上的个人项目,直接使用 Kubernetes 往往意味着需要额外理解:
Pod
Deployment
Service
ConfigMap
Secret
Ingress 或 Gateway
PersistentVolume
探针
调度
滚动发布
这些概念确实重要,但并不会因为项目规模很小就自动变简单。
更合理的学习顺序通常是:
Linux 进程
→ systemd
→ Nginx
→ Docker
→ Docker Compose
→ 多实例与负载均衡
→ Kubernetes
先理解请求、进程、端口、数据卷和反向代理,再进入 Kubernetes,才不会把 YAML 配置误认为部署原理本身。
五、持久化不是挂一个 Volume 就结束了
部署系统时,需要把数据分成三类:
业务数据
文件数据
临时数据
它们的保存方式和可靠性要求不同。
关系型数据库是业务数据的主要来源
用户、商品、订单、权限和支付记录通常保存在 PostgreSQL 或 MySQL 中。
数据库有两种主要部署选择。
自建数据库:
应用服务器
└── Docker Compose
└── PostgreSQL / MySQL
优点是成本低、结构直观,适合个人项目和测试环境。缺点是数据库与应用共享服务器,一旦服务器磁盘损坏、误删数据或者整台实例不可用,应用和数据库可能同时失效。
托管数据库:
应用服务器
→ 云数据库 RDS
托管数据库通常会承担一部分备份、监控、版本维护、高可用和故障恢复工作。代价是成本更高,也仍然需要正确配置账号权限、网络访问和备份保留策略。
对于学习项目,可以先在 Compose 中运行数据库;对于真实付费业务、重要用户数据和无法接受长时间恢复的系统,应该认真评估托管数据库。
Docker Volume 与数据库备份不是一回事
Docker Volume 是由 Docker 管理的持久化数据存储。即使使用它的容器被删除,Volume 中的数据仍可以保留。
但是:
容器删除
≠ Volume 删除
不代表:
服务器损坏
≠ 数据丢失
如果 Volume 仍然位于同一台服务器的系统磁盘上,服务器磁盘损坏或整台实例被删除时,数据仍可能消失。
因此需要明确区分:
持久化:进程或容器重建后数据仍存在
备份:原始数据损坏后可以从另一份副本恢复
高可用:一台节点失效后服务仍能继续工作
这三者解决的是不同问题。
PostgreSQL 提供 pg_dump、pg_restore、pg_basebackup 等备份和恢复工具。
一个基础备份流程可以是:
每天凌晨
→ 执行 pg_dump
→ 压缩备份文件
→ 计算校验值
→ 上传到 OSS 或另一台服务器
→ 删除超过保留期的旧备份
→ 记录任务结果
→ 备份失败时告警
示例:
pg_dump \
--host=127.0.0.1 \
--username=example_backup \
--format=custom \
--file=/var/backups/example/example-$(date +%F-%H%M).dump \
example
但“备份命令执行成功”并不等于备份可用。需要定期在独立数据库中执行恢复测试:
创建临时数据库
→ pg_restore
→ 检查关键表
→ 验证数据量和业务查询
只有真正恢复过的备份,才更接近可信备份。
用户文件放在哪里
用户头像、商品图片、PDF 和视频可以选择三种存储方式。
本地目录:
/srv/example/uploads
适合内部工具、低访问量项目和临时应用。必须保证目录不在容器可写层,并配置磁盘备份。
Docker Volume 或挂载目录:
volumes:
- /srv/example/uploads:/app/uploads
适合单机项目,但仍然依赖这一台服务器。
对象存储:
阿里云 OSS
腾讯云 COS
Amazon S3
MinIO
更适合公网图片、附件、大文件和未来可能扩展多实例的应用。
数据库中建议保存:
bucket
object_key
original_name
content_type
size
owner_id
status
而不是永久保存一个可能过期或变更域名的完整签名 URL。
Redis 是否需要持久化取决于业务用途
Redis 可以只作为缓存,也可以承担会话、延迟任务、消息和部分临时状态。
如果 Redis 只保存数据库查询缓存:
Redis 数据丢失
→ 应用重新查询数据库
→ 缓存逐渐重建
这种情况下可以接受不持久化。
如果 Redis 保存:
登录会话
待处理任务
限时订单状态
未消费消息
就必须明确数据丢失的后果。
Redis 提供 RDB 快照、AOF 追加日志、两者结合以及关闭持久化等方式。RDB 更接近定时快照,AOF 记录写操作并可在启动时重放。
不要因为配置了:
appendonly yes
就把 Redis 当成关系型数据库。Redis 的数据模型、事务能力、备份方式和故障恢复机制仍然与 PostgreSQL 不同。
六、日志、监控、安全和自动部署让系统真正可维护
项目能够打开,只能说明第一次部署成功。项目是否能够长期运行,取决于是否具备可观测和可恢复能力。
日志应该回答什么问题
当用户反馈“下午两点上传失败”时,日志至少应该帮助回答:
哪一个请求
哪个用户
访问了哪个接口
请求落在哪个服务实例
处理了多长时间
返回了什么状态码
异常发生在哪一步
数据库或第三方服务返回了什么错误
推荐结构化日志至少包含:
{
"timestamp": "2026-08-04T14:00:00+08:00",
"level": "ERROR",
"service": "example-backend",
"environment": "production",
"requestId": "req-7c194",
"method": "POST",
"path": "/api/files",
"status": 500,
"durationMs": 842,
"userId": 42,
"message": "Upload failed",
"exception": "..."
}
requestId 很重要。一次请求可能经过:
Nginx
→ 后端
→ 数据库
→ OSS
→ 第三方接口
所有组件都记录同一个请求标识,排查时才能把分散的日志串起来。
容器应用通常优先把日志写到标准输出和标准错误:
stdout
stderr
再由 Docker、journald 或日志采集器统一处理。不要让每个容器无限向内部文件写日志,否则可能造成容器磁盘不断增长,也不利于统一检索。
日志方案可以分为三个阶段:
入门:
journalctl / docker logs
单机生产:
日志轮转 + Loki + Grafana
较大系统:
Loki 或 ELK/OpenSearch + 统一采集 + 告警
Loki 以日志标签作为主要索引,并与 Grafana 配合查询和展示日志。它适合已经使用 Prometheus 和 Grafana 的系统。
不要把用户 ID、请求 ID等高基数字段全部设置成 Loki 标签,否则会产生大量日志流。高基数字段更适合保存在结构化日志内容或元数据中。
监控和日志解决的问题不同
日志回答:
某一次请求为什么失败
监控回答:
系统当前是否正在大面积失败
基础设施指标包括:
CPU
内存
磁盘空间
磁盘 IO
网络流量
进程数量
容器状态
应用指标包括:
每秒请求数
平均响应时间
P95 / P99 延迟
HTTP 4xx / 5xx 比例
数据库连接池
任务队列长度
缓存命中率
业务指标包括:
注册成功率
支付成功率
文件上传成功率
订单创建数量
消息发送失败数量
Prometheus 是面向指标采集、查询和告警的监控工具,通常通过定期抓取应用暴露的指标端点获取时间序列数据。
Grafana 可以将 Prometheus 指标和 Loki 日志放在同一界面中。当错误率突然上升时,可以先看到指标异常,再跳转到相同时间范围的日志。
一个个人项目不一定一开始就需要部署完整的 Prometheus、Grafana 和 Loki。最低限度应该有:
站点可用性检查
服务器 CPU 和内存监控
磁盘空间告警
应用错误日志
数据库备份失败告警
HTTPS 证书过期提醒
安全需要覆盖入口、应用和数据
服务器安全组与 UFW 应只开放必要端口。数据库和 Redis 不应直接暴露给公网。
应用层还需要考虑:
登录和权限校验;
密码安全存储;
JWT 和 Session 过期策略;
CORS;
CSRF;
SQL 注入;
文件类型和大小限制;
接口限流;
上传文件内容检测;
敏感日志脱敏;
依赖漏洞更新。
CORS 解决的是浏览器是否允许某个来源读取响应,不是服务端身份验证。把:
Access-Control-Allow-Origin: *
改成指定域名,并不能代替后端权限校验。
生产环境密钥不应进入 Docker 镜像。否则即使运行时不显示这些值,任何能够拉取镜像的人仍可能从镜像层中找到密钥。
Docker Socket 也不应暴露给普通应用容器:
/var/run/docker.sock
获得 Docker Socket 的访问权限,通常意味着能够在宿主机上创建高权限容器,风险接近获得服务器控制权。
从手动部署升级到 CI/CD
最初可以手动部署:
本地构建
→ SCP 上传
→ SSH 登录服务器
→ 重启服务
但随着发布次数增加,这种方式容易出现:
忘记运行测试;
上传了错误文件;
服务器上直接修改代码;
不知道当前运行哪个版本;
发布步骤依赖个人记忆;
无法快速回滚。
更标准的流程是:
开发者推送代码
→ CI 安装依赖
→ 运行测试
→ 构建前端和后端
→ 构建 Docker 镜像
→ 推送镜像仓库
→ 部署到测试环境
→ 执行健康检查
→ 审批生产发布
→ 生产服务器拉取指定镜像
→ 执行数据库迁移
→ 启动新版本
→ 验证健康状态
可选择:
GitHub Actions
GitLab CI/CD
Jenkins
阿里云云效
腾讯云 CODING
GitHub Actions 的 Environment 可以为不同部署环境设置环境变量、Secret、允许部署的分支和审批规则。
发布时不要始终使用无法追踪的镜像标签:
latest
更合理的是:
example-backend:1.4.2
example-backend:git-a82fc71
服务器应明确记录:
当前版本
上一个稳定版本
发布时间
对应 Git Commit
执行过的数据库迁移
回滚时才能执行:
APP_VERSION=1.4.1 docker compose up -d
但应用回滚不代表数据库自动回滚。数据库变更必须尽量向前兼容,否则旧版本代码可能无法读取新结构。
七、一次完整上线应该按照什么顺序执行
把前面的组件放到一起,一个基本 Web 项目的完整上线流程可以整理为下面这条主线。
第一阶段:完成可部署代码
1. 前后端功能在本地运行
2. 代码提交到 Git
3. 锁定依赖版本
4. 区分开发、测试和生产配置
5. 移除硬编码密码
6. 提供生产启动命令
7. 提供健康检查接口
8. 建立数据库迁移
9. 明确文件存储方式
10. 让应用日志输出到 stdout
第二阶段:准备基础设施
11. 购买 Linux 云服务器
12. 创建普通管理用户
13. 配置 SSH 公钥
14. 更新系统软件包
15. 设置安全组和防火墙
16. 购买并配置域名
17. 必要时完成 ICP 备案
18. 安装 Docker 或对应运行时
19. 安装 Nginx 或 Caddy
20. 准备数据库、Redis 和 OSS
第三阶段:第一次发布
21. 构建前端静态文件
22. 构建后端产物或 Docker 镜像
23. 把生产配置安全地放入服务器
24. 创建数据库和最小权限账号
25. 执行数据库迁移
26. 启动数据库和 Redis
27. 启动后端服务
28. 使用本机地址测试后端
29. 部署前端构建文件
30. 配置 Nginx 反向代理
31. 通过公网 IP 测试
32. 配置 DNS
33. 申请 HTTPS 证书
34. 使用域名测试完整功能
第四阶段:补齐生产能力
35. 配置进程或容器自动重启
36. 配置开机自启
37. 配置日志轮转或日志收集
38. 配置服务器和应用监控
39. 配置站点可用性检查
40. 配置数据库自动备份
41. 把备份复制到服务器之外
42. 执行一次恢复测试
43. 配置证书自动续期
44. 配置部署版本记录
45. 准备回滚方法
46. 建立自动化发布流程
三种推荐部署组合
学习型项目:
Ubuntu
+ Nginx
+ systemd
+ Spring Boot / FastAPI / Node.js
+ PostgreSQL
+ OSS
+ journald
+ 定时 pg_dump
这套方案最适合理解 Linux 进程、端口、文件路径、反向代理和服务管理。
单服务器标准项目:
Ubuntu
+ Nginx 或 Caddy
+ Docker Compose
+ 后端容器
+ PostgreSQL 或云数据库
+ Redis
+ OSS
+ GitHub Actions
+ 云监控或 Prometheus
+ Loki / Grafana
+ 自动备份
这套方案适合个人产品、小团队项目和第一版商业系统。
多服务器业务系统:
DNS
+ CDN / WAF
+ 云负载均衡
+ Kubernetes 或托管容器平台
+ 多个应用副本
+ 托管数据库
+ 托管 Redis
+ OSS
+ 消息队列
+ Prometheus / Grafana
+ 集中式日志
+ 自动扩缩容
+ 灰度和滚动发布
不要从第三套架构开始搭建只有几十个用户的项目。系统复杂度应该随着真实流量、可用性要求和团队规模增加,而不是为了看起来像“大厂架构”提前堆叠组件。
上线验收标准
不能把“浏览器能打开首页”作为项目已经上线的标准。
至少要验证:
使用域名可以访问,而不是只通过 IP;
HTTP 会正确跳转到 HTTPS;
HTTPS 证书有效并能够自动续期;
前端刷新子路由不会出现 404;
API 请求正确经过反向代理;
后端无法通过公网端口直接访问;
数据库和 Redis 没有暴露公网;
服务器重启后服务自动恢复;
容器重新创建后数据库数据仍然存在;
用户上传文件仍然可以访问;
错误请求能在日志中通过 Request ID 定位;
磁盘空间不足时能够收到告警;
数据库备份任务真实执行;
备份可以在独立环境中恢复;
新版本发布失败后能够回滚;
回滚后的代码与数据库结构仍然兼容;
密钥没有出现在 Git 仓库和 Docker 镜像中;
普通用户不能访问管理接口;
移动端、桌面端和弱网环境下核心功能可用。
可以直接交给 Cursor、Codex 或 Claude Code 的部署提示词
你是一名具备 Linux、Docker、网络、安全和生产环境经验的 DevOps 工程师。请帮助我把当前 Web 项目部署到一台 Linux 服务器,并通过域名和 HTTPS 对外提供服务。
不要立即生成一套与项目无关的通用 Docker、Nginx 配置。请先检查当前代码仓库,并完成以下分析:
1. 识别前端和后端使用的技术栈、版本、构建工具和启动命令。
2. 检查项目目录结构,以及前后端是同一仓库还是独立仓库。
3. 检查当前是否已有 Dockerfile、compose.yaml、Nginx 配置、systemd 服务或 CI/CD 工作流。
4. 检查项目使用的数据库、Redis、对象存储、消息队列和第三方服务。
5. 搜索硬编码的 localhost、端口、数据库密码、JWT Secret、OSS 密钥和开发环境地址。
6. 检查前端 API Base URL、跨域配置、History 路由和生产构建输出目录。
7. 检查后端生产启动方式、健康检查、日志输出、数据库迁移和优雅关闭逻辑。
8. 检查用户上传文件当前保存在哪里,部署后是否会因为容器重建而丢失。
9. 先向我说明当前项目的运行模型、存在的问题和建议部署方案,再开始修改代码。
目标部署结构优先考虑:
用户浏览器
→ DNS
→ Linux 服务器 443 端口
→ Nginx 或 Caddy
├── 提供前端静态文件
└── 转发 /api 请求
→ 后端应用
├── PostgreSQL / MySQL
├── Redis
└── OSS
如果项目是 Next.js、Nuxt、SSR 或其他服务端渲染项目,请根据实际运行模型调整,不要强行把它当成纯静态 SPA。
部署方案选择要求:
- 如果项目只有一个后端服务,可以评估 systemd。
- 如果项目包含后端、数据库和 Redis,优先评估 Docker Compose。
- 不要为了单服务器小项目默认引入 Kubernetes。
- 复用项目已有依赖,不随意增加重量级部署平台。
- 说明为什么选择 systemd、Docker Compose、Nginx、Caddy 或其他方案。
- 明确区分宿主机端口、容器端口和公网端口。
- 数据库和 Redis 不允许直接向公网开放。
- 后端端口优先只绑定 127.0.0.1 或容器内部网络。
- 公网原则上只开放 80、443 和受限的 SSH 端口。
配置管理要求:
- 开发、测试、生产配置必须分离。
- 不允许在代码、Dockerfile、Compose 文件或 Nginx 配置中硬编码生产密钥。
- 提供 .env.example,但不得写入真实密码。
- 生产 Secret 应从环境变量、服务器受限文件或现有 Secret 管理系统注入。
- 检查 .gitignore,防止真实 .env、日志、备份和密钥进入 Git。
- 不要把密钥写入 Docker 镜像构建参数或镜像层。
容器化要求:
- 根据项目技术栈创建清晰的多阶段 Dockerfile。
- 使用固定或合理约束的基础镜像版本,不盲目使用 latest。
- 以非 root 用户运行应用。
- 设置正确的工作目录、文件权限和启动命令。
- 添加健康检查或提供可供部署系统调用的健康接口。
- 处理 SIGTERM,实现优雅关闭。
- 设置合理的重启策略。
- 只挂载真正需要持久化的目录。
- 不把源码目录整体挂载到生产容器。
- 不将数据库数据保存在容器可写层。
- 不向普通应用容器挂载 Docker Socket。
Nginx 或 Caddy 要求:
- 正确配置域名。
- 前端静态文件使用生产构建产物。
- SPA 项目配置 History 路由回退。
- /api 请求转发到正确的后端地址。
- 设置 Host、X-Real-IP、X-Forwarded-For、X-Forwarded-Proto。
- 根据项目实际情况处理 WebSocket、SSE、上传大小和代理超时。
- 不允许通过修改 CORS 掩盖反向代理路径错误。
- 配置 HTTP 到 HTTPS 跳转。
- 说明证书申请方式和自动续期方式。
- 修改配置后先执行语法检查,再平滑重载。
数据库和持久化要求:
- 数据库使用独立账号,遵循最小权限原则。
- 使用项目已有迁移工具管理表结构。
- 明确迁移在发布流程中的执行时机。
- 说明数据库迁移失败时如何停止发布。
- 说明应用回滚时数据库兼容性如何处理。
- PostgreSQL、MySQL 和 Redis 的数据目录必须使用持久化 Volume 或托管服务。
- 用户上传文件优先使用现有 OSS;如使用本地目录,必须挂载到宿主持久化路径并配置备份。
- 明确说明 Volume 只解决容器重建,不等于数据库备份。
- 创建数据库定时备份方案,并将副本保存到服务器之外。
- 提供一次真实恢复测试的方法。
日志和监控要求:
- 应用日志优先输出到 stdout 和 stderr。
- 日志至少包含时间、等级、环境、服务名、Request ID、路径、状态码、耗时和异常。
- 不记录密码、Token、Cookie、AccessKey Secret 和完整身份证号。
- 配置日志大小限制和轮转,避免磁盘被写满。
- 提供查看 Nginx、应用、数据库和容器日志的命令。
- 配置健康检查接口。
- 给出最小监控方案,包括站点可用性、CPU、内存、磁盘、5xx 错误和备份失败告警。
- 如果项目已有 Prometheus、Grafana、Loki 或云监控,请优先复用。
CI/CD 要求:
- 检查当前使用 GitHub、GitLab 还是其他代码平台。
- 自动执行依赖安装、测试和构建。
- Docker 镜像使用 Git Commit 或版本号作为标签。
- 镜像推送到现有镜像仓库。
- 生产密钥只能通过 CI/CD Secret 或 Environment 注入。
- 部署前执行数据库备份和迁移检查。
- 部署后调用健康检查。
- 健康检查失败时停止发布并恢复上一个稳定版本。
- 不使用无法追踪来源的 latest 作为唯一生产版本。
- 记录发布时间、Git Commit、镜像版本和迁移版本。
安全要求:
- 创建普通部署用户,不长期使用 root 运行应用。
- SSH 优先使用公钥认证。
- 配置云安全组和 Linux 防火墙。
- 数据库和 Redis 只允许内部访问。
- 检查文件上传大小、类型和路径安全。
- 检查 CORS、CSRF、Cookie Secure、SameSite 和代理头配置。
- 检查默认账号和弱密码。
- 检查依赖漏洞和过期基础镜像。
- 不因为部署方便而使用 0.0.0.0 暴露所有内部服务。
请先输出:
1. 当前项目技术栈和运行方式。
2. 当前部署相关文件及其作用。
3. 发现的风险和缺失项。
4. 推荐的最终架构。
5. 浏览器请求的完整数据流。
6. 数据库、Redis、文件和日志的持久化方案。
7. 计划新增或修改的文件列表。
8. 实施步骤和回滚方案。
完成修改后,请说明:
1. 修改了哪些文件。
2. 每个文件承担什么职责。
3. 服务器需要安装哪些软件。
4. 服务器需要创建哪些目录和用户。
5. 需要配置哪些环境变量。
6. 需要开放哪些端口。
7. DNS 应该怎样配置。
8. HTTPS 证书怎样申请和续期。
9. 第一次部署需要执行哪些命令。
10. 后续版本怎样发布。
11. 服务怎样启动、停止、重启和查看状态。
12. 日志怎样查看和轮转。
13. 数据库怎样备份和恢复。
14. 发布失败怎样回滚。
15. 服务器重启后怎样验证所有服务自动恢复。
最终不能只验证“容器已经启动”或“首页能够打开”,还必须测试:
- 前端刷新子路由;
- API 请求和代理路径;
- 登录、权限和 Cookie;
- 数据库读写;
- Redis 连接;
- 文件上传和下载;
- 大文件和超时场景;
- HTTPS 跳转;
- WebSocket 或 SSE;
- 应用异常退出后的自动重启;
- 服务器重启后的自动恢复;
- 容器重新创建后的数据持久化;
- 数据库备份和恢复;
- 日志检索;
- 磁盘空间告警;
- 新版本发布;
- 健康检查失败回滚;
- 未授权用户无法访问内部服务。