Patet

模板与清单

复制即用的配置与清单——拿过去、替换占位符、直接上线,省去一行行从头敲的工夫。

这一篇是「复制即用」中心:把现成的配置和清单放在这里,复制过去、改掉占位符、直接发布。所有模板都尽量保持正确且精简,没有多余内容。

占位符统一用 example.com(你的域名)、/var/www/app(你的应用目录)、youruser(你的部署账号),用之前记得全局替换成自己的真实值。

工具 / Tools

下面三个交互式工具能帮你边查边做,配合本页模板一起用:

复制即用模板 / Copy-paste templates

每个模板都先用一两句说明用途,再给出可直接粘贴的代码。改完占位符就能用。

Nginx 反向代理 + HTTPS

把公网流量反向代理到本机上跑的应用(这里假设应用监听 127.0.0.1:3000),同时强制 HTTPS、加上常用安全响应头和 gzip。证书路径按 Certbot 自动签发的默认位置填写。配套阅读 接入 Cloudflare运维安全

# /etc/nginx/sites-available/example.com
# 80 端口:全部 301 跳转到 HTTPS。
# 跳转目标写死成固定的规范域名(而不是 $host),
# 避免伪造的 Host 头把它变成开放重定向。
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# 443 端口:真正提供服务
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com www.example.com;

    # Certbot 自动管理的证书路径
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # 安全响应头
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;

    # 反向代理到本机应用
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
    }
}

启用并重载:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Dockerfile(Node 应用)

多阶段构建:第一阶段装依赖并构建,第二阶段只拷贝运行所需文件,镜像更小。基于 node:20-alpine,以非 root 用户运行,只装生产依赖。

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

# ---- 运行阶段 ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# 只装生产依赖
COPY package*.json ./
RUN npm ci --omit=dev

# 拷贝构建产物
COPY --from=builder /app/dist ./dist

# 以非 root 用户运行(alpine 自带 node 用户)
USER node

EXPOSE 3000
CMD ["node", "dist/server.js"]

docker-compose.yml

一个应用服务 + 一个 Postgres 服务。Postgres 用具名卷持久化数据,并配了健康检查;应用通过 depends_on 等数据库就绪后再启动。敏感配置走 env_file(即下面的 .env不要提交进 Git)。应用端口绑定到 127.0.0.1,只让反向代理(nginx)能访问——如果写成 3000:3000 会暴露在所有网卡上,而且 Docker 自己的 iptables 规则会绕过 UFW。

services:
  app:
    build: .
    restart: unless-stopped
    ports:
      # 只绑定到本机回环;由 nginx 负责 TLS 并反代到这里
      - "127.0.0.1:3000:3000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

GitHub Actions 部署

推送到 main 分支后自动构建,再通过 SSH + rsync 把产物同步到你的服务器并重启服务。服务器地址、用户名、私钥,以及服务器的 known_hosts 都放在仓库的 Settings → Secrets and variables → Actions 里,不要写进文件。先在本地用 ssh-keyscan your.server.com 生成 SSH_KNOWN_HOSTS 的值并粘贴进 Secret,这样部署时会校验服务器身份,而不是盲目信任拿到的任意主机密钥。

name: Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: npm

      - name: Install & build
        run: |
          npm ci
          npm run build

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          # 固定服务器主机密钥(用 `ssh-keyscan your.server.com` 生成此 Secret)
          echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts

      - name: Deploy with rsync
        run: |
          # 同步到 dist/ 子目录(而非应用根目录),这样 --delete 不会删掉 .env,
          # 路径也与下面 systemd 的 ExecStart 一致。
          rsync -az --delete ./dist/ \
            "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/app/dist/"
          ssh "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
            "sudo systemctl restart myapp"

不想碰服务器?用 Cloudflare Pages

如果是静态站点或全栈框架,可以省掉 SSH,直接用 Cloudflare Pages 部署。把上面的 Deploy 步骤换成:

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy ./dist --project-name=my-project

详见 接入 Cloudflare

robots.txt

放在站点根目录,告诉搜索引擎爬虫可以抓取全站,并指向你的 sitemap。生成更完整的版本可以用 SEO 代码生成器

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

sshd_config 加固片段

禁掉 root 直接登录和密码登录,只允许密钥登录——这是服务器安全最重要的一步。改完务必先用密钥登录测试新会话再关旧会话,免得把自己锁在门外。更完整的加固见 运维安全

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 20
# 只允许指定用户登录(可选)
AllowUsers youruser

改完重载 SSH 服务:

sudo sshd -t && sudo systemctl reload ssh

.env.example

把这个文件提交进 Git 当作「字段说明书」,让协作者知道需要哪些变量;真正带密钥的 .env绝不能提交,记得加进 .gitignore

# 复制为 .env 并填入真实值
# 注意:.env 含密钥,务必加入 .gitignore,绝不要提交!

NODE_ENV=production
PORT=3000

# 数据库连接串
DATABASE_URL=postgres://youruser:CHANGE_ME@localhost:5432/myapp

# 会话 / JWT 密钥,用随机长字符串:openssl rand -hex 32
SESSION_SECRET=CHANGE_ME_to_a_long_random_string

别把真实 .env 提交进 Git

密钥一旦进了 Git 历史,即使后续删除也等于已经公开了。务必在第一次提交前就把 .env 加进 .gitignore

systemd service

把 Node 应用做成系统服务,让它开机自启、崩溃自动重启,并以非 root 用户运行。环境变量从 EnvironmentFile 读取(即上面的 .env)。

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node App
After=network.target

[Service]
Type=simple
User=youruser
Group=youruser
WorkingDirectory=/var/www/app
EnvironmentFile=/var/www/app/.env
ExecStart=/usr/bin/node /var/www/app/dist/server.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

启用并启动:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

ufw 防火墙设置

最小开放原则:只放行 SSH、HTTP、HTTPS,其余端口一律拒绝。先放行 SSH enable,否则可能把自己挡在门外。完整防护策略见 运维安全

# 默认拒绝所有入站、放行所有出站
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 放行必要端口
sudo ufw allow OpenSSH      # 22
sudo ufw allow 80/tcp       # HTTP
sudo ufw allow 443/tcp      # HTTPS

# 启用并查看状态
sudo ufw enable
sudo ufw status verbose

接下来

模板只是起点。每个主题的完整讲解在对应的指南里:

本步骤检查清单

  • 所有模板里的占位符(example.com/var/www/appyouruser)都已替换成真实值
  • .env 已加入 .gitignore,密钥没有提交进 Git
  • 部署用的 Secrets(SSH 私钥、API token)都配在平台里,没有写死在仓库文件中
  • sshd_config 后,先用新会话验证能登录,再关掉旧会话
  • 启用 ufw 前已先放行 SSH,确认没把自己锁在门外

完整清单见 上线自查清单

本页目录