<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Python  |  takeHo（たけほ）のへなちょこ台帳</title>
	<atom:link href="https://blog.takeho.com/category/web/python/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.takeho.com</link>
	<description>いわゆる自由帳ってところです。</description>
	<lastBuildDate>Thu, 17 Sep 2026 01:36:23 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6</generator>

<image>
	<url>https://blog.takeho.com/wp-content/uploads/2024/08/icon-150x150.png</url>
	<title>Python  |  takeHo（たけほ）のへなちょこ台帳</title>
	<link>https://blog.takeho.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Python・FlaskでMySQLログイン認証を作る方法―MVC構成、登録確認メール、パスワード再設定まで詳しく解説</title>
		<link>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/</link>
					<comments>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:28:07 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Python Flask]]></category>
		<category><![CDATA[ログイン認証]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=2005</guid>

					<description><![CDATA[PythonでWebアプリを作り始めると、早い段階で必要になるのがログイン認証です。単にユーザー名とパスワードを照合するだけなら短いコードでも作れますが、実際のサービスではアカウント作成、入力値の確認、メールアドレスの有 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>PythonでWebアプリを作り始めると、早い段階で必要になるのがログイン認証です。単にユーザー名とパスワードを照合するだけなら短いコードでも作れますが、実際のサービスではアカウント作成、入力値の確認、メールアドレスの有効性確認、ログアウト、パスワードを忘れた場合の再設定まで必要です。</p>



<p>この記事では、PythonのWebフレームワーク「Flask」とMySQLを使い、認証機能をMVC形式で整理して作る手順を解説します。認証情報は「ユーザー名・メールアドレス・パスワード」の3項目です。パスワードは平文で保存せず、ハッシュ化して安全に管理します。</p>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2"><label class="toc-title" for="toc-checkbox-2">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">この記事で作る機能</a></li><li><a href="#toc2" tabindex="0">使用する構成</a></li><li><a href="#toc3" tabindex="0">MVC形式の考え方</a></li><li><a href="#toc4" tabindex="0">完成時のファイル構成</a></li><li><a href="#toc5" tabindex="0">1. Pythonの仮想環境を作る</a></li><li><a href="#toc6" tabindex="0">2. 必要なライブラリをインストールする</a></li><li><a href="#toc7" tabindex="0">3. MySQLにデータベースと専用ユーザーを作る</a></li><li><a href="#toc8" tabindex="0">4. 環境変数を設定する</a></li><li><a href="#toc9" tabindex="0">5. 設定ファイルを作る</a></li><li><a href="#toc10" tabindex="0">6. 拡張機能をまとめる</a></li><li><a href="#toc11" tabindex="0">7. Userモデルを作る</a></li><li><a href="#toc12" tabindex="0">8. フォームとバリデーションを作る</a></li><li><a href="#toc13" tabindex="0">9. 有効期限付きトークンを作る</a></li><li><a href="#toc14" tabindex="0">10. メール送信処理を作る</a></li><li><a href="#toc15" tabindex="0">11. Controllerを作る</a></li><li><a href="#toc16" tabindex="0">12. Flaskアプリを初期化する</a></li><li><a href="#toc17" tabindex="0">13. HTMLテンプレートを作る</a></li><li><a href="#toc18" tabindex="0">14. MySQLへテーブルを作成する</a></li><li><a href="#toc19" tabindex="0">15. 動作を確認する</a></li><li><a href="#toc20" tabindex="0">本番公開前に追加したい対策</a></li><li><a href="#toc21" tabindex="0">よくあるエラー</a><ol><li><a href="#toc22" tabindex="0">Access denied for user</a></li><li><a href="#toc23" tabindex="0">No module named PyMySQL</a></li><li><a href="#toc24" tabindex="0">確認メールのURLがlocalhostになる</a></li><li><a href="#toc25" tabindex="0">CSRF token is missing</a></li></ol></li><li><a href="#toc26" tabindex="0">まとめ</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">この記事で作る機能</span></h2>



<ul class="wp-block-list">
<li>ユーザー名、メールアドレス、パスワードによるアカウント作成</li>



<li>フォーム入力値のバリデーション</li>



<li>確認メールによるメールアドレスの有効性確認</li>



<li>ログイン、ログアウト</li>



<li>ログインが必要なページの保護</li>



<li>パスワード紛失時の再設定メール送信</li>



<li>期限付きトークンによるパスワード再設定</li>



<li>MySQLへのユーザー情報保存</li>
</ul>



<h2 class="wp-block-heading"><span id="toc2">使用する構成</span></h2>



<p>今回は次のライブラリを使います。</p>



<ul class="wp-block-list">
<li><strong>Flask</strong>：Pythonの軽量Webフレームワーク</li>



<li><strong>Flask-SQLAlchemy</strong>：MySQLのデータをPythonのクラスとして扱う</li>



<li><strong>Flask-Migrate</strong>：テーブル構造の変更を管理する</li>



<li><strong>Flask-Login</strong>：ログイン状態を管理する</li>



<li><strong>Flask-WTF / WTForms</strong>：フォームとバリデーション、CSRF対策</li>



<li><strong>Flask-Mail</strong>：確認メールや再設定メールを送信する</li>



<li><strong>itsdangerous</strong>：有効期限付きの安全なトークンを作る</li>



<li><strong>PyMySQL</strong>：PythonからMySQLへ接続する</li>



<li><strong>python-dotenv</strong>：環境変数を読み込む</li>
</ul>



<h2 class="wp-block-heading"><span id="toc3">MVC形式の考え方</span></h2>



<p>Flaskにはフォルダ構成を強制する決まりがありません。そのため、すべてを1つの <code>app.py</code> に書くこともできます。しかし、機能が増えると修正箇所が分かりにくくなります。今回は役割ごとに分離します。</p>



<ul class="wp-block-list">
<li><strong>Model</strong>：MySQLのテーブルとデータ操作。今回は <code>models/user.py</code></li>



<li><strong>View</strong>：利用者に表示するHTML。今回は <code>templates/</code></li>



<li><strong>Controller</strong>：URLごとの処理、入力確認、画面遷移。今回は <code>controllers/auth_controller.py</code></li>
</ul>



<p>FlaskではControllerをBlueprintで分けると、認証機能を他の機能から切り離しやすくなります。</p>



<h2 class="wp-block-heading"><span id="toc4">完成時のファイル構成</span></h2>



<pre class="wp-block-code"><code>python-example/
├── run.py
├── requirements.txt
├── .env
├── .gitignore
├── migrations/
└── app/
    ├── __init__.py
    ├── config.py
    ├── extensions.py
    ├── models/
    │   ├── __init__.py
    │   └── user.py
    ├── forms/
    │   ├── __init__.py
    │   └── auth_form.py
    ├── controllers/
    │   ├── __init__.py
    │   └── auth_controller.py
    ├── services/
    │   ├── __init__.py
    │   ├── mail_service.py
    │   └── token_service.py
    └── templates/
        ├── base.html
        ├── index.html
        └── auth/
            ├── register.html
            ├── login.html
            ├── forgot_password.html
            └── reset_password.html</code></pre>



<h2 class="wp-block-heading"><span id="toc5">1. Pythonの仮想環境を作る</span></h2>



<pre class="wp-block-code"><code>mkdir python-example
cd python-example
python3 -m venv venv
source venv/bin/activate</code></pre>



<p><code>python3 -m venv venv</code> は、プロジェクト専用のPython環境を作るコマンドです。ほかのPythonアプリとライブラリのバージョンが混ざるのを防ぎます。Windowsの場合は <code>venv\Scripts\activate</code> で有効化します。</p>



<h2 class="wp-block-heading"><span id="toc6">2. 必要なライブラリをインストールする</span></h2>



<pre class="wp-block-code"><code>pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail email-validator PyMySQL python-dotenv
pip freeze &gt; requirements.txt</code></pre>



<p><code>email-validator</code> は、メールアドレスの形式をWTFormsで確認するために必要です。インストール後の状態を <code>requirements.txt</code> に記録しておけば、別のサーバーでも <code>pip install -r requirements.txt</code> で同じ環境を作れます。</p>



<h2 class="wp-block-heading"><span id="toc7">3. MySQLにデータベースと専用ユーザーを作る</span></h2>



<p>MySQLへ管理権限のあるユーザーでログインします。</p>



<pre class="wp-block-code"><code>mysql -u root -p</code></pre>



<p>続いて、データベースとアプリ専用ユーザーを作成します。</p>



<pre class="wp-block-code"><code>CREATE DATABASE python_example
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

CREATE USER 'python_user'@'localhost'
  IDENTIFIED BY '十分に長く推測されにくいパスワード';

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
  ON python_example.* TO 'python_user'@'localhost';

FLUSH PRIVILEGES;
EXIT;</code></pre>



<p><code>utf8mb4</code> を指定すると、日本語だけでなく絵文字も保存できます。本番環境ではrootユーザーをアプリから使わず、対象データベースだけを操作できる専用ユーザーを使います。</p>



<h2 class="wp-block-heading"><span id="toc8">4. 環境変数を設定する</span></h2>



<p>パスワードやメールサーバーの認証情報をソースコードへ直接書かないように、プロジェクト直下へ <code>.env</code> を作ります。</p>



<pre class="wp-block-code"><code>SECRET_KEY=ここに長いランダム文字列
DATABASE_URL=mysql+pymysql://python_user:DBパスワード@localhost/python_example?charset=utf8mb4

MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=no-reply@example.com
MAIL_PASSWORD=メール用パスワード
MAIL_DEFAULT_SENDER=no-reply@example.com</code></pre>



<p><code>SECRET_KEY</code> はCSRF対策やトークン署名に使います。次のコマンドで生成できます。</p>



<pre class="wp-block-code"><code>python -c "import secrets; print(secrets.token_hex(32))"</code></pre>



<p><code>.gitignore</code> には必ず次を記載します。</p>



<pre class="wp-block-code"><code>venv/
.env
__pycache__/
*.pyc</code></pre>



<h2 class="wp-block-heading"><span id="toc9">5. 設定ファイルを作る</span></h2>



<p><code>app/config.py</code> を作成します。</p>



<pre class="wp-block-code"><code>import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.environ&#91;"SECRET_KEY"]
    SQLALCHEMY_DATABASE_URI = os.environ&#91;"DATABASE_URL"]
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    MAIL_SERVER = os.environ&#91;"MAIL_SERVER"]
    MAIL_PORT = int(os.getenv("MAIL_PORT", "587"))
    MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "true").lower() == "true"
    MAIL_USERNAME = os.environ&#91;"MAIL_USERNAME"]
    MAIL_PASSWORD = os.environ&#91;"MAIL_PASSWORD"]
    MAIL_DEFAULT_SENDER = os.environ&#91;"MAIL_DEFAULT_SENDER"]</code></pre>



<p><code>os.environ["名前"]</code> は、設定がない場合にエラーとなります。重要な設定の不足に気づきやすいため、本番用の認証情報にはこの書き方が向いています。</p>



<h2 class="wp-block-heading"><span id="toc10">6. 拡張機能をまとめる</span></h2>



<p><code>app/extensions.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
mail = Mail()

login_manager.login_view = "auth.login"
login_manager.login_message = "ログインしてください。"
login_manager.login_message_category = "warning"</code></pre>



<p>ここでは拡張機能の本体だけを作り、アプリとの接続は後ほど <code>create_app()</code> の中で行います。この形式はApplication Factoryパターンと呼ばれ、テストや設定の切り替えがしやすくなります。</p>



<h2 class="wp-block-heading"><span id="toc11">7. Userモデルを作る</span></h2>



<p><code>app/models/user.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from datetime import datetime, timezone
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login_manager

class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False, index=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(255), nullable=False)
    email_verified = db.Column(db.Boolean, nullable=False, default=False)
    created_at = db.Column(
        db.DateTime,
        nullable=False,
        default=lambda: datetime.now(timezone.utc)
    )

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))</code></pre>



