docs: use ubuntu for staging bootstrap

This commit is contained in:
Jesse_Chen
2026-07-20 15:31:36 +08:00
parent 7a4c278886
commit 3959ff2b3a
2 changed files with 84 additions and 65 deletions
@@ -17,7 +17,7 @@
- Runtime environment file is exactly `/opt/jyotisha-staging/.env.staging` with mode `0600`. - Runtime environment file is exactly `/opt/jyotisha-staging/.env.staging` with mode `0600`.
- SSH, Supabase, database, and model-provider credentials must be staging-specific. - SSH, Supabase, database, and model-provider credentials must be staging-specific.
- Do not expose container ports `3000` or `5200` on the host. - Do not expose container ports `3000` or `5200` on the host.
- Do not disable root/password SSH until deploy-key login succeeds in a second terminal. - Do not disable password SSH until both `ubuntu` admin-key login and `deploy` deploy-key login succeed in separate terminals.
- Do not execute a database reset against any linked remote project. - Do not execute a database reset against any linked remote project.
--- ---
@@ -25,7 +25,7 @@
## File and Control-Plane Map ## File and Control-Plane Map
- Cloud provider console: OS image, rescue console, security group. - Cloud provider console: OS image, rescue console, security group.
- Local Mac: `~/.ssh/jyotisha-staging` and `~/.ssh/jyotisha-staging.pub`. - Local Mac: deploy pair `~/.ssh/jyotisha-staging*` and admin pair `~/.ssh/jyotisha-staging-admin*`.
- VPS: `/home/deploy/.ssh/authorized_keys`, `/etc/ssh/sshd_config.d/60-jyotisha-staging.conf`, `/etc/docker/daemon.json`, `/opt/jyotisha-staging/.env.staging`. - VPS: `/home/deploy/.ssh/authorized_keys`, `/etc/ssh/sshd_config.d/60-jyotisha-staging.conf`, `/etc/docker/daemon.json`, `/opt/jyotisha-staging/.env.staging`.
- DNS provider: `A staging.jyotisha.chat -> 118.26.111.127`. - DNS provider: `A staging.jyotisha.chat -> 118.26.111.127`.
- Supabase Dashboard: a new staging project, Auth URL configuration, staging credentials. - Supabase Dashboard: a new staging project, Auth URL configuration, staging credentials.
@@ -75,10 +75,10 @@ Expected: a provider-console screenshot or rule list contains only the four inte
From the local Mac: From the local Mac:
```bash ```bash
ssh root@118.26.111.127 ssh ubuntu@118.26.111.127
``` ```
Expected: a first-use host-key prompt followed by the provider's root-password prompt, then a root shell. Do not send the root password in chat, GitHub, or shell history. Expected: a first-use host-key prompt followed by the provider's `ubuntu` password prompt, then an Ubuntu shell. Do not send the password in chat, GitHub, or shell history.
- [ ] **Step 4: Confirm machine identity from the server** - [ ] **Step 4: Confirm machine identity from the server**
@@ -106,68 +106,86 @@ at least 40 GB root disk
### Task 2: Create and Verify the Dedicated Deploy Identity ### Task 2: Create and Verify the Dedicated Deploy Identity
**Interfaces:** **Interfaces:**
- Consumes: Initial root access from Task 1. - Consumes: Initial `ubuntu` access from Task 1.
- Produces: `deploy@118.26.111.127` authenticated by `~/.ssh/jyotisha-staging`. - Produces: `ubuntu@118.26.111.127` authenticated by the admin key and `deploy@118.26.111.127` authenticated by the deploy key.
- [ ] **Step 1: Generate a dedicated key on the local Mac** - [ ] **Step 1: Generate separate admin and deploy keys on the local Mac**
Run locally, not on the VPS: Run locally, not on the VPS:
```bash ```bash
test ! -e "$HOME/.ssh/jyotisha-staging-admin"
test ! -e "$HOME/.ssh/jyotisha-staging" test ! -e "$HOME/.ssh/jyotisha-staging"
ssh-keygen -t ed25519 -a 64 -N '' -f "$HOME/.ssh/jyotisha-staging-admin" -C "jyotisha-staging-admin"
ssh-keygen -t ed25519 -a 64 -N '' -f "$HOME/.ssh/jyotisha-staging" -C "github-actions-jyotisha-staging" ssh-keygen -t ed25519 -a 64 -N '' -f "$HOME/.ssh/jyotisha-staging" -C "github-actions-jyotisha-staging"
chmod 600 "$HOME/.ssh/jyotisha-staging-admin"
chmod 600 "$HOME/.ssh/jyotisha-staging" chmod 600 "$HOME/.ssh/jyotisha-staging"
chmod 644 "$HOME/.ssh/jyotisha-staging-admin.pub"
chmod 644 "$HOME/.ssh/jyotisha-staging.pub" chmod 644 "$HOME/.ssh/jyotisha-staging.pub"
ssh-keygen -lf "$HOME/.ssh/jyotisha-staging-admin.pub"
ssh-keygen -lf "$HOME/.ssh/jyotisha-staging.pub" ssh-keygen -lf "$HOME/.ssh/jyotisha-staging.pub"
``` ```
Expected: the first command exits successfully, two key files are created, and the fingerprint uses `ED25519`. This no-passphrase key is dedicated to the staging deploy user and GitHub Environment; it must never be used for production or copied to another host. Expected: four key files are created and both fingerprints use `ED25519`. The admin private key remains only on the Mac. The deploy private key is later stored only in the GitHub `staging` Environment and must never be used for production.
- [ ] **Step 2: Create the deploy user on the VPS** - [ ] **Step 2: Install only the admin public key on the ubuntu account**
Run in the root SSH session: Run locally and type the server password only at the terminal prompt:
```bash ```bash
adduser --disabled-password --gecos "" deploy ssh-copy-id -i "$HOME/.ssh/jyotisha-staging-admin.pub" ubuntu@118.26.111.127
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh ssh -i "$HOME/.ssh/jyotisha-staging-admin" -o IdentitiesOnly=yes ubuntu@118.26.111.127 'id && sudo -n true'
install -d -m 755 -o deploy -g deploy /opt/jyotisha-staging ```
Expected: the first command installs the public key; the second logs in as `ubuntu`. `sudo -n true` must exit `0`; if the provider requires a sudo password, keep the interactive admin session open and use `sudo` with the password typed directly at its prompt.
- [ ] **Step 3: Create the deploy user on the VPS**
Log in with the admin key and run:
```bash
ssh -i "$HOME/.ssh/jyotisha-staging-admin" -o IdentitiesOnly=yes ubuntu@118.26.111.127
sudo adduser --disabled-password --gecos "" deploy
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -d -m 755 -o deploy -g deploy /opt/jyotisha-staging
``` ```
Expected: Expected:
```bash ```bash
id deploy id deploy
stat -c '%U %G %a %n' /home/deploy/.ssh /opt/jyotisha-staging sudo stat -c '%U %G %a %n' /home/deploy/.ssh /opt/jyotisha-staging
``` ```
The output shows user `deploy`, `.ssh` mode `700`, and `/opt/jyotisha-staging` owned by `deploy`. The output shows user `deploy`, `.ssh` mode `700`, and `/opt/jyotisha-staging` owned by `deploy`.
- [ ] **Step 3: Copy only the public key to the VPS** - [ ] **Step 4: Copy only the deploy public key to the VPS**
From a second local terminal: From a second local terminal:
```bash ```bash
scp "$HOME/.ssh/jyotisha-staging.pub" root@118.26.111.127:/tmp/jyotisha-staging.pub scp -i "$HOME/.ssh/jyotisha-staging-admin" -o IdentitiesOnly=yes \
"$HOME/.ssh/jyotisha-staging.pub" ubuntu@118.26.111.127:/tmp/jyotisha-staging.pub
``` ```
Then in the root VPS session: Then in the authenticated `ubuntu` session:
```bash ```bash
install -m 600 -o deploy -g deploy /tmp/jyotisha-staging.pub /home/deploy/.ssh/authorized_keys sudo install -m 600 -o deploy -g deploy /tmp/jyotisha-staging.pub /home/deploy/.ssh/authorized_keys
shred -u /tmp/jyotisha-staging.pub sudo shred -u /tmp/jyotisha-staging.pub
``` ```
Expected: Expected:
```bash ```bash
stat -c '%U %G %a %n' /home/deploy/.ssh/authorized_keys sudo stat -c '%U %G %a %n' /home/deploy/.ssh/authorized_keys
``` ```
Output: `deploy deploy 600 /home/deploy/.ssh/authorized_keys`. Output: `deploy deploy 600 /home/deploy/.ssh/authorized_keys`.
- [ ] **Step 4: Verify deploy-key login in a new terminal** - [ ] **Step 5: Verify deploy-key login in a new terminal**
Keep the root session open. From the local Mac: Keep the `ubuntu` admin session open. From the local Mac:
```bash ```bash
ssh -i "$HOME/.ssh/jyotisha-staging" -o IdentitiesOnly=yes deploy@118.26.111.127 'id && hostname' ssh -i "$HOME/.ssh/jyotisha-staging" -o IdentitiesOnly=yes deploy@118.26.111.127 'id && hostname'
@@ -175,12 +193,12 @@ ssh -i "$HOME/.ssh/jyotisha-staging" -o IdentitiesOnly=yes deploy@118.26.111.127
Expected: exit code `0`; output contains `uid=` for `deploy`. Do not continue if this fails. Expected: exit code `0`; output contains `uid=` for `deploy`. Do not continue if this fails.
- [ ] **Step 5: Pin and compare the server host key** - [ ] **Step 6: Pin and compare the server host key**
On the VPS root session: On the VPS `ubuntu` session:
```bash ```bash
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
``` ```
On the local Mac: On the local Mac:
@@ -200,34 +218,34 @@ Expected: both fingerprints are identical. Preserve the exact line in `/tmp/jyot
- [ ] **Step 1: Install base administration packages** - [ ] **Step 1: Install base administration packages**
Run as root on the VPS: Run from the authenticated `ubuntu` session with `sudo`:
```bash ```bash
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update sudo apt-get update
apt-get dist-upgrade -y sudo apt-get dist-upgrade -y
apt-get install -y ca-certificates curl git rsync ufw unattended-upgrades sudo apt-get install -y ca-certificates curl git rsync ufw unattended-upgrades
hostnamectl set-hostname jyotisha-staging sudo hostnamectl set-hostname jyotisha-staging
timedatectl set-timezone UTC sudo timedatectl set-timezone UTC
systemctl enable --now unattended-upgrades sudo systemctl enable --now unattended-upgrades
``` ```
Expected: all commands exit `0` and `hostnamectl --static` prints `jyotisha-staging`. Expected: all commands exit `0` and `hostnamectl --static` prints `jyotisha-staging`.
- [ ] **Step 2: Create swap only if the VPS has none** - [ ] **Step 2: Create swap only if the VPS has none**
Run as root: Run with `sudo`:
```bash ```bash
if [ "$(swapon --noheadings | wc -l)" -eq 0 ]; then if [ "$(swapon --noheadings | wc -l)" -eq 0 ]; then
fallocate -l 4G /swapfile sudo fallocate -l 4G /swapfile
chmod 600 /swapfile sudo chmod 600 /swapfile
mkswap /swapfile sudo mkswap /swapfile
swapon /swapfile sudo swapon /swapfile
printf '%s\n' '/swapfile none swap sw 0 0' >> /etc/fstab printf '%s\n' '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab >/dev/null
fi fi
sysctl vm.swappiness=10 sudo sysctl vm.swappiness=10
printf '%s\n' 'vm.swappiness=10' > /etc/sysctl.d/60-jyotisha-staging.conf printf '%s\n' 'vm.swappiness=10' | sudo tee /etc/sysctl.d/60-jyotisha-staging.conf >/dev/null
``` ```
Expected: Expected:
@@ -242,27 +260,27 @@ Output shows one 4 GB swap file and one matching `fstab` entry.
- [ ] **Step 3: Configure UFW before enabling it** - [ ] **Step 3: Configure UFW before enabling it**
Run as root: Run with `sudo`:
```bash ```bash
ufw default deny incoming sudo ufw default deny incoming
ufw default allow outgoing sudo ufw default allow outgoing
ufw allow 22/tcp comment 'SSH' sudo ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP' sudo ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS' sudo ufw allow 443/tcp comment 'HTTPS'
ufw allow 443/udp comment 'HTTP3' sudo ufw allow 443/udp comment 'HTTP3'
ufw --force enable sudo ufw --force enable
ufw status verbose sudo ufw status verbose
``` ```
Expected: UFW is active; only `22/tcp`, `80/tcp`, `443/tcp`, and `443/udp` are allowed. Docker-published ports must still be reviewed separately because Docker can bypass UFW; the application Compose file may publish only 80/443. Expected: UFW is active; only `22/tcp`, `80/tcp`, `443/tcp`, and `443/udp` are allowed. Docker-published ports must still be reviewed separately because Docker can bypass UFW; the application Compose file may publish only 80/443.
- [ ] **Step 4: Harden SSH with a configuration snippet** - [ ] **Step 4: Harden SSH with a configuration snippet**
Run as root: Run with `sudo`:
```bash ```bash
install -m 600 /dev/null /etc/ssh/sshd_config.d/60-jyotisha-staging.conf sudo install -m 600 /dev/null /etc/ssh/sshd_config.d/60-jyotisha-staging.conf
printf '%s\n' \ printf '%s\n' \
'PubkeyAuthentication yes' \ 'PubkeyAuthentication yes' \
'PasswordAuthentication no' \ 'PasswordAuthentication no' \
@@ -270,23 +288,24 @@ printf '%s\n' \
'PermitRootLogin no' \ 'PermitRootLogin no' \
'X11Forwarding no' \ 'X11Forwarding no' \
'MaxAuthTries 3' \ 'MaxAuthTries 3' \
> /etc/ssh/sshd_config.d/60-jyotisha-staging.conf | sudo tee /etc/ssh/sshd_config.d/60-jyotisha-staging.conf >/dev/null
sshd -t sudo sshd -t
systemctl reload ssh sudo systemctl reload ssh
``` ```
Expected: `sshd -t` emits nothing and exits `0`. Expected: `sshd -t` emits nothing and exits `0`.
- [ ] **Step 5: Re-test access before closing the root session** - [ ] **Step 5: Re-test access before closing the original password session**
From the local Mac: From the local Mac:
```bash ```bash
ssh -i "$HOME/.ssh/jyotisha-staging-admin" -o IdentitiesOnly=yes ubuntu@118.26.111.127 'printf "admin-key-ok\n"'
ssh -i "$HOME/.ssh/jyotisha-staging" -o IdentitiesOnly=yes deploy@118.26.111.127 'printf "deploy-key-ok\n"' ssh -i "$HOME/.ssh/jyotisha-staging" -o IdentitiesOnly=yes deploy@118.26.111.127 'printf "deploy-key-ok\n"'
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=0 root@118.26.111.127 true ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=0 ubuntu@118.26.111.127 true
``` ```
Expected: the first command prints `deploy-key-ok`. The second command is rejected. Only now close the original root session. Expected: the first two commands print `admin-key-ok` and `deploy-key-ok`. The password-only command is rejected. Only now close the original password-authenticated session.
### Task 4: Install Docker and Bound Its Disk Usage ### Task 4: Install Docker and Bound Its Disk Usage
@@ -296,7 +315,7 @@ Expected: the first command prints `deploy-key-ok`. The second command is reject
- [ ] **Step 1: Install Docker from Docker's official apt repository** - [ ] **Step 1: Install Docker from Docker's official apt repository**
Use the provider's authenticated rescue/console root session. The `deploy` user intentionally has no general sudo access. Run: Use the authenticated `ubuntu` admin-key session and enter `sudo -i`. The `deploy` user intentionally has no general sudo access. Run:
```bash ```bash
apt-get update apt-get update
@@ -321,7 +340,7 @@ Expected: packages come from `download.docker.com`; no convenience `curl | sh` i
- [ ] **Step 2: Configure bounded Docker logs** - [ ] **Step 2: Configure bounded Docker logs**
Run as root on the fresh server: Run in the `sudo -i` admin shell on the fresh server:
```bash ```bash
printf '%s\n' \ printf '%s\n' \
@@ -340,7 +359,7 @@ Expected: `docker info --format '{{.LoggingDriver}}'` prints `local`.
- [ ] **Step 3: Allow the deploy user to run Docker** - [ ] **Step 3: Allow the deploy user to run Docker**
Run as root: Run in the `sudo -i` admin shell:
```bash ```bash
usermod -aG docker deploy usermod -aG docker deploy
@@ -593,10 +612,10 @@ Expected: 22 is reachable. Before application deployment, 80/443 may refuse beca
- [ ] **Step 3: Reboot once and verify the bootstrap survives** - [ ] **Step 3: Reboot once and verify the bootstrap survives**
From the provider's authenticated console root session: From the authenticated `ubuntu` admin-key session:
```bash ```bash
systemctl reboot sudo systemctl reboot
``` ```
Wait for the provider console to report the VPS online, then run locally: Wait for the provider console to report the VPS online, then run locally:
@@ -613,6 +632,7 @@ Expected: deploy-key login works after reboot, swap is present, Docker is `activ
Go only if all are true: Go only if all are true:
```text ```text
ubuntu admin key works
deploy key works deploy key works
root/password SSH is disabled root/password SSH is disabled
host-key fingerprints match host-key fingerprints match
@@ -25,7 +25,7 @@ staging 不得写入生产数据库,不得复用 service-role key、数据库
## 服务器基础设计 ## 服务器基础设计
- 操作系统:Ubuntu 24.04 LTS x86_64。 - 操作系统:Ubuntu 24.04 LTS x86_64。
- 访问:首次通过供应商 root 凭据进入;建立 `deploy` 用户和独立 Ed25519 key;验证 key 登录后再关闭 SSH 密码登录和直接 root 登录。 - 访问:供应商默认 SSH 用户为 `ubuntu`;本机管理密钥只授权给 `ubuntu`GitHub deploy 密钥只授权给 `deploy`。两个 key 登录都验证成功后再关闭 SSH 密码登录和直接 root 登录。
- 内存:2 vCPU / 4 GB RAM,增加 4 GB swapstaging 部署串行执行,避免构建峰值并发。 - 内存:2 vCPU / 4 GB RAM,增加 4 GB swapstaging 部署串行执行,避免构建峰值并发。
- 防火墙:只开放 SSH、80、443Python API 5200 和 Next.js 3000 只在 Docker 网络暴露。 - 防火墙:只开放 SSH、80、443Python API 5200 和 Next.js 3000 只在 Docker 网络暴露。
- 运行时:Docker Engine、Buildx 和 Compose plugin,从 Docker 官方 apt repository 安装。 - 运行时:Docker Engine、Buildx 和 Compose plugin,从 Docker 官方 apt repository 安装。
@@ -89,7 +89,7 @@ push staging
服务器基础验收: 服务器基础验收:
- deploy key 登录成功,密码/root 登录按设计受限; - ubuntu admin key 与 deploy key 分别登录成功,密码/root 登录按设计受限;
- UFW 与云防火墙只开放预期端口; - UFW 与云防火墙只开放预期端口;
- Docker/Compose 正常;swap 生效;重启后容器能恢复。 - Docker/Compose 正常;swap 生效;重启后容器能恢复。
@@ -128,4 +128,3 @@ push staging
- 不购买或部署国内后端服务器。 - 不购买或部署国内后端服务器。
- 不在本机运行大模型。 - 不在本机运行大模型。
- 不在 staging 和 production 之间做应用双写。 - 不在 staging 和 production 之间做应用双写。