记录个人博客的服务端部署配置,包括 Nginx 反代、Docker 容器、SSL 证书自动续期、MySQL 和 Redis。整套方案跑在一台单机上,用 Nginx 替代 K8S 做流量分发。
主机上直接装 Nginx 做反代,把流量分给博客前端、NestJS 后端和 Portainer 管理面板。
yum install nginx
配置文件 zefey.conf:
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream blog {
server 127.0.0.1:8618;
keepalive 64;
}
upstream blog-nestjs {
server 127.0.0.1:8619;
keepalive 64;
}
upstream portainer {
server 127.0.0.1:9000;
keepalive 64;
}
server {
listen 80;
server_name www.zefey.com zefey.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.zefey.com zefey.com;
server_tokens off;
ssl_certificate "/etc/pki/nginx/fullchain.cer";
ssl_certificate_key "/etc/pki/nginx/zefey.com.key";
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location /portainer {
rewrite ^/portainer/(.*)$ /$1 break;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://portainer;
}
location /api {
client_max_body_size 50m;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://blog-nestjs;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://blog;
}
}
$connection_upgrade),管理 Docker 容器时需要实时通信client_max_body_size 50mproxy_hide_header 隐藏服务端信息,减少暴露面gzip_types 覆盖了 JSON、JS、CSS、SVG 等常见前端资源,不加的话默认只压缩 text/htmlssl_session_cache 减少握手开销HSTS 强制浏览器走 HTTPS、X-Frame-Options 防止被嵌套、X-Content-Type-Options 防 MIME 嗅探博客前端和 NestJS 后端都跑在 Docker 里,Portainer 用来可视化管理容器。使用 docker-compose 统一编排,迁移时带走一个文件就行。
version: '3.8'
services:
blog:
image: zefey/blog:1.0.6
ports:
- "8618:80"
restart: unless-stopped
blog-nestjs:
image: blog-nestjs:1.0.9
ports:
- "8619:3000"
volumes:
- /var/log/blog:/app/logs
restart: unless-stopped
mysql:
image: mysql:8.0
ports:
- "127.0.0.1:3306:3306"
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: blog
volumes:
- mysql_data:/var/lib/mysql
- /backup/mysql:/backup
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379"
command: redis-server --requirepass your_password --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
portainer:
image: portainer/portainer-ce:lts
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
restart: always
volumes:
portainer_data:
mysql_data:
redis_data:
docker compose up -d
unless-stopped,手动 stop 不会被拉起,只有意外退出才重启127.0.0.1,外部无法直连,只能通过 Nginx 反代访问,避免管理面板暴露在公网127.0.0.1,只有本机容器和宿主机能访问,外部不可达mysql_data 卷,挂载 /backup/mysql 目录方便容器内 dump 备份appendonly 持久化,数据存到 redis_data 卷,重启不丢数据redis:7-alpine 体积只有 30MB 左右,个人站够用如果不想用 compose,也可以单独启动:
docker run -dp 8618:80 --name blog --restart=unless-stopped zefey/blog:1.0.6
docker run -dp 8619:3000 --name blog-nestjs --restart=unless-stopped -v /var/log/blog:/app/logs blog-nestjs:1.0.9
docker volume create mysql_data
docker run -d \
-p 127.0.0.1:3306:3306 \
--name mysql --restart=unless-stopped \
-e MYSQL_ROOT_PASSWORD=your_password \
-e MYSQL_DATABASE=blog \
-v mysql_data:/var/lib/mysql \
-v /backup/mysql:/backup \
mysql:8.0 \
--default-authentication-plugin=mysql_native_password \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
docker volume create redis_data
docker run -d \
-p 127.0.0.1:6379:6379 \
--name redis --restart=unless-stopped \
-v redis_data:/data \
redis:7-alpine \
redis-server --requirepass your_password --appendonly yes
docker volume create portainer_data
docker run -d \
-p 127.0.0.1:9000:9000 \
-p 127.0.0.1:9443:9443 \
--name portainer --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
用 acme.sh 通过阿里云 DNS 验证申请 Let's Encrypt 证书,支持泛域名。证书到期前会自动续期并重启 Nginx。
申请证书:
acme.sh --issue --dns dns_ali -d zefey.com -d *.zefey.com
安装证书到 Nginx 指定路径,续期后自动重启:
acme.sh --install-cert -d zefey.com \
--key-file /etc/pki/nginx/zefey.com.key \
--fullchain-file /etc/pki/nginx/fullchain.cer \
--reloadcmd "systemctl restart nginx"
顺便把证书部署到七牛云 CDN:
acme.sh --deploy -d zefey.com --deploy-hook qiniu
MySQL 和 Redis 已容器化,统一在 docker-compose 中管理。
mysql:8.0 官方镜像,稳定且长期维护command 指定了 utf8mb4 字符集,避免中文和 emoji 存储问题mysql_data,容器删了数据还在MYSQL_ROOT_PASSWORD 初始化 root 密码并创建 MYSQL_DATABASE 指定的库连接方式(NestJS 后端 .env):
DB_HOST=mysql
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=your_password
DB_DATABASE=blog
容器间通过服务名 mysql 直连,不走宿主机端口。宿主机上也可以通过 127.0.0.1:3306 连接,方便本地用 Navicat 等工具调试。
redis:7-alpine,体积小(~30MB)性能足够--requirepass 设置密码,--appendonly yes 开启 AOF 持久化redis_data 卷连接方式:
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=your_password
同样容器间走服务名,宿主机走 127.0.0.1:6379。
如果之前 MySQL 有数据,需要先 dump 再导入:
# 1. 宿主机上导出
mysqldump -u root -p --all-databases > /backup/full_dump.sql
# 2. 启动容器后导入
docker compose up -d mysql
docker compose exec mysql mysql -u root -p'your_password' < /backup/full_dump.sql
# 3. 确认数据无误后关掉宿主机的 MySQL
systemctl stop mysql
systemctl disable mysql
Redis 如果只做缓存没有持久化需求,直接启动容器即可,缓存会自动重建。
单机部署最怕磁盘挂了数据全丢。MySQL 容器化后,备份命令通过 docker compose exec 执行:
# 每天凌晨 3 点备份,保留最近 7 天
0 3 * * * docker compose -f /path/to/docker-compose.yml exec -T mysql mysqldump -u root -p'your_password' --all-databases | gzip > /backup/mysql_$(date +\%F).sql.gz && find /backup -name "mysql_*.sql.gz" -mtime +7 -delete
可以再同步一份到对象存储(七牛 qshell 或阿里 ossutil),异地备份才是真备份。
Docker named volume 的数据也可以直接备份:
# 备份 mysql_data 卷
docker run --rm -v mysql_data:/source -v /backup:/target alpine tar czf /target/mysql_volume_$(date +%F).tar.gz -C /source .