<p>データベースにはパスワードそのものではなく <code>password_hash</code> を保存します。<code>generate_password_hash()</code> は同じパスワードでも毎回異なるハッシュ値を作ります。ログイン時は <code>check_password()</code> で照合します。</p>



<p><code>email_verified</code> はメール確認が完了したかを示す項目です。登録直後は <code>False</code>、確認用URLを開いたあとに <code>True</code> へ変更します。</p>



<h2 class="wp-block-heading"><span id="toc12">8. フォームとバリデーションを作る</span></h2>



<p><code>app/forms/auth_form.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import (
    DataRequired, Email, EqualTo, Length, Regexp, ValidationError
)
from app.models.user import User

class RegisterForm(FlaskForm):
    username = StringField(
        "ユーザー名",
        validators=&#91;
            DataRequired(),
            Length(min=3, max=50),
            Regexp(
                r"^&#91;A-Za-z0-9_]+$",
                message="半角英数字とアンダースコアのみ使用できます。"
            )
        ]
    )
    email = StringField(
        "メールアドレス",
        validators=&#91;DataRequired(), Email(), Length(max=255)]
    )
    password = PasswordField(
        "パスワード",
        validators=&#91;
            DataRequired(),
            Length(min=12, max=128),
            Regexp(r"^(?=.*&#91;a-z])(?=.*&#91;A-Z])(?=.*\d).+$",
                   message="英大文字・英小文字・数字を含めてください。")
        ]
    )
    password_confirm = PasswordField(
        "パスワード（確認）",
        validators=&#91;DataRequired(), EqualTo("password")]
    )
    submit = SubmitField("アカウントを作成")

    def validate_username(self, field):
        if User.query.filter_by(username=field.data.strip()).first():
            raise ValidationError("このユーザー名はすでに使われています。")

    def validate_email(self, field):
        email = field.data.strip().lower()
        if User.query.filter_by(email=email).first():
            raise ValidationError("このメールアドレスはすでに登録されています。")

