GCE VM は 1 台のまま維持しつつ、`portfolio-hp` と `reading-time-tracker` の公開中コード、Git 作業コピー、DB、env、backup を分離した。単一 VM 構成のままでも、更新失敗、権限事故、DB 巻き込みを避けるための最小限の運用境界を作っている。
入口から静的ページ、SPA、API、DB までの流れを 1 枚にまとめた。Cloudflare と Nginx で入口を統一しつつ、`portfolio-hp` と `reading-time-tracker` の役割を分けている。
すべての公開トラフィックは Cloudflare → GCE VM → Nginx を通る。
/ と /page2.html は静的 HTML、/books/ は SPA を返す。
/books/api/ は Gunicorn に流し、最終的に SQLite と backup 運用へつながる。
本番で変化するものを Git 管理配下から外し、repo と runtime を分離する。これが今回の改善の中心で、手戻りや誤操作が起きても被害を局所化できるようにしている。
/home/itamishotaro/portfolio-hp
/opt/reading-time-tracker/repo
/opt/reading-time-tracker/current
portfolio は `index.html` と `page2.html` の symlink で公開。
/var/lib/reading-time-tracker/books.db
/var/backups/reading-time-tracker
/etc/reading-time-tracker/reading-time-tracker.env
repo 配下に `.env` を置かない。
reading-time-tracker: repo -> /opt/reading-time-tracker/repo current -> /opt/reading-time-tracker/releases/20260411003055 frontend -> /var/www/html/books -> /var/www/releases/reading-time-tracker/20260411003055 db -> /var/lib/reading-time-tracker/books.db env -> /etc/reading-time-tracker/reading-time-tracker.env backup -> /var/backups/reading-time-tracker/books-YYYYMMDDHHMMSS.db
Nginx は静的サイト、SPA、API の入口を 1 つにまとめる。`/books/` は frontend release の symlink を返し、`/books/api/` は unix socket 経由で Gunicorn に流す。
location = /books/api/admin/session {
limit_req zone=books_admin_session burst=5 nodelay;
}
location /books/api/ {
limit_req zone=books_api burst=20 nodelay;
proxy_pass http://unix:/run/reading-time-tracker/reading-time-tracker.sock;
}
backend は repo 直参照ではなく `current` を読む。virtualenv も release ごとに作らず、`shared/.venv` を使い回す。これで release 切替と runtime を分離できる。
sudo systemctl status reading-time-tracker --no-pager -l sudo journalctl -u reading-time-tracker -n 200 --no-pager curl -I https://itamishotaro.com/books/api/health
デプロイは release 作成後に symlink を切り替える。rollback は symlink の戻しだけで完結させ、DB restore は service stop を必ず挟む。
portfolio-hp: git pull ./deploy/release.sh reading-time-tracker: git pull ./deploy/release.sh
frontend: ln -sfnT previous /var/www/html/books backend: ln -sfnT previous /opt/reading-time-tracker/current systemctl restart reading-time-tracker portfolio: ln -sfnT previous/index.html /var/www/html/index.html
systemctl stop reading-time-tracker restore backup db owner / perm 修正 systemctl start reading-time-tracker curl /books/api/health
SQLite backup と healthcheck は timer に寄せた。障害が軽微なら healthcheck が 1 回だけ backend restart を試み、それでも戻らなければ journald に失敗を残す。
sudo systemctl list-timers --all | grep reading-time-tracker sudo systemctl start reading-time-tracker-backup.service sudo journalctl -u reading-time-tracker-backup.service -n 50 --no-pager sudo journalctl -u reading-time-tracker-healthcheck.service -n 50 --no-pager
この構成で「今まともに動いているか」を最短で見るなら、URL 4 本、service 1 本、symlink 4 本、backup 1 本を見れば十分です。
curl -I https://itamishotaro.com/ curl -I https://itamishotaro.com/page2.html curl -I https://itamishotaro.com/books/ curl -I https://itamishotaro.com/books/api/health
systemctl status reading-time-tracker --no-pager -l readlink -f /opt/reading-time-tracker/current readlink -f /var/www/html/books readlink -f /var/www/html/index.html readlink -f /var/www/html/page2.html ls -1 /var/backups/reading-time-tracker
/ が 200
/books/ が 200
/books/api/health が 200
symlink が release を指す
次回の再発防止のために、実際に詰まった点を運用資料として残す。概念図だけではここが抜けやすい。
現行の実測値: backend current -> /opt/reading-time-tracker/releases/20260411003055 frontend books -> /var/www/releases/reading-time-tracker/20260411003055 portfolio index -> /var/www/releases/portfolio-hp/20260411000949/index.html portfolio page2 -> /var/www/releases/portfolio-hp/20260411000949/page2.html backup file -> /var/backups/reading-time-tracker/books-20260411004529.db