速查手册

出问题时按「我干了什么」找,不用背命令

🌐 Nginx 常用片段 (6)

反向代理、跨域、gzip、SPA 兜底 —— 都是「配了却没生效」的重灾区

反向代理到本地上游

proxy_pass 末尾带不带斜杠,路径拼接结果完全不同:带 / 会剥掉 location 前缀。

location /api/ {
    proxy_pass http://127.0.0.1:8080/;   # 末尾 / → 剥掉 /api 前缀
    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;
}

跨域 CORS(含预检)

预检请求是 OPTIONS,必须在 location 里显式 return 204,否则 405 会让浏览器直接判失败。

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin  $http_origin always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
        add_header Access-Control-Max-Age 86400 always;
        return 204;
    }
    add_header Access-Control-Allow-Origin $http_origin always;
    add_header Access-Control-Allow-Credentials true always;
}

gzip 压缩

gzip 对已压缩格式(png/jpg/woff2)没意义,还会浪费 CPU;只在 nginx 直接返回文本时才有明显收益。

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/json
           application/xml image/svg+xml;

SPA 兜底(且不要把目录当文件)

try_files 的 $uri/ 一旦命中真实目录就交给 index 模块,目录里没有 index.html 会直接 403,不会回退。

root /var/www/app/dist;
index index.html;

location / {
    try_files $uri $uri/ /index.html;
}

# 静态资源加长缓存(文件名带 hash 才敢这么写)
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

HTTPS 与裸域跳转

裸域跳 www 必须在 80 和 443 两个 server 块里都写,只写 80 会让 https 访问裸域落到证书错误。

server {
    listen 80;
    server_name example.com www.example.com;
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/acme;      # 放 dist 之外,避免部署时被 --delete 删掉
        try_files $uri =404;
    }
    location / { return 301 https://www.example.com$request_uri; }
}

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/www.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
    return 301 https://www.example.com$request_uri;
}

安全响应头

这些头是否存在,用响应头分析工具粘贴检查即可,不必自己写脚本。

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CSP 一开始别写太严,先 report-only 观察
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

🐳 Dockerfile 常用写法 (4)

多阶段构建、非 root、时区 —— 影响镜像体积与线上行为的三件事

多阶段构建(Node → Nginx)

构建依赖留在 builder 阶段,最终镜像只有静态产物 + nginx,体积能从 1GB+ 降到 50MB 级。

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

非 root 运行

容器默认是 root。加一行 USER 就能避免容器逃逸后直接拿到宿主机 root。

RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --chown=app:app . .
USER app

让层缓存真正生效

先 COPY 依赖清单再装依赖,最后才 COPY 源码 —— 改代码时不会让依赖层失效。

# 好:依赖层可复用
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# 坏:一行 COPY . . 会让任何代码改动都重装依赖
# COPY . .
# RUN npm ci

时区与 locale

Alpine 默认 UTC,日志时间比本地时间差 8 小时是常见困惑来源。

RUN apk add --no-cache tzdata \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone
ENV TZ=Asia/Shanghai

🔧 Git 翻车恢复 (8)

按「我干了什么蠢事」组织。先想清楚你做了什么,再挑对应那条。

我把提交打到错误的分支上了

先在新分支上留住提交,再把原分支退回去。cherry-pick 比 reset 安全,因为不会丢东西。

git branch feature-x            # 在当前提交处建分支
git reset --hard origin/main    # 原分支退回远端状态
git switch feature-x            # 提交已经在 feature-x 上了

我 reset --hard 之后发现丢东西了

被 reset 掉的提交不会立刻消失,reflog 里还有。找到那一条再 reset 回去。

git reflog                    # 找到 reset 之前的那条 HEAD@{n}
git reset --hard HEAD@{3}     # 退回到那一刻
# 只想捞回某个文件:
git checkout HEAD@{3} -- path/to/file

rebase 冲突太多,我想放弃

rebase 中途退出有两个方向:回到开始前,或者跳过这个提交继续。

git rebase --abort     # 完全放弃,回到 rebase 之前
git rebase --skip      # 跳过当前这个冲突的提交,继续
git rebase --continue  # 解决完冲突后继续

我 rebase 完 push 被拒绝了

rebase 改写了历史,普通 push 必然被拒。用 --force-with-lease:它会在远端被别人更新过时拒绝推送,比 --force 安全得多。

git push --force-with-lease origin feature-x
# 不要用 --force:它会把别人刚推的提交一起覆盖掉

分支删了才发现还要用

本地删除后还能从 reflog 恢复;如果从未推送过,尽快操作,gc 之后会真的没了。

git reflog                                   # 找分支最后一次提交
git branch recovered-branch <commit-ish>     # 用它重建分支

我 stash 之后忘了 pop

stash 不会丢,列出全部再挑那条恢复,不需要记住编号顺序。

git stash list
git stash show -p stash@{2}    # 先看内容确认是那一条
git stash pop stash@{2}        # 恢复并从栈中删除
git stash apply stash@{2}      # 恢复但保留在栈里(更保险)

我提交了密钥 / 密码

最重要的一条:把 key 换掉。历史可以改写,但泄露过的 key 必须视为已泄露。删除历史只防止后续扩散。

# 1) 先去平台把该 key 作废并重新生成(这一步不能省)
# 2) 再从历史里移除(需要强推,会改写所有协作者的克隆)
git filter-repo --path .env --invert-paths
# 或只从最新提交移除,后续提交补 .gitignore
git rm --cached .env && echo ".env" >> .gitignore && git commit -m "chore: remove .env"

merge 之后我的改动被覆盖了

merge 冲突时选了 theirs 就会丢掉自己的改动。用 reflog 回到 merge 之前重做,而不是手工再改一遍。

git reflog                       # 找 merge 之前的 HEAD
git reset --hard HEAD@{1}        # 回到 merge 前
git merge --no-commit origin/x   # 重新 merge,冲突逐个看清再解