class LoginForm(FlaskForm):
    login_id = StringField(
        "ユーザー名またはメールアドレス",
        validators=&#91;DataRequired(), Length(max=255)]
    )
    password = PasswordField("パスワード", validators=&#91;DataRequired()])
    submit = SubmitField("ログイン")

class ForgotPasswordForm(FlaskForm):
    email = StringField(
        "メールアドレス",
        validators=&#91;DataRequired(), Email(), Length(max=255)]
    )
    submit = SubmitField("再設定メールを送信")

class ResetPasswordForm(FlaskForm):
    password = PasswordField(
        "新しいパスワード",
        validators=&#91;
            DataRequired(),
            Length(min=12, max=128),
            Regexp(r"^(?=.*&#91;a-z])(?=.*&#91;A-Z])(?=.*\d).+$",
                   message="英大文字・英小文字・数字を含めてください。")
        ]
    )
    password_confirm = PasswordField(
        "新しいパスワード（確認）",
        validators=&#91;DataRequired(), EqualTo("password")]
    )
    submit = SubmitField("パスワードを変更")</code></pre>



<p>ブラウザ側の <code>required</code> だけでは不十分です。利用者はHTTPリクエストを直接送ることもできるため、必ずPython側でも確認します。主な確認内容は、必須入力、長さ、メール形式、使用可能文字、パスワード一致、重複登録です。</p>



<h2 class="wp-block-heading"><span id="toc13">9. 有効期限付きトークンを作る</span></h2>



<p><code>app/services/token_service.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask import current_app
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired

def _serializer():
    return URLSafeTimedSerializer(current_app.config&#91;"SECRET_KEY"])

def create_token(user_id, purpose):
    return _serializer().dumps({"user_id": user_id}, salt=purpose)

def verify_token(token, purpose, max_age=3600):
    try:
        data = _serializer().loads(token, salt=purpose, max_age=max_age)
        return data.get("user_id")
    except (SignatureExpired, BadSignature):
        return None</code></pre>



<p><code>purpose</code> には、メール確認なら <code>email-confirm</code>、パスワード再設定なら <code>password-reset</code> を指定します。用途ごとにsaltを分けることで、確認用トークンをパスワード再設定へ流用できないようにします。<code>max_age=3600</code> は有効期限1時間です。</p>



<h2 class="wp-block-heading"><span id="toc14">10. メール送信処理を作る</span></h2>



<p><code>app/services/mail_service.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask import url_for
from flask_mail import Message
from app.extensions import mail
from app.services.token_service import create_token

def send_verification_email(user):
    token = create_token(user.id, "email-confirm")
    verify_url = url_for(
        "auth.verify_email", token=token, _external=True
    )
    message = Message(
        subject="メールアドレスの確認",
        recipients=&#91;user.email],
        body=f"""アカウント作成ありがとうございます。
次のURLを1時間以内に開いてください。

{verify_url}

心当たりがない場合は、このメールを破棄してください。
"""
    )
    mail.send(message)

def send_password_reset_email(user):
    token = create_token(user.id, "password-reset")
    reset_url = url_for(
        "auth.reset_password", token=token, _external=True
    )
    message = Message(
        subject="パスワード再設定",
        recipients=&#91;user.email],
        body=f"""次のURLを1時間以内に開き、新しいパスワードを設定してください。

{reset_url}

依頼していない場合は、このメールを破棄してください。
"""
    )
    mail.send(message)</code></pre>



<p><code>_external=True</code> を指定すると、メール本文で利用できる絶対URLが作られます。本番環境ではHTTPSで公開し、正しいドメインが生成されるようにWebサーバー側のHost設定も確認します。</p>



<h2 class="wp-block-heading"><span id="toc15">11. Controllerを作る</span></h2>



<p><code>app/controllers/auth_controller.py</code> を作ります。</p>



<pre class="wp-block-code"><code>from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy import or_
from app.extensions import db
from app.models.user import User
from app.forms.auth_form import (
    RegisterForm, LoginForm, ForgotPasswordForm, ResetPasswordForm
)
from app.services.mail_service import (
    send_verification_email, send_password_reset_email
)
from app.services.token_service import verify_token

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")

@auth_bp.route("/register", methods=&#91;"GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect(url_for("index"))

    form = RegisterForm()
    if form.validate_on_submit():
        user = User(
            username=form.username.data.strip(),
            email=form.email.data.strip().lower()
        )
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()

        send_verification_email(user)
        flash("確認メールを送信しました。メール内のURLを開いてください。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/register.html", form=form)

@auth_bp.route("/verify-email/&lt;token&gt;")
def verify_email(token):
    user_id = verify_token(token, "email-confirm", max_age=3600)
    user = db.session.get(User, user_id) if user_id else None

    if not user:
        flash("確認URLが無効、または有効期限が切れています。", "danger")
        return redirect(url_for("auth.login"))

    if not user.email_verified:
        user.email_verified = True
        db.session.commit()

    flash("メールアドレスを確認しました。ログインできます。", "success")
    return redirect(url_for("auth.login"))

@auth_bp.route("/login", methods=&#91;"GET", "POST"])
def login():
    if current_user.is_authenticated:
        return redirect(url_for("index"))

    form = LoginForm()
    if form.validate_on_submit():
        login_id = form.login_id.data.strip()
        user = User.query.filter(
            or_(User.username == login_id, User.email == login_id.lower())
        ).first()

        if not user or not user.check_password(form.password.data):
            flash("ユーザー名、メールアドレス、またはパスワードが違います。", "danger")
            return render_template("auth/login.html", form=form)

        if not user.email_verified:
            flash("メールアドレスの確認が完了していません。", "warning")
            return render_template("auth/login.html", form=form)

        login_user(user)
        next_url = request.args.get("next")
        if next_url and next_url.startswith("/"):
            return redirect(next_url)
        return redirect(url_for("index"))

    return render_template("auth/login.html", form=form)

@auth_bp.route("/logout", methods=&#91;"POST"])
@login_required
def logout():
    logout_user()
    flash("ログアウトしました。", "success")
    return redirect(url_for("auth.login"))

@auth_bp.route("/forgot-password", methods=&#91;"GET", "POST"])
def forgot_password():
    form = ForgotPasswordForm()
    if form.validate_on_submit():
        user = User.query.filter_by(
            email=form.email.data.strip().lower()
        ).first()

        if user:
            send_password_reset_email(user)

        flash("登録済みの場合は、再設定メールを送信しました。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/forgot_password.html", form=form)

@auth_bp.route("/reset-password/&lt;token&gt;", methods=&#91;"GET", "POST"])
def reset_password(token):
    user_id = verify_token(token, "password-reset", max_age=3600)
    user = db.session.get(User, user_id) if user_id else None

    if not user:
        flash("再設定URLが無効、または有効期限が切れています。", "danger")
        return redirect(url_for("auth.forgot_password"))

    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash("パスワードを変更しました。", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/reset_password.html", form=form)</code></pre>



<p>パスワード再設定では、メールアドレスが未登録の場合でも同じ案内を表示しています。「そのメールアドレスが登録されているか」を第三者に知られにくくするためです。また、ログアウトをGETではなくPOSTにしているのは、外部サイトのリンクを開いただけでログアウトさせられる動作を防ぐためです。</p>



<h2 class="wp-block-heading"><span id="toc16">12. Flaskアプリを初期化する</span></h2>



<p><code>app/__init__.py</code> を作成します。</p>



<pre class="wp-block-code"><code>from flask import Flask, render_template
from flask_login import login_required
from app.config import Config
from app.extensions import db, migrate, login_manager, mail

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    mail.init_app(app)

    from app.controllers.auth_controller import auth_bp
    app.register_blueprint(auth_bp)

    @app.route("/")
    def index():
        return render_template("index.html")

    @app.route("/mypage")
    @login_required
    def mypage():
        return "ログイン済みのユーザーだけが表示できます。"

    return app</code></pre>



<p><code>run.py</code> は次の内容です。</p>



<pre class="wp-block-code"><code>from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run(debug=True)</code></pre>



<h2 class="wp-block-heading"><span id="toc17">13. HTMLテンプレートを作る</span></h2>



<p><code>app/templates/base.html</code> では、CSRFトークンを含むログアウトフォームとメッセージ表示を用意します。</p>



<pre class="wp-block-code"><code>&lt;!doctype html&gt;
&lt;html lang="ja"&gt;
&lt;head&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
  &lt;title&gt;{% block title %}Python Example{% endblock %}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;nav&gt;
    &lt;a href="{{ url_for('index') }}"&gt;ホーム&lt;/a&gt;
    {% if current_user.is_authenticated %}
      &lt;form method="post" action="{{ url_for('auth.logout') }}"&gt;
        &lt;input type="hidden" name="csrf_token" value="{{ csrf_token() }}"&gt;
        &lt;button type="submit"&gt;ログアウト&lt;/button&gt;
      &lt;/form&gt;
    {% else %}
      &lt;a href="{{ url_for('auth.login') }}"&gt;ログイン&lt;/a&gt;
      &lt;a href="{{ url_for('auth.register') }}"&gt;アカウント作成&lt;/a&gt;
    {% endif %}
  &lt;/nav&gt;

  {% with messages = get_flashed_messages(with_categories=true) %}
    {% for category, message in messages %}
      &lt;p class="{{ category }}"&gt;{{ message }}&lt;/p&gt;
    {% endfor %}
  {% endwith %}

  {% block content %}{% endblock %}
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p>フォームは共通して、ラベル、入力欄、バリデーションエラーを表示します。例として <code>app/templates/auth/register.html</code> を示します。</p>



<pre class="wp-block-code"><code>{% extends "base.html" %}
{% block title %}アカウント作成{% endblock %}
{% block content %}
&lt;h1&gt;アカウント作成&lt;/h1&gt;
&lt;form method="post" novalidate&gt;
  {{ form.hidden_tag() }}

  {{ form.username.label }}
  {{ form.username(autocomplete="username") }}
  {% for error in form.username.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.email.label }}
  {{ form.email(autocomplete="email") }}
  {% for error in form.email.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.password.label }}
  {{ form.password(autocomplete="new-password") }}
  {% for error in form.password.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.password_confirm.label }}
  {{ form.password_confirm(autocomplete="new-password") }}
  {% for error in form.password_confirm.errors %}
    &lt;p class="error"&gt;{{ error }}&lt;/p&gt;
  {% endfor %}

  {{ form.submit() }}
&lt;/form&gt;
{% endblock %}</code></pre>



<p><code>form.hidden_tag()</code> にはCSRFトークンが含まれます。第三者サイトから勝手にフォームを送信される攻撃を防ぐため、省略しないようにします。ログイン画面にはユーザー名またはメールアドレス、パスワード、ログインボタンと、パスワード再設定画面へのリンクを置きます。</p>



<h2 class="wp-block-heading"><span id="toc18">14. MySQLへテーブルを作成する</span></h2>



<p>モデルを読み込める状態にしてからマイグレーションを実行します。</p>



<pre class="wp-block-code"><code>export FLASK_APP=run.py
flask db init
flask db migrate -m "create users table"
flask db upgrade</code></pre>



<p><code>flask db init</code> は初回だけ実行します。<code>flask db migrate</code> はモデルとの差分から変更ファイルを作り、<code>flask db upgrade</code> が実際にMySQLへ反映します。本番環境では、生成された変更内容を確認してから適用してください。</p>



<p>テーブルを確認する場合は次のようにします。</p>



<pre class="wp-block-code"><code>mysql -u python_user -p python_example
SHOW TABLES;
DESCRIBE users;</code></pre>



<h2 class="wp-block-heading"><span id="toc19">15. 動作を確認する</span></h2>



<pre class="wp-block-code"><code>flask --app run.py run --debug</code></pre>



<p>ブラウザで <code>http://127.0.0.1:5000/auth/register</code> を開き、次の順番で確認します。</p>



<ol class="wp-block-list">
<li>未入力や不正なメール形式でエラーが表示される</li>



<li>短いパスワードや条件を満たさないパスワードが拒否される</li>



<li>正常に登録するとMySQLへユーザーが追加される</li>



<li>確認メールが届き、URLを開くと <code>email_verified</code> が有効になる</li>



<li>確認前はログインできず、確認後はログインできる</li>



<li>同じユーザー名やメールアドレスを重複登録できない</li>



<li>パスワード再設定メールが届き、新しいパスワードに変更できる</li>



<li>期限切れ、改ざん済みトークンが拒否される</li>



<li>未ログインで <code>/mypage</code> を開くとログイン画面へ移動する</li>
</ol>



<h2 class="wp-block-heading"><span id="toc20">本番公開前に追加したい対策</span></h2>



<p>ここまでで基本的な認証は動きますが、インターネットへ公開する場合は次の対策も重要です。</p>



<ul class="wp-block-list">
<li>必ずHTTPSを使用する</li>



<li>ログインや再設定メール送信へ回数制限を入れる</li>



<li>連続失敗を記録し、総当たり攻撃を検知する</li>



<li>セッションCookieへ <code>Secure</code>、<code>HttpOnly</code>、適切な <code>SameSite</code> を設定する</li>



<li>確認メールの再送機能を用意し、再送にも回数制限を入れる</li>



<li>メール送信はCeleryなどのバックグラウンド処理へ分離する</li>



<li>パスワード変更後に既存セッションを無効化する仕組みを検討する</li>



<li>ユーザー入力や認証結果をログへ残す際、パスワードやトークンを書かない</li>



<li>MySQLとアプリのバックアップ、復元手順を用意する</li>
</ul>



<p>Cookie設定の一例は次のとおりです。</p>



<pre class="wp-block-code"><code>SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True</code></pre>



<h2 class="wp-block-heading"><span id="toc21">よくあるエラー</span></h2>



<h3 class="wp-block-heading"><span id="toc22">Access denied for user</span></h3>



<p>MySQLのユーザー名、パスワード、接続元ホスト、GRANTの対象を確認します。<code>'python_user'@'localhost'</code> と <code>'python_user'@'127.0.0.1'</code> はMySQLでは別の接続元として扱われる場合があります。</p>



<h3 class="wp-block-heading"><span id="toc23">No module named PyMySQL</span></h3>



<p>仮想環境が有効か確認し、<code>pip install PyMySQL</code> を実行します。Gunicornやsystemdから起動する場合は、仮想環境内の実行ファイルを指定します。</p>



<h3 class="wp-block-heading"><span id="toc24">確認メールのURLがlocalhostになる</span></h3>



<p>本番ドメインやリバースプロキシの設定が正しくFlaskへ渡っていない可能性があります。ApacheやNginxのHostヘッダー、HTTPS判定、必要に応じてProxyFixの設定を確認します。</p>



<h3 class="wp-block-heading"><span id="toc25">CSRF token is missing</span></h3>



<p>テンプレート内のフォームに <code>{{ form.hidden_tag() }}</code> があるか確認します。通常のHTMLフォームを使う場合も、CSRFトークンのhidden項目が必要です。</p>



<h2 class="wp-block-heading"><span id="toc26">まとめ</span></h2>



<p>PythonとFlaskでログイン認証を作る場合は、ログイン処理だけでなく、登録時の入力確認、メールアドレス確認、パスワードのハッシュ化、再設定用トークンの期限管理までを一つの流れとして考えることが大切です。</p>



<p>MVC形式でModel、View、Controllerを分け、メール処理とトークン処理をServiceへ分離しておけば、機能追加やテストもしやすくなります。まずはローカル環境で登録から再設定まで一通り確認し、その後にHTTPS、回数制限、ログ管理など本番向けの安全対策を追加すると進めやすいでしょう。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/x0y9hpuj4w632wsf0pvk6n5ok0xyfioq/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Rocky Linuxで初めてのPython Webアプリ開発―Flask・Gunicorn・Apacheの環境構築をコマンドごとに解説</title>
		<link>https://blog.takeho.com/o008odgrqz2gfjy0z7wx43kdvrxxk973/</link>
					<comments>https://blog.takeho.com/o008odgrqz2gfjy0z7wx43kdvrxxk973/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Tue, 15 Sep 2026 09:40:00 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Flask]]></category>
		<category><![CDATA[Gunicorn]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=2001</guid>

					<description><![CDATA[PythonでWebアプリを作ってみたいと思っても、最初は「Pythonを入れた後に何をすればよいのか」「Apacheだけでは動かないのか」「FlaskとGunicornは何が違うのか」と迷いやすいものです。 この記事で [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>PythonでWebアプリを作ってみたいと思っても、最初は「Pythonを入れた後に何をすればよいのか」「Apacheだけでは動かないのか」「FlaskとGunicornは何が違うのか」と迷いやすいものです。</p>



<p>この記事では、Rocky Linux上にPythonの実行環境を用意し、Flaskで作ったWebアプリをApache経由で公開するまでを、初めて学ぶ人向けに説明します。例として使うホスト名と設置先は次のとおりです。</p>



<pre class="wp-block-code"><code>ホスト名：python.example.com
設置先：/var/www/html/python.example.com</code></pre>



<p>実際に試す場合は、<code>python.example.com</code>を自分のドメインへ読み替えてください。</p>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-4"><label class="toc-title" for="toc-checkbox-4">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">PythonとPHPではWebアプリの動かし方が少し違う</a></li><li><a href="#toc2" tabindex="0">Flaskを選ぶ理由</a></li><li><a href="#toc3" tabindex="0">1．Rocky Linuxを更新する</a></li><li><a href="#toc4" tabindex="0">2．Pythonとpipをインストールする</a></li><li><a href="#toc5" tabindex="0">3．Webアプリ用ディレクトリを作る</a></li><li><a href="#toc6" tabindex="0">4．Pythonの仮想環境を作る</a></li><li><a href="#toc7" tabindex="0">5．pipを更新してFlaskとGunicornを入れる</a></li><li><a href="#toc8" tabindex="0">6．最小のFlaskアプリを作る</a></li><li><a href="#toc9" tabindex="0">7．開発用サーバーで確認する</a></li><li><a href="#toc10" tabindex="0">8．Gunicornで起動する</a></li><li><a href="#toc11" tabindex="0">9．Apacheでpython.example.comだけを転送する</a></li><li><a href="#toc12" tabindex="0">10．Apacheの設定を確認して反映する</a></li><li><a href="#toc13" tabindex="0">11．本番ではGunicornをsystemdへ登録する</a><ol><li><a href="#toc14" tabindex="0">systemd設定の意味</a></li></ol></li><li><a href="#toc15" tabindex="0">12．サービスを登録して起動する</a></li><li><a href="#toc16" tabindex="0">13．プログラム更新後の反映とログ確認</a></li><li><a href="#toc17" tabindex="0">よくあるトラブル</a><ol><li><a href="#toc18" tabindex="0">502 Proxy Errorが表示される</a></li><li><a href="#toc19" tabindex="0">Address already in useと表示される</a></li><li><a href="#toc20" tabindex="0">Apacheの既存サイトまで転送される</a></li></ol></li><li><a href="#toc21" tabindex="0">まとめ</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">PythonとPHPではWebアプリの動かし方が少し違う</span></h2>



<p>PHPはApacheがPHPファイルを直接読み込み、処理結果を返す構成がよく使われます。一方、PythonのWebアプリでは、ApacheがPythonファイルを直接実行するのではなく、Gunicornというアプリケーションサーバーへアクセスを転送する構成が一般的です。</p>



<pre class="wp-block-code"><code>ブラウザ
  ↓
Apache
  ↓
Gunicorn
  ↓
Flaskで作ったPythonアプリ</code></pre>



<p>それぞれの役割を簡単に整理すると、FlaskはWebアプリを作るためのフレームワーク、Gunicornは作成したアプリを常時動かすサーバー、ApacheはインターネットからのHTTP・HTTPSアクセスを受け付ける入口です。</p>



<h2 class="wp-block-heading"><span id="toc2">Flaskを選ぶ理由</span></h2>



<p>PythonにはDjango、Flask、FastAPIなどのWebフレームワークがあります。初めて小さなWebアプリを作る場合は、構成が単純で必要な機能を少しずつ追加できるFlaskが分かりやすいでしょう。</p>



<ul class="wp-block-list">
<li>Flask：小規模なWebサイト、学習、簡単な業務アプリ向け</li>



<li>Django：ログインや管理画面を含む大きめのサービス向け</li>



<li>FastAPI：スマートフォンアプリや外部サービス向けのAPI開発に向く</li>
</ul>



<p>今回はFlaskを使います。</p>



<h2 class="wp-block-heading"><span id="toc3">1．Rocky Linuxを更新する</span></h2>



<pre class="wp-block-code"><code>sudo dnf update -y</code></pre>



<p><code>sudo</code>は管理者権限でコマンドを実行する指定です。<code>dnf</code>はRocky Linuxでソフトウェアを管理する仕組み、<code>update</code>はインストール済みパッケージを更新する操作です。<code>-y</code>を付けると、途中の確認へ自動的にyesと回答します。</p>



<p>本番サーバーでは更新による影響がないか確認してから実行してください。</p>



<h2 class="wp-block-heading"><span id="toc4">2．Pythonとpipをインストールする</span></h2>



<pre class="wp-block-code"><code>sudo dnf install -y python3 python3-pip</code></pre>



<p><code>install</code>はパッケージのインストール、<code>python3</code>はPython本体、<code>python3-pip</code>はPython用パッケージ管理ツールです。pipを使うことでFlaskやGunicornを追加できます。</p>



<p>インストール後にバージョンを確認します。</p>



<pre class="wp-block-code"><code>python3 --version
pip3 --version</code></pre>



<p><code>--version</code>はインストールされているバージョンを表示する指定です。バージョン番号が表示されれば、Pythonを実行できる状態です。</p>



<h2 class="wp-block-heading"><span id="toc5">3．Webアプリ用ディレクトリを作る</span></h2>



<pre class="wp-block-code"><code>sudo mkdir -p /var/www/html/python.example.com
sudo chown -R rocky:rocky /var/www/html/python.example.com
cd /var/www/html/python.example.com</code></pre>



<p><code>mkdir</code>はディレクトリを作るコマンドです。<code>-p</code>を付けると、途中のディレクトリが存在しない場合もまとめて作成します。</p>



<p><code>chown</code>は所有者を変更するコマンドです。<code>-R</code>は内部のファイルやディレクトリにも再帰的に設定する指定です。ここではRocky Linuxへログインしているユーザー名を<code>rocky</code>としています。別のユーザーを使っている場合は読み替えてください。</p>



<p><code>cd</code>は作業するディレクトリを移動するコマンドです。</p>



<h2 class="wp-block-heading"><span id="toc6">4．Pythonの仮想環境を作る</span></h2>



<pre class="wp-block-code"><code>python3 -m venv venv</code></pre>



<p><code>python3</code>でPython 3を起動し、<code>-m venv</code>で仮想環境を作る標準機能を呼び出しています。最後の<code>venv</code>は作成するディレクトリ名です。</p>



<p>仮想環境とは、Webアプリ専用のPython環境です。OS全体へFlaskなどを直接インストールせず、アプリごとに使用するパッケージとバージョンを分けられます。</p>



<p>続いて仮想環境を有効にします。</p>



<pre class="wp-block-code"><code>source venv/bin/activate</code></pre>



<p><code>source</code>は指定した設定ファイルを現在のシェルへ読み込むコマンドです。実行後、プロンプトの先頭に<code>(venv)</code>と表示されれば有効になっています。</p>



<p>仮想環境を終了するときは次を実行します。</p>



<pre class="wp-block-code"><code>deactivate</code></pre>



<p>SSHで再ログインした後は仮想環境が解除されていますが、作り直す必要はありません。アプリのディレクトリへ移動し、再び<code>source venv/bin/activate</code>を実行します。</p>



<h2 class="wp-block-heading"><span id="toc7">5．pipを更新してFlaskとGunicornを入れる</span></h2>



<pre class="wp-block-code"><code>python -m pip install --upgrade pip
pip install flask gunicorn</code></pre>



<p><code>python -m pip</code>は、現在有効になっているPython環境のpipを実行する書き方です。<code>install</code>はインストール、<code>--upgrade</code>は既に入っているパッケージを新しいバージョンへ更新する指定です。</p>



<p>2行目ではFlaskとGunicornを仮想環境内へインストールしています。</p>



<ul class="wp-block-list">
<li><code>flask</code>：URLとPythonの処理を結び付け、Web画面を作るフレームワーク</li>



<li><code>gunicorn</code>：Flaskアプリを本番サーバーで動かすためのWSGIサーバー</li>
</ul>



<h2 class="wp-block-heading"><span id="toc8">6．最小のFlaskアプリを作る</span></h2>



<p><code>/var/www/html/python.example.com/app.py</code>を次の内容で作成します。</p>



<pre class="wp-block-code"><code>from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Python Webアプリが動いています"

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000)</code></pre>



<p><code>from flask import Flask</code>はFlask本体を読み込みます。<code>app = Flask(__name__)</code>はWebアプリを作成する処理です。</p>



<p><code>@app.route("/")</code>は、トップページのURLへアクセスされたときに、その直後の関数を実行する指定です。<code>return</code>でブラウザへ返す内容を指定しています。</p>



<p>最後の<code>app.run()</code>は、app.pyを直接実行した場合に127.0.0.1の8000番ポートで開発用サーバーを起動する処理です。127.0.0.1は同じサーバー内部からだけ接続できるアドレスです。</p>



<h2 class="wp-block-heading"><span id="toc9">7．開発用サーバーで確認する</span></h2>



<pre class="wp-block-code"><code>cd /var/www/html/python.example.com
source venv/bin/activate
python app.py</code></pre>



<p>別のSSH接続から次を実行します。</p>



<pre class="wp-block-code"><code>curl http://127.0.0.1:8000/</code></pre>



<p><code>curl</code>はコマンド上からHTTPアクセスを行うツールです。「Python Webアプリが動いています」と返れば、Flaskアプリは正常に動いています。</p>



<p>開発用サーバーは確認には便利ですが、本番公開を目的としたものではありません。本番ではGunicornを使います。</p>



<h2 class="wp-block-heading"><span id="toc10">8．Gunicornで起動する</span></h2>



<pre class="wp-block-code"><code>gunicorn --bind 127.0.0.1:8000 app:app</code></pre>



<p><code>gunicorn</code>はアプリケーションサーバーを起動するコマンドです。<code>--bind 127.0.0.1:8000</code>は、同じサーバー内部の8000番ポートで待ち受ける指定です。</p>



<p>最後の<code>app:app</code>は、コロンの左側がファイル名<code>app.py</code>、右側がファイル内で作成した<code>app</code>変数を示します。</p>



<p>すでに8000番ポートが使用されている場合は「Address already in use」と表示されます。使用中のプロセスは次のコマンドで確認できます。</p>



<pre class="wp-block-code"><code>sudo ss -lntp | grep :8000</code></pre>



<p><code>ss</code>は通信ポートの使用状況を調べるコマンド、<code>-lntp</code>は待ち受け中のTCPポートとプロセスを数値表示する指定です。<code>grep :8000</code>で8000番を含む行だけに絞っています。</p>



<h2 class="wp-block-heading"><span id="toc11">9．Apacheでpython.example.comだけを転送する</span></h2>



<p>Rocky LinuxではApacheのサービス名とパッケージ名は<code>httpd</code>です。</p>



<pre class="wp-block-code"><code>sudo dnf install -y httpd
sudo systemctl enable --now httpd</code></pre>



<p><code>systemctl</code>はサービスを管理するコマンドです。<code>enable</code>はOS起動時の自動起動を有効にし、<code>--now</code>は自動起動の設定と同時に、その場でサービスも起動します。</p>



<p><code>/etc/httpd/conf.d/python.example.com.conf</code>を作り、次の設定を記載します。</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName python.example.com

    ErrorLog /var/log/httpd/python.example.com-error.log
    CustomLog /var/log/httpd/python.example.com-access.log combined

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/
&lt;/VirtualHost&gt;</code></pre>



<p><code>VirtualHost *:80</code>はHTTPの80番ポート用設定、<code>ServerName</code>は対象のホスト名です。ProxyPassをこのVirtualHostの中へ記載することで、<code>python.example.com</code>へのアクセスだけがGunicornへ転送されます。他のホストには影響しません。</p>



<p><code>ProxyPreserveHost On</code>は、ブラウザから送られたホスト名を転送先へ引き継ぎます。<code>ProxyPass</code>はApacheからGunicornへ転送する設定、<code>ProxyPassReverse</code>はGunicornから返された応答ヘッダーを外部公開用に調整する設定です。</p>



<p>この構成ではApacheがファイルを直接表示しないため、基本的に<code>DocumentRoot</code>は必要ありません。ページの内容はFlaskが返します。</p>



<h2 class="wp-block-heading"><span id="toc12">10．Apacheの設定を確認して反映する</span></h2>



<pre class="wp-block-code"><code>sudo apachectl configtest
sudo systemctl reload httpd</code></pre>



<p><code>apachectl configtest</code>はApache設定の文法確認です。「Syntax OK」と表示されたことを確認してから反映します。</p>



<p><code>reload</code>はサービスを停止せずに設定を読み直す操作です。設定に誤りがある状態でいきなり再起動するより、安全に確認できます。</p>



<p>Rocky LinuxでSELinuxが有効な場合は、ApacheからGunicornへのネットワーク接続を許可します。</p>



<pre class="wp-block-code"><code>sudo setsebool -P httpd_can_network_connect 1</code></pre>



<p><code>setsebool</code>はSELinuxの許可項目を変更するコマンドです。<code>-P</code>を付けると再起動後も設定が維持されます。実際に転送するホストはApacheのVirtualHost設定で指定した<code>python.example.com</code>だけです。</p>



<h2 class="wp-block-heading"><span id="toc13">11．本番ではGunicornをsystemdへ登録する</span></h2>



<p>次のようなコマンドでGunicornをバックグラウンド起動することもできます。</p>



<pre class="wp-block-code"><code>gunicorn --bind 127.0.0.1:8000 --daemon app:app</code></pre>



<p>ただし、手動起動ではサーバー再起動後に自動で立ち上がらず、異常終了時の復旧やログ確認も面倒です。本番ではsystemdへサービスとして登録します。</p>



<p>サービス名は任意ですが、何の処理か分かりやすい<code>gunicorn-python-example.service</code>とします。</p>



<pre class="wp-block-code"><code>sudo vi /etc/systemd/system/gunicorn-python-example.service</code></pre>



<p>ファイルへ次を記載します。</p>



<pre class="wp-block-code"><code>&#91;Unit]
Description=Gunicorn for python.example.com
After=network.target

&#91;Service]
User=rocky
Group=rocky
WorkingDirectory=/var/www/html/python.example.com

ExecStart=/var/www/html/python.example.com/venv/bin/gunicorn     --workers 2     --bind 127.0.0.1:8000     app:app

Restart=always
RestartSec=5

&#91;Install]
WantedBy=multi-user.target</code></pre>



<h3 class="wp-block-heading"><span id="toc14">systemd設定の意味</span></h3>



<ul class="wp-block-list">
<li><code>Description</code>：サービスの説明</li>



<li><code>After=network.target</code>：ネットワークの準備後に起動</li>



<li><code>User</code>と<code>Group</code>：Gunicornを実行するLinuxユーザーとグループ</li>



<li><code>WorkingDirectory</code>：アプリの作業ディレクトリ</li>



<li><code>ExecStart</code>：実際に起動するGunicornのコマンド</li>



<li><code>--workers 2</code>：リクエストを処理するワーカープロセスを2つ起動</li>



<li><code>Restart=always</code>：Gunicornが停止した場合に再起動</li>



<li><code>RestartSec=5</code>：再起動まで5秒待機</li>



<li><code>WantedBy=multi-user.target</code>：通常のサーバー起動時に利用するサービスとして登録</li>
</ul>



<p>仮想環境を<code>source</code>で有効にしなくても、ExecStartで仮想環境内のGunicornをフルパス指定しているため、正しい環境が使用されます。</p>



<h2 class="wp-block-heading"><span id="toc15">12．サービスを登録して起動する</span></h2>



<pre class="wp-block-code"><code>sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn-python-example</code></pre>



<p><code>daemon-reload</code>は新しく作成・変更したsystemd設定を読み直す操作です。設定ファイルを編集しただけではsystemdへ反映されないため、必ず実行します。</p>



<p><code>enable --now</code>により、自動起動の有効化と現在の起動を同時に行います。状態は次で確認できます。</p>



<pre class="wp-block-code"><code>sudo systemctl status gunicorn-python-example</code></pre>



<p><code>active (running)</code>と表示されれば起動しています。</p>



<h2 class="wp-block-heading"><span id="toc16">13．プログラム更新後の反映とログ確認</span></h2>



<p><code>app.py</code>などを変更した後は、Gunicornを再起動します。</p>



<pre class="wp-block-code"><code>sudo systemctl restart gunicorn-python-example</code></pre>



<p>Pythonプログラムだけを変更した場合、Apacheを再起動する必要はありません。ApacheのVirtualHost設定を変更した場合だけ、構文確認後にApacheをreloadします。</p>



<p>Gunicornのログを直近100件確認する場合は次を実行します。</p>



<pre class="wp-block-code"><code>sudo journalctl -u gunicorn-python-example -n 100</code></pre>



<p>リアルタイムで確認する場合は次のとおりです。</p>



<pre class="wp-block-code"><code>sudo journalctl -u gunicorn-python-example -f</code></pre>



<p><code>journalctl</code>はsystemdが管理するログを表示するコマンドです。<code>-u</code>は対象サービス、<code>-n 100</code>は直近100件、<code>-f</code>は新しいログを継続表示する指定です。</p>



<h2 class="wp-block-heading"><span id="toc17">よくあるトラブル</span></h2>



<h3 class="wp-block-heading"><span id="toc18">502 Proxy Errorが表示される</span></h3>



<p>ApacheからGunicornへ接続できていない可能性があります。Gunicornの状態、8000番ポート、SELinuxを順番に確認します。</p>



<pre class="wp-block-code"><code>sudo systemctl status gunicorn-python-example
sudo ss -lntp | grep :8000
sudo journalctl -u gunicorn-python-example -n 100</code></pre>



<h3 class="wp-block-heading"><span id="toc19">Address already in useと表示される</span></h3>



<p>手動で起動したGunicornが残っている、または別のアプリが8000番ポートを使っています。使用中のプロセスを確認し、同じアプリを二重に起動しないようにします。</p>



<h3 class="wp-block-heading"><span id="toc20">Apacheの既存サイトまで転送される</span></h3>



<p><code>ProxyPass</code>がVirtualHostの外側に書かれていないか確認します。<code>python.example.com</code>のVirtualHost内だけに記載すれば、このホストだけが転送対象になります。</p>



<h2 class="wp-block-heading"><span id="toc21">まとめ</span></h2>



<p>Pythonのコード自体は比較的読みやすい一方、Webアプリを本番公開するにはFlask、Gunicorn、Apache、systemdの役割を理解する必要があります。最初は登場人物が多く見えますが、それぞれの役割は明確です。</p>



<ul class="wp-block-list">
<li>Flask：Webアプリの処理を作る</li>



<li>Gunicorn：Flaskアプリを本番環境で動かす</li>



<li>Apache：外部からのアクセスを受け付けてGunicornへ転送する</li>



<li>systemd：Gunicornの起動、停止、自動起動、再起動、ログを管理する</li>
</ul>



<p>まずはトップページに文字を表示する最小アプリを動かし、仕組みを確認してからHTMLテンプレート、MySQL、投稿フォーム、ログイン機能などを追加していくと理解しやすくなります。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/o008odgrqz2gfjy0z7wx43kdvrxxk973/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
