將新的 VPS 設定為生產環境(適用於新手)
概觀
此技能逐步引導您將乾淨的 Ubuntu 伺服器準備好投入生產環境,使其能夠以最大自主性進行維護。目標受眾是沒有系統管理經驗的使用者。因此:
- 安全的預設值,而非「專家設定」。不採用 NOPASSWD-sudo,也不為了方便而停用檢查。對專家來說簡化生活的事,對新手可能是個陷阱。
- 透過自動化實現自主性。安全性更新、備份和監控應自動運行。使用者只需對警報做出反應。
- 每個步驟都需驗證。變更後必須檢查結果,然後才能進行下一步。
目標作業系統: Ubuntu 22.04 / 24.04 LTS。其他發行版的命令會有所不同。
黃金法則(在任何變更之前)
- 備份設定檔:
sudo cp /path/config /path/config.bak.$(date +%F-%H%M) - 檢查狀態:
sudo systemctl status <service>/sudo docker ps - 變更後檢查結果(日誌、狀態、測試連線)。
- 在確認新 SSH 工作階段中的存取權限和 sudo 能正常運作之前,切勿關閉當前的 SSH 工作階段。失去對遠端伺服器的 SSH 連線等於必須使用供應商提供的控制台(VNC)進行連線。
生產環境安全性原則(在所有步驟中遵守):
- 機密資訊(API 金鑰、令牌、密碼)只能放在
.env檔案或環境變數中,絕對不能寫在程式碼裡。 .env檔案必須加入.gitignore(否則機密資訊會在首次提交時洩漏到 Git)。- 資料庫不應暴露在網際網路上 — 僅限於 localhost 或透過私有網路/VPN。
- 伺服器上不應啟用除錯模式:
DEBUG=False、NODE_ENV=production。 - 應用程式不應以 root 身分執行 — 應使用專用使用者或容器。
- 不應有不必要的開放埠口(管理頁面、儀表板、監控指標應關閉或僅限信任的 IP 存取)。
- 不要為了方便而停用安全檢查(
verify=False、allowAll— 僅限於本地端使用)。 - SSH 僅允許金鑰登入,不允許密碼登入。
0. 診斷 — 現有配置
請優先執行此步驟。 主機商提供的伺服器通常已有部分配置(如 cloud-init、ufw 等)。
# 系統
lsb_release -a && uname -r
free -h && df -h && nproc
# 我是誰以及權限
whoami && id
# SSH 設定(檢查實際生效的值,而不僅僅是設定檔!)
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|pubkeyauthentication|^port '
# 防火牆
sudo ufw status verbose
# 監聽中的埠口
sudo ss -tlnp
# 自動更新(已啟用?)
systemctl status unattended-upgrades 2>/dev/null | head -5
cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null
# Docker / fail2ban
sudo docker ps 2>/dev/null
sudo fail2ban-client status 2>/dev/null
診斷完成後,向使用者報告發現的內容,並在獲得確認後才開始進行設定。
1. 初始設定與安全強化
請參考第 0 節 — 部分設定可能已完成。不要重複執行。
1.1 更新系統
sudo apt update && sudo apt upgrade -y
sudo apt autoremove -y
# 如果核心已更新 — 需要重新啟動(請參閱輸出結果)。請有意識地重新啟動:
# sudo reboot # 然後在約 30-60 秒後重新連線
1.2 建立專用的 sudo 使用者(不要用 root 身分操作)
sudo adduser <USERNAME> # 會要求設定密碼 — 選擇一個強密碼並保存在密碼管理員中
sudo usermod -aG sudo <USERNAME>
對於新手,我們保留 sudo 需要輸入密碼的設定。 這是刻意的:NOPASSWD-sudo 會在發生金鑰或工作階段洩漏時移除最後一道防線。sudo 密碼也作為備用方案,以防 SSH 金鑰遺失(請參閱 1.3)。
1.3 SSH 金鑰(使用金鑰進行無密碼登入)
金鑰類型 — ed25519(非 RSA)。檔案名稱 — <server>_<device>,並設定密碼短語。
在本機端:
ssh-keygen -t ed25519 -f ~/.ssh/<server>_<device> -C "<server>_<device>"
ssh-copy-id -i ~/.ssh/<server>_<device>.pub <USERNAME>@<SERVER_IP>
請在另一個 SSH 工作階段中測試金鑰登入(不要關閉舊的工作階段!):
ssh -i ~/.ssh/<server>_<device> -o IdentitiesOnly=yes <USERNAME>@<SERVER_IP> 'whoami; sudo whoami'
在本機端的 ~/.ssh/config 中設定便利的別名:
Host <alias>
HostName <SERVER_IP>
User <USERNAME>
IdentityFile ~/.ssh/<server>_<device>
IdentitiesOnly yes
1.4 SSH 強化設定
建立 /etc/ssh/sshd_config.d/99-hardening.conf:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowTcpForwarding local
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers <USERNAME>
AllowTcpForwarding local允許本地埠轉發(ssh -L— 在設定網域前,可用來存取127.0.0.1上的服務),並禁止遠端轉發(-R)。如果完全不需要埠轉發,則設為no。
套用並檢查:
sudo sshd -t # 語法正確?
sudo systemctl restart ssh
sudo sshd -T | grep -iE 'permitroot|passwordauth' # 實際生效的值應為 no
陷阱 1 — cloud-init 會覆寫強化設定。 檔案 /etc/ssh/sshd_config.d/50-cloud-init.conf 通常包含 PasswordAuthentication yes,而在 sshd_config.d/ 目錄中採用「先比對先贏」的規則。請透過 sudo sshd -T(檢查實際生效的值)來驗證,而不僅僅檢查您自己的設定檔。如果設定被覆寫,請修改/刪除 50-cloud-init.conf 中的對應行。
陷阱 2 — Ubuntu 24.04 使用 ssh.socket。 sshd_config 中的 Port 設定會被忽略。若要變更 SSH 埠號:sudo systemctl edit ssh.socket → 加入 ListenStream=<PORT>(並移除預設埠)。請透過供應商提供的控制台進行驗證 — 此處的錯誤會鎖住您的存取權限。
1.5 自動安全性更新 ⭐(實現自主性的核心)
這是實現「伺服器自我維護」最重要的一步。沒有它,套件中的漏洞會不斷累積。
sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure -plow unattended-upgrades # 選擇 "Yes" — 會建立 20auto-upgrades
檢查 /etc/apt/apt.conf.d/20auto-upgrades:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
在 /etc/apt/apt.conf.d/50unattended-upgrades 中(取消註解/設定關鍵行):
// 套用安全性更新(預設為啟用):
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// 如果更新需要重新啟動(例如核心更新),則自動重新啟動。選擇負載最低的時間視窗:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
「自動重新啟動」的決策:
true= 伺服器在核心更新時會於夜間自動重新啟動(更安全,但可能會有短暫停機)。false= 核心會更新,但重新啟動需要您手動執行(需要留意)。對於單一生產伺服器的新手來說,通常true搭配夜間時間視窗是較好的選擇。
檢查 — 不實際安裝的模擬執行:
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -20
systemctl list-timers 'apt-daily*' # 計時器是否正在執行?
1.6 fail2ban(保護 SSH 免受暴力破解攻擊)
基本設定就已足夠 — 不要在初期就使用 recidive/nftables 來增加複雜度。
sudo apt install fail2ban -y
/etc/fail2ban/jail.d/sshd.local:
[sshd]
enabled = true
bantime = 1h
findtime = 30m
maxretry = 4
# 不要封鎖自己:加入您的靜態 IP(查詢:curl ifconfig.me)
ignoreip = 127.0.0.1/8 ::1 <YOUR_HOME_IP>
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
1.7 時區與(針對小型 VPS)Swap 空間
sudo timedatectl set-timezone <Region/City> # 例如 Asia/Taipei 或 UTC
# 對於記憶體較小(< 2 GB)的 VPS,需要 Swap 空間以防止 OOM 終止服務:
free -h
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf && sudo sysctl --system
2. UFW 防火牆
原則:預設關閉所有連線,僅開放必要連線。 首先必須允許 SSH,否則您會將自己鎖在外面。
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH' # 重要:在 enable 之前執行!
sudo ufw enable
sudo ufw status verbose
對於 Web 伺服器,請加入 HTTP/HTTPS:
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
管理頁面/儀表板/監控指標 — 僅限信任的 IP 存取,而非對「所有人」開放:
sudo ufw allow from <TRUSTED_IP> to any port <ADMIN_PORT> proto tcp comment 'admin panel'
絕對不要使用
sudo ufw reset— 它會重置所有規則,包括 SSH。
3. Docker(選擇性 — 如果應用程式以容器方式執行)
安裝
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker <USERNAME> # 重新登入以使群組權限生效
成為
docker群組的成員實際上等同於擁有 root 權限(透過docker run -v /:/host)。這對於單一管理者來說是可接受的,但不要將此群組授予您不信任其擁有完整伺服器存取權限的使用者。
日誌輪替(必要 — 否則日誌會耗盡磁碟空間)
/etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker
⚠️ Docker 會繞過 UFW
使用 -p 8080:8080 映射的連接埠,即使 UFW 未允許,也可從網際網路存取。
安全選項:
- 僅綁定到 localhost:
-p 127.0.0.1:8080:8080(僅能透過 nginx/SSH 隧道存取)。 - 完全不要將資料庫的連接埠映射到外部 — 在 Docker 網路內部使用服務名稱進行通訊。
4. nginx + HTTPS(選擇性 — 如果有 Web 應用程式和網域)
使用 nginx 作為應用程式的反向代理(應用程式監聽 127.0.0.1,對外僅開放 nginx)。
sudo apt install nginx -y
/etc/nginx/sites-available/<app>:
server {
listen 80;
server_name <DOMAIN>;
location / {
proxy_pass http://127.0.0.1:<APP_PORT>;
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;
}
}
sudo ln -s /etc/nginx/sites-available/<app> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
透過 Let's Encrypt 免費取得 HTTPS(certbot 會自動簽發並設定自動續約):
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d <DOMAIN>
# 自動續約已由計時器設定完成。驗證:
sudo certbot renew --dry-run
安全強化:在 /etc/nginx/nginx.conf 中加入 server_tokens off;(隱藏 nginx 版本)以及安全性標頭(Strict-Transport-Security、X-Content-Type-Options nosniff)。
5. 自動備份(restic + 異地備份)
完整說明(安裝、儲存庫、腳本、計時器、還原): 請參閱 references/backups.md — 內有逐步指引。
絕對不能違反的關鍵原則:
- 備份必須是異地備份。 存放在同一台伺服器(或磁碟)上的副本無法在伺服器/磁碟損毀時保護資料。最低要求 — 外部儲存空間(相容於 S3 的服務:Backblaze B2、rsync.net 等)。
- 儲存庫的加密密碼必須存放在伺服器外部(密碼管理員)。遺失密碼 → 所有備份將永久無法讀取。這是導致備份失效最常見的原因。
- 未經還原驗證的備份就不是備份。 至少進行一次測試還原(請參閱 backups.md 的「練習還原」章節)。
- 自動化: 透過 systemd 計時器每日執行 + 自動輪替舊的快照。
備份內容
請以類別而非個別路徑進行思考 — 這樣才不會遺漏任何項目:
| 類別 | 路徑範例 | 原因 |
|---|---|---|
| 系統設定檔 | 整個 /etc 目錄 | nginx、ssh、ufw、fail2ban、systemd 單元 — 用於將伺服器恢復原狀 |
| 應用程式資料 | /home/<USERNAME>/app/data、/var/lib/<service> | 無法重新安裝的內容:使用者產生的內容、上傳檔案 |
| 資料庫匯出 | /var/backups/*.sql.gz | 在執行 restic 備份之前,透過 pg_dump/.backup 進行資料庫匯出(存活的資料庫檔案可能已損毀) |
| 機密資訊 | .env、/root/.config/* | 以加密形式備份(restic 會加密儲存庫)。加密密碼 — 存放在伺服器外部 |
不需要備份的內容(可以重新安裝,只會使備份變大):
node_modules、.venv/venv、Docker 映像檔(docker pull)、系統套件(apt)、快取、/var/log、/tmp。這些路徑已包含在腳本的 --exclude 參數中(請參閱 backups.md)。
保留策略 — 選擇您的政策
restic 儲存去重複化的快照,因此「保留更多副本」在儲存空間上成本很低。
命令:restic forget --prune --keep-daily N --keep-weekly N --keep-monthly N
| 策略 | 命令 | 適合對象 | 說明 |
|---|---|---|---|
| 基本 (預設) | --keep-daily 7 --keep-weekly 4 --keep-monthly 6 | 大多數生產伺服器 | 可還原至過去 7 天中的任何一天 + 長達半年的歷史紀錄。既能捕捉最近的錯誤,也能追溯到「問題是何時發生的」。backups.md 中已採用此設定。 |
| 輕量 | --keep-daily 7 --keep-weekly 4 | 沒有重要累積資料的網站/機器人 | 大約 1 個月的歷史紀錄。佔用最少空間,較為簡單。適用於遺失舊資料影響不大的情況。 |
| 長期 | --keep-daily 14 --keep-weekly 8 --keep-monthly 12 | 包含重要資料的資料庫、財務、法律要求 | 一年歷史紀錄 + 14 天的每日備份。儲存成本較高,但可防止未立即發現的「靜默」資料損毀。 |
給新手的建議: 從基本策略開始 — 在儲存成本極低的情況下,它涵蓋了兩種場景(快速還原與長期歷史紀錄)。您可以隨時切換到長期策略,只需更改
forget中的數字即可 — 舊的快照不會因此遺失。
關於資料庫的重要提醒: 如果您備份的是「存活的」資料庫檔案,保留策略是沒有意義的 — 該檔案可能已損毀。請務必先執行資料庫匯出(
pg_dump/ SQLite.backup),然後讓 restic 備份匯出的檔案。
6. 監控
…
[此處截斷以利翻譯]