2026年9月5日

2026年9月5日

VPSでPythonのWebアプリをデプロイする方法(Gunicorn+Nginx+systemd)

VPS で Python の Web アプリを公開する定番の構成は、「アプリを Gunicorn で動かし、systemd で常駐させ、前段に Nginx を置いて HTTPS 化する」形です。この記事では Ubuntu の VPS を例に、Flask アプリを本番運用できる状態にするまでの手順と、各手順でつまずきやすい点を説明します。Django や FastAPI でも、起動コマンドが変わるだけで流れは同じです。

全体の構成と、なぜこの形にするのか

開発中に使う flask run や Django の runserver は、開発用のサーバーです。同時に多数のリクエストをさばくことやセキュリティ面を想定しておらず、本番で使わないよう公式ドキュメントでも案内されています。そこで役割を次のように分けます。

  • Gunicorn:Python のアプリを複数のワーカープロセスで動かす WSGI サーバー
  • systemd:Gunicorn をサービスとして登録し、OS 起動時の自動起動や異常終了時の再起動を担当
  • Nginx:外部からの HTTP/HTTPS を受け、静的ファイルを返し、残りを Gunicorn へ中継(リバースプロキシ)

Gunicorn は 127.0.0.1 だけで待ち受け、外部に直接公開しないのがポイントです。

1. サーバーの準備

OS の更新、作業用ユーザーの作成、SSH の鍵認証、ファイアウォールの設定を先に済ませます。ファイアウォールでは SSH・HTTP・HTTPS のポートだけを開けます。

続いて Python の仮想環境に必要なパッケージと Nginx を入れます。

sudo apt update
sudo apt install python3-venv python3-pip nginx
python3 --version

最近の Ubuntu や Debian では、システムの Python に pip install で直接パッケージを入れようとすると externally-managed-environment というエラーで止まります。これは OS のパッケージと pip のパッケージが衝突しないようにする仕組み(PEP 668)で、回避フラグを付けるのではなく、アプリごとに仮想環境(venv)を作るのが正しい対処です。Ubuntu に入っている Python のバージョンは OS のバージョンによって異なり、現行の LTS では 3.12 系以降です(2026年10月現在)。アプリが要求するバージョンと合っているかを確認してください。

2. アプリの配置と仮想環境

アプリ専用のユーザーとディレクトリを用意します。root や自分のログインユーザーで動かさないことで、万一アプリに脆弱性があっても被害の範囲を狭められます。

sudo adduser --system --group --home /srv/myapp myapp
sudo -u myapp git clone https://github.com/your-name/myapp.git /srv/myapp/app
cd /srv/myapp/app
sudo -u myapp python3 -m venv /srv/myapp/venv
sudo -u myapp /srv/myapp/venv/bin/pip install -r requirements.txt gunicorn

requirements.txt には、手元で動作確認したバージョンを固定して書いておくと、サーバー側で予期しない新バージョンが入るのを防げます。

ここで、Gunicorn 単体で起動するか確かめます。Flask の場合、app.py の中の app オブジェクトを app:app の形で指定します。

sudo -u myapp /srv/myapp/venv/bin/gunicorn --bind 127.0.0.1:8000 app:app
# 別のターミナルで
curl -I http://127.0.0.1:8000/

ModuleNotFoundError が出る場合は、作業ディレクトリが違うか、モジュール名:変数名 の指定が実際のファイル構成と合っていません。Django なら myproject.wsgi:application のように、wsgi.py の場所を指定します。

3. 設定値と秘密情報を環境変数で渡す

データベースのパスワードや SECRET_KEY をソースコードに書くと、Git に誤ってコミットする危険があります。サーバー上に権限を絞った環境変数ファイルを置き、systemd から読み込ませます。

sudo install -o root -g myapp -m 640 /dev/null /etc/myapp.env
sudo nano /etc/myapp.env
# /etc/myapp.env(値はダミー)
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://myapp:your-db-password@127.0.0.1/myapp

アプリ側では os.environ で読み取ります。Django では本番で DEBUG = False にし、ALLOWED_HOSTS に公開するドメインを設定します。DEBUG が有効なままだと、エラー時に設定値やスタックトレースが外部に表示されてしまいます。

4. systemd でサービス化する

# /etc/systemd/system/myapp.service
[Unit]
Description=myapp (Gunicorn)
After=network.target

[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp.env
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
sudo journalctl -u myapp -n 50

ワーカー数は CPU コア数を目安に調整します。Gunicorn のドキュメントでは「コア数×2+1」が出発点として紹介されていますが、メモリが少ない VPS ではワーカーを増やすほどメモリを消費するので、free -h で余裕を見ながら決めてください。起動しないときは、まず journalctl のログを読みます。多くの場合、パスの誤りか環境変数の不足がそのまま書かれています。

FastAPI などの ASGI アプリでは、Gunicorn に Uvicorn のワーカークラスを指定するか、Uvicorn を直接起動します。ワーカークラスの提供元パッケージは Uvicorn のバージョンによって変わっているため、使うバージョンの公式ドキュメントで指定方法を確認してください。

5. Nginx をリバースプロキシにする

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/app/static/;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        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/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t で構文チェックをしてから再読み込みする習慣をつけると、設定ミスでサイト全体が止まるのを防げます。502 Bad Gateway が出る場合は Gunicorn が起動していないか、proxy_pass のポートが違います。Django では collectstatic で静的ファイルを集め、その場所を alias に指定します。

X-Forwarded-Proto などのヘッダーは、アプリが「HTTPS でアクセスされた」と正しく判定するために渡しています。Flask なら Werkzeug の ProxyFix、Django なら SECURE_PROXY_SSL_HEADER の設定で、このヘッダーを信頼するよう指定します。信頼するのは自分の Nginx を経由した場合だけにしてください。

6. HTTPS 化と更新の手順

DNS がサーバーを向いたら、Certbot の Nginx プラグインで Let's Encrypt の証明書を取得します。

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
sudo certbot renew --dry-run

アプリを更新するときは、git pull → 依存パッケージの更新 → (Django なら migrate と collectstatic)→ sudo systemctl restart myapp の順に進めます。データベースのマイグレーションを伴う更新の前には、バックアップを取っておいてください。手順が固まったら、シェルスクリプトや CI にまとめると作業ミスを減らせます。

まとめ

  • 本番では開発用サーバーではなく、Gunicorn+systemd+Nginx の構成にする。
  • パッケージはシステムの Python ではなく、アプリごとの venv に入れる。
  • アプリは専用ユーザーで動かし、秘密情報は権限を絞った環境変数ファイルで渡す。
  • Gunicorn は 127.0.0.1 で待ち受け、外部には Nginx だけを公開する。
  • 起動しない・502 が出るときは journalctl と nginx -t から確認する。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