チートシート

コマンド名ではなく「何をしたか」から探せます

🌐 Nginx snippets (6)

Reverse proxy, CORS, gzip, SPA fallback — the usual "configured but not working" traps

Reverse proxy to a local upstream

A trailing slash on proxy_pass changes path joining: with "/", the location prefix is stripped.

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 with preflight

Preflight is an OPTIONS request; return 204 explicitly or a 405 makes the browser fail the request.

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 compression

gzip is useless for already-compressed formats (png/jpg/woff2) and wastes CPU; it only helps for text served directly.

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 fallback without breaking directories

Once $uri/ matches a real directory, index handling takes over — no index.html there means 403, no fallback.

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 and apex redirect

Redirect apex→www in both the :80 and :443 blocks; doing only :80 leaves HTTPS apex on a certificate error.

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;
}

Security headers

Check their presence by pasting the response into the header analyzer — no need to script it.

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 patterns (4)

Multi-stage builds, non-root, timezone — the three things that change image size and runtime behavior

Multi-stage build (Node → Nginx)

Build deps stay in the builder stage; the final image only carries the static output plus nginx.

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;"]

Run as non-root

Containers default to root; one USER line prevents host root after a container escape.

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

Make layer caching work

Copy manifests, install deps, then copy source — editing code no longer busts the dependency layer.

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

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

Timezone and locale

Alpine defaults to UTC; log timestamps eight hours off local time is a common surprise.

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

🔧 Git recovery (8)

Organized by "what did I just do". Figure out the mistake first, then pick the matching fix.

I committed to the wrong branch

Save the commit on a new branch, then rewind the original. cherry-pick is safer than reset.

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

I lost work after reset --hard

Reset commits live on in the reflog for a while. Find the entry and reset back.

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

Rebase has too many conflicts, I want out

Two ways out: return to the pre-rebase state, or skip this commit and continue.

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

Push rejected after rebase

Rebase rewrites history so a plain push is rejected. Use --force-with-lease — it refuses if someone else pushed meanwhile.

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

I deleted a branch I still need

Locally deleted branches can be recovered from reflog; if never pushed, act fast before gc.

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

I stashed and forgot to pop

Stashes are never lost — list them all and pick the right one; order does not matter.

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

I committed a secret

Rotate the key first. History rewriting only limits further spread — treat the leaked key as compromised.

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

My changes got overwritten by a merge

Choosing "theirs" during conflicts discards your work. Use reflog to return before the merge and redo it.

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