<?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>MySQL  |  takeHo（たけほ）のへなちょこ台帳</title>
	<atom:link href="https://blog.takeho.com/tag/mysql/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>MySQL  |  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>DockerでYii2開発環境を最短構築！初心者でもできるMySQL・phpMyAdmin・Mailpit連携ガイド</title>
		<link>https://blog.takeho.com/build-your-yii2-development-environment-with-docker-in-no-time-a-beginners-guide-to-integrating-mysql-phpmyadmin-and-mailpit/</link>
					<comments>https://blog.takeho.com/build-your-yii2-development-environment-with-docker-in-no-time-a-beginners-guide-to-integrating-mysql-phpmyadmin-and-mailpit/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Fri, 12 Sep 2025 11:24:00 +0000</pubDate>
				<category><![CDATA[Yii2]]></category>
		<category><![CDATA[ウェブ・開発]]></category>
		<category><![CDATA[Composer]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Mailpit]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=1275</guid>

					<description><![CDATA[本記事では、Windows環境で Yii2（Basicテンプレート） を用いたPHPアプリ開発用に、MySQL、phpMyAdmin、そしてテスト用SMTPサーバ Mailpit（メール受信UI付き）を Docker で [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>本記事では、Windows環境で <strong>Yii2（Basicテンプレート）</strong> を用いたPHPアプリ開発用に、<strong>MySQL</strong>、<strong>phpMyAdmin</strong>、そしてテスト用SMTPサーバ <strong>Mailpit</strong>（メール受信UI付き）を <strong>Docker</strong> で一気に構築する手順を解説します。プロジェクトのルートは次のパスに固定します。</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>ルート</td><td><code>C:\Work\Workspace\Docker\example.com</code></td></tr><tr><td>フレームワーク本体</td><td><code>C:\Work\Workspace\Docker\example.com\htdocs</code></td></tr><tr><td>ドキュメントルート</td><td> <code>C:\Work\Workspace\Docker\example.com\htdocs\web</code></td></tr><tr><td>Webアクセス</td><td><a href="http://localhost:8080"> http://localhost:8080</a></td></tr><tr><td>phpMyAdmin</td><td><a href="http://localhost:8081">http://localhost:8081</a></td></tr></tbody></table></figure>



<p>この記事は <strong>「そのままコピペで動く」</strong> を重視しています。</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">事前準備とディレクトリ構成</a></li><li><a href="#toc2" tabindex="0">docker-compose.yml の作成</a></li><li><a href="#toc3" tabindex="0">PHP（php-fpm）用 Dockerfile と設定</a></li><li><a href="#toc4" tabindex="0">Nginx の設定</a></li><li><a href="#toc5" tabindex="0">php.ini の最低限の調整</a></li><li><a href="#toc6" tabindex="0">コンテナの起動と初期化</a></li><li><a href="#toc7" tabindex="0">Yii2 のインストール（コンテナ内でComposer実行）</a></li><li><a href="#toc8" tabindex="0">DB接続設定（.env と Yii2設定）</a></li><li><a href="#toc9" tabindex="0">Mailpit（メール送信テスト）設定</a></li><li><a href="#toc10" tabindex="0">動作確認（Web・DB・Mail）</a></li><li><a href="#toc11" tabindex="0">トラブルシューティング</a><ol><ol><li><a href="#toc12" tabindex="0">ポート競合</a></li><li><a href="#toc13" tabindex="0">MySQLへ接続できない</a></li><li><a href="#toc14" tabindex="0">Windows で権限由来の書き込みエラー</a></li><li><a href="#toc15" tabindex="0">ファイルの改行コード</a></li><li><a href="#toc16" tabindex="0">古いコンテナやイメージの削除でエラー</a></li><li><a href="#toc17" tabindex="0">Composerが遅い/タイムアウト</a></li></ol></li></ol></li><li><a href="#toc18" tabindex="0">よく使うコマンド集</a></li><li><a href="#toc19" tabindex="0">あとがき</a><ol><ol><ol><li><a href="#toc20" tabindex="0">引用元</a></li></ol></li></ol></li></ol></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">事前準備とディレクトリ構成</span></h2>



<p>以下のフォルダを作成します。</p>



<pre class="wp-block-code"><code>C:\Work\Workspace\Docker\example.com\
├─ htdocs\ ← Yii2のアプリ本体が入る（後で自動生成）
├─ docker\
│ ├─ php\
│ │ ├─ Dockerfile
│ │ └─ php.ini
│ └─ nginx\
│ └─ default.conf
└─ docker-compose.yml</code></pre>



<h2 class="wp-block-heading"><span id="toc2">docker-compose.yml の作成</span></h2>



<p><strong>ファイル:</strong> <code>C:\Work\Workspace\Docker\example.com\docker-compose.yml</code></p>



<pre class="wp-block-code"><code>services:
  nginx:
    image: nginx:1.27-alpine
    container_name: example_nginx
    ports:
      - "8080:80" # Web → http://localhost:8080
    volumes:
      - ./htdocs:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php
    networks:
      - appnet

  php:
    build:
      context: ./docker/php
      dockerfile: Dockerfile
    container_name: example_php
    volumes:
      - ./htdocs:/var/www/html
    environment:
      PHP_MEMORY_LIMIT: 512M
      PHP_MAX_EXECUTION_TIME: 120
    networks:
      - appnet
  db:
    image: mysql:8.0
    container_name: example_db
    command: &#91;"mysqld", "--default-authentication-plugin=mysql_native_password", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_0900_ai_ci"]
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: example
      MYSQL_USER: app
      MYSQL_PASSWORD: apppass
      TZ: Asia/Tokyo
    ports:
      - "3307:3306" # ローカルMySQLと衝突回避したい場合は3307→3306
    volumes:
      - dbdata:/var/lib/mysql
    networks:
      - appnet

  phpmyadmin:
    image: phpmyadmin:5-apache
    container_name: example_pma
    environment:
      PMA_HOST: db
      PMA_USER: root
      PMA_PASSWORD: root
    ports:
      - "8081:80" # phpMyAdmin → http://localhost:8081
    depends_on:
      - db
    networks:
      - appnet

  mailpit:
    image: axllent/mailpit:latest
    container_name: example_mailpit
    ports:
      - "1025:1025" # SMTP (アプリからの送信先)
      - "8025:8025" # Web UI → http://localhost:8025
    networks:
      - appnet

volumes:
  dbdata:

networks:
  appnet:
    driver: bridge</code></pre>



<div class="wp-block-cocoon-blocks-tab-caption-box-1 tab-caption-box block-box not-nested-style cocoon-block-tab-caption-box"><div class="tab-caption-box-label block-box-label box-label"><span class="tab-caption-box-label-text block-box-label-text box-label-text">補足</span></div><div class="tab-caption-box-content block-box-content box-content">
<p>既存でMySQLがWindowsに入っている場合、<code>ports</code> を <code>"3307:3306"</code> のように <strong>ホスト側を3307</strong> にすると衝突しません。</p>
</div></div>



<h2 class="wp-block-heading"><span id="toc3">PHP（php-fpm）用 Dockerfile と設定</span></h2>



<p><strong>ファイル:</strong> <code>C:\Work\Workspace\Docker\example.com\docker\php\Dockerfile</code></p>



<pre class="wp-block-code"><code>FROM php:8.3-fpm-alpine

# 必要拡張の導入
RUN apk add --no-cache \
    git zip unzip icu-dev oniguruma-dev libpng-dev libjpeg-turbo-dev libwebp-dev libzip-dev autoconf build-base \
 &amp;&amp; docker-php-ext-configure gd --with-jpeg --with-webp \
 &amp;&amp; docker-php-ext-install -j$(nproc) pdo_mysql intl mbstring gd zip exif bcmath opcache

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html</code></pre>



<h2 class="wp-block-heading"><span id="toc4">Nginx の設定</span></h2>



<p><strong>ファイル:</strong> <code>C:\Work\Workspace\Docker\example.com\docker\nginx\default.conf</code></p>



<pre class="wp-block-code"><code>server {
  listen 80;
  server_name example.com localhost;

  root /var/www/html/web;
  index index.php index.html;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ \.php$ {
    include       fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass php:9000;
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
  }

  location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?|ttf)$ {
    expires 7d;
    add_header Cache-Control "public";
  }
}</code></pre>



<h2 class="wp-block-heading"><span id="toc5">php.ini の最低限の調整</span></h2>



<p><strong>ファイル:</strong> <code>C:\Work\Workspace\Docker\example.com\docker\php\php.ini</code></p>



<pre class="wp-block-code"><code>memory_limit = ${PHP_MEMORY_LIMIT}
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
upload_max_filesize = 32M
post_max_size = 32M
date.timezone = Asia/Tokyo</code></pre>



<h2 class="wp-block-heading"><span id="toc6">コンテナの起動と初期化</span></h2>



<p><strong>PowerShell</strong> で次を実行します。</p>



<pre class="wp-block-code"><code>cd C:\Work\Workspace\Docker\example.com
# 初回ビルド＆起動
docker compose up -d --build

# 状態確認
docker compose ps</code></pre>



<ul class="wp-block-list">
<li><code>http://localhost:8080</code> → Nginx（Yii2はまだ未導入なので404/エラー想定）</li>



<li><code>http://localhost:8081</code> → phpMyAdmin（<code>root</code> / <code>root</code>、サーバ: <code>db</code> でログイン可）</li>



<li><code>http://localhost:8025</code> → Mailpit（受信ボックスUI）</li>
</ul>



<h2 class="wp-block-heading"><span id="toc7">Yii2 のインストール（コンテナ内でComposer実行）</span></h2>



<p><code>htdocs</code> に Yii2 Basic を展開します。</p>



<pre class="wp-block-code"><code># PHPコンテナに入る
docker compose exec php sh

# コンテナ内で実行（/var/www/html）
composer create-project yiisoft/yii2-app-basic .
exit</code></pre>



<p>作成後、Windows側では <code>C:\Work\Workspace\Docker\example.com\htdocs</code> にファイルが展開されます。Nginx の <code>root</code> は <code>/web</code> 配下なので、<code>web/index.php</code> が入口になります。</p>



<h2 class="wp-block-heading"><span id="toc8">DB接続設定（.env と Yii2設定）</span></h2>



<p>Yii2 Basic はデフォルトで <code>.env</code> はありませんが、設定を分けたい場合は導入も可能です。ここではシンプルに <code>config/db.php</code> を直接編集します。</p>



<p><strong>ファイル:</strong> <code>htdocs\config\db.php</code></p>



<pre class="wp-block-code"><code>&lt;?php
return &#91;
  'class' => 'yii\db\Connection',
  'dsn' => 'mysql:host=db;port=3306;dbname=example',
  'username' => 'app',
  'password' => 'apppass',
  'charset' => 'utf8mb4',
  'attributes' => &#91;
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_STRINGIFY_FETCHES => false,
  ],
];</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>host</code> は <code><strong>db</strong></code>（docker-compose のサービス名）を指定します。ホストOSから直接接続する場合は <code>localhost:3307</code> です。</p>
</blockquote>



<p>初期テーブルを作る例として、マイグレーションを1本作ってみます（任意）。</p>



<pre class="wp-block-code"><code># PHPコンテナに入る
docker compose exec php sh

# マイグレーション作成
php yii migrate/create create_post_table

# 生成されたファイル(例: migrations/mYYYYMMDD_HHMMSS_create_post_table.php)を編集
exit</code></pre>



<p>サンプル（生成ファイルを編集）</p>



<pre class="wp-block-code"><code>public function safeUp()
{
  $this->createTable('{{%post}}', &#91;
    'id' => $this->primaryKey(),
    'title' => $this->string()->notNull(),
    'body' => $this->text(),
    'created_at' => $this->dateTime()->defaultExpression('CURRENT_TIMESTAMP'),
  ]);
}


public function safeDown()
{
  $this->dropTable('{{%post}}');
}</code></pre>



<pre class="wp-block-code"><code># もう一度入って実行
docker compose exec php sh
php yii migrate --interactive=0
exit</code></pre>



<h2 class="wp-block-heading"><span id="toc9">Mailpit（メール送信テスト）設定</span></h2>



<p>開発中のメールは <strong>本当に送らない</strong> のが安全です。Mailpit に流してUIで確認しましょう。</p>



<p><strong>ファイル:</strong> <code>htdocs\config\web.php</code>（<code>components</code> に <code>mailer</code> を追記）</p>



<pre class="wp-block-code"><code>'components' => &#91;
  // ... 既存設定 ...
  'mailer' => &#91;
    'class' => 'yii\swiftmailer\Mailer',
    'useFileTransport' => false, // falseでSMTPへ送信
    'transport' => &#91;
      'class' => 'Swift_SmtpTransport',
      'host' => 'mailpit',
      'port' => 1025,
      'encryption' => null,
    ],
  ],
],</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>もし Yii2 を <code>yiisoft/mailer</code> に置き換える場合は適宜設定を読み替えてください（SwiftMailerは非推奨化済みですが、Yii2 Basic既定の形のままでもMailpit連携は可能です）。</p>
</blockquote>



<p>テスト送信例（コントローラやコンソールから）</p>



<pre class="wp-block-code"><code>Yii::$app->mailer->compose()
         ->setFrom('no-reply@example.com')
         ->setTo('test@example.com')
         ->setSubject('Mailpit送信テスト')
         ->setTextBody('これはテスト本文です')
         ->send();</code></pre>



<p><code>http://localhost:8025</code> を開くと、受信メールが一覧表示されます。</p>



<h2 class="wp-block-heading"><span id="toc10">動作確認（Web・DB・Mail）</span></h2>



<ol class="wp-block-list">
<li><strong>Web</strong><br><code>http://localhost:8080</code> にアクセスして Yii2 のトップが表示されること。</li>



<li><strong>DB</strong><br><code>http://localhost:8081</code> → phpMyAdmin に <code>root/root</code> でログイン。<code>example</code> データベースに <code>post</code> テーブルができていること。</li>



<li><strong>Mail</strong><br>サンプルコードを実行し、<code>http://localhost:8025</code> の受信箱にメールが入ること。</li>
</ol>



<h2 class="wp-block-heading"><span id="toc11">トラブルシューティング</span></h2>



<h4 class="wp-block-heading"><span id="toc12">ポート競合</span></h4>



<p><code>8080</code>/<code>8081</code>/<code>8025</code>/<code>1025</code>/<code>3307</code> が使用中 → <code>docker-compose.yml</code> の <code>ports</code> 左側（ホスト側）を変更。</p>



<h4 class="wp-block-heading"><span id="toc13">MySQLへ接続できない</span></h4>



<ul class="wp-block-list">
<li><code>host</code> は <code><strong>db</strong></code>（Docker内部）か、ホスト接続では <code>localhost:3307</code> を指定。</li>



<li>初回起動直後はDBの立ち上がり待ちが必要。数秒待って再試行。</li>
</ul>



<h4 class="wp-block-heading"><span id="toc14">Windows で権限由来の書き込みエラー</span></h4>



<p><code>htdocs/runtime</code> と <code>htdocs/web/assets</code> の書き込み権限が必要。コンテナ側で <code>chmod -R 777 runtime web/assets</code>（開発用途限定）。</p>



<h4 class="wp-block-heading"><span id="toc15">ファイルの改行コード</span></h4>



<p>Nginxやシェル用ファイルは <strong>LF</strong> を推奨。VS Codeで <code>LF</code> へ変換。</p>



<h4 class="wp-block-heading"><span id="toc16">古いコンテナやイメージの削除でエラー</span></h4>



<p>例: <code>Error response from daemon: conflict: unable to delete ... image is being used by stopped container ...</code></p>



<pre class="wp-block-code"><code>docker ps -a # 停止中を含め一覧
docker rm &lt;CONTAINER_ID> # 停止中コンテナを削除
docker rmi &lt;IMAGE_ID> # 参照がなくなったらイメージ削除
# 一括停止・削除（このプロジェクト内）
docker compose down -v # コンテナ・ネットワーク・ボリューム削除
docker system prune -f # 不要なリソース掃除</code></pre>



<h4 class="wp-block-heading"><span id="toc17">Composerが遅い/タイムアウト</span></h4>



<p>一時的に <code>-vvv</code> で状況確認。社内プロキシ下では環境変数の設定を。</p>



<h2 class="wp-block-heading"><span id="toc18">よく使うコマンド集</span></h2>



<pre class="wp-block-code"><code># 起動・ビルド
cd C:\Work\Workspace\Docker\example.com
docker compose up -d --build

# 停止
docker compose down

# 停止＋ボリュームごと削除（DB初期化したいとき）
docker compose down -v

# ログ参照
docker compose logs -f nginx
docker compose logs -f php
docker compose logs -f db

# コンテナに入る
docker compose exec php sh

# 権限（開発用）
docker compose exec php sh -c "chmod -R 777 runtime web/assets"</code></pre>



<h2 class="wp-block-heading"><span id="toc19">あとがき</span></h2>



<p>ここまでで、<strong>Windows + Docker</strong> 上に <strong>Yii2 + MySQL + phpMyAdmin + Mailpit</strong> の開発環境が整いました。実運用では Mailpit の代わりに本番SMTPを使いますが、開発段階では誤送信防止のため <strong>Mailpit固定</strong> を強くおすすめします。また、MySQLのパスワードや公開ポートは本番用に必ず見直してください。必要に応じて <strong>SSL</strong> や <strong>Xdebug</strong> の導入、<strong>Nginxのキャッシュ/セキュリティヘッダ</strong> の調整などを行うと、より快適で安全な環境になります。</p>



<div class="wp-block-cocoon-blocks-blank-box-1 blank-box block-box">
<h5 class="wp-block-heading"><span id="toc20">引用元</span></h5>



<ul class="wp-block-list">
<li>Yii Framework 公式ドキュメント（Yii2 Basicテンプレート）<br><a rel="noopener" href="https://www.yiiframework.com/doc/guide/2.0/ja" target="_blank">https://www.yiiframework.com/doc/guide/2.0/ja</a></li>



<li>Docker 公式ドキュメント<br><a rel="noopener" href="https://docs.docker.com/" target="_blank">https://docs.docker.com/</a></li>



<li>Docker Compose 公式ドキュメント<br><a rel="noopener" href="https://docs.docker.com/compose/" target="_blank">https://docs.docker.com/compose/</a></li>



<li>MySQL 公式ドキュメント<br><a rel="noopener" href="https://dev.mysql.com/doc/" target="_blank">https://dev.mysql.com/doc/</a></li>



<li>phpMyAdmin 公式サイト<br><a rel="noopener" href="https://www.phpmyadmin.net/" target="_blank">https://www.phpmyadmin.net/</a></li>



<li>Mailpit（開発用SMTP &amp; Web UIツール）公式リポジトリ<br><a rel="noopener" href="https://github.com/axllent/mailpit" target="_blank">https://github.com/axllent/mailpit</a></li>



<li>Nginx 公式ドキュメント<br><a href="https://nginx.org/en/docs/">https://nginx.org/en/docs/</a></li>



<li>PHP 公式マニュアル（Docker Hub版）<br><a rel="noopener" href="https://hub.docker.com/_/php" target="_blank">https://hub.docker.com/_/php</a></li>



<li>Composer 公式サイト<br><a rel="noopener" href="https://getcomposer.org/" target="_blank">https://getcomposer.org/</a></li>
</ul>
</div>



<p></p>



<p></p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/build-your-yii2-development-environment-with-docker-in-no-time-a-beginners-guide-to-integrating-mysql-phpmyadmin-and-mailpit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PHPアプリの性能最適化ガイド: 高速なWebサイト構築のポイント</title>
		<link>https://blog.takeho.com/performance-optimization-guide-for-php-apps-the-key-to-building-fast-websites/</link>
					<comments>https://blog.takeho.com/performance-optimization-guide-for-php-apps-the-key-to-building-fast-websites/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Sat, 08 Mar 2025 08:53:48 +0000</pubDate>
				<category><![CDATA[ウェブ・開発]]></category>
		<category><![CDATA[Cache]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=822</guid>

					<description><![CDATA[はじめに PHPを使ったWebサイトを構築する際、性能問題は必ずといっていいほど議論されるテーマです。サイトのロード時間を短縮し、スケーラビリティを向上させることで、ユーザー体験を劇的に改善できます。 本記事では、PHP [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">はじめに</h2>



<p>PHPを使ったWebサイトを構築する際、性能問題は必ずといっていいほど議論されるテーマです。サイトのロード時間を短縮し、スケーラビリティを向上させることで、ユーザー体験を劇的に改善できます。</p>



<p>本記事では、PHPの性能最適化に関する実践的な手法を詳しく解説します。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">1. コードレベルでできる性能向上の基礎</h2>



<h3 class="wp-block-heading">1-1. 型指定の活用</h3>



<p>PHP 7以降では型指定 (type hinting) を使用することで、インタプリタのオーバーヘッドを削減し、パフォーマンスを向上させることができます。</p>



<pre class="wp-block-code"><code>// 型指定なし
function add($a, $b) {
    return $a + $b;
}

// 型指定を追加
function add(int $a, int $b): int {
    return $a + $b;
}</code></pre>



<p>特に引数や戻り値の型指定を積極的に行うことで、エンジンの最適化が効きやすくなります。</p>



<h3 class="wp-block-heading">1-2. strict_types の利用</h3>



<p><code>declare(strict_types=1);</code> を利用すると、厳密な型チェックが行われ、予期しない型変換によるエラーを未然に防ぐことができます。</p>



<pre class="wp-block-code"><code>declare(strict_types=1);

function multiply(int $a, int $b): int {
    return $a * $b;
}</code></pre>



<p>これにより、意図しないデータ型の処理を防ぎ、処理の高速化が期待できます。</p>



<h3 class="wp-block-heading">1-3. ループの最適化</h3>



<p>PHPではループ処理がパフォーマンスに大きく影響します。ループ回数を減らし、計算を最小限に抑えることで、処理速度を向上させることができます。</p>



<pre class="wp-block-code"><code>// NG: ループのたびに関数を呼び出す
for ($i = 0; $i &lt; count($array); $i++) {
    echo $array&#91;$i];
}

// OK: count() を事前に変数に格納
$count = count($array);
for ($i = 0; $i &lt; $count; $i++) {
    echo $array&#91;$i];
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. データベースクエリの最適化</h2>



<h3 class="wp-block-heading">2-1. インデックスの適切な利用</h3>



<p>MySQLなどのRDBMSを使用する場合、適切なインデックスを設定することでクエリの速度を大幅に向上させることができます。</p>



<pre class="wp-block-code"><code>CREATE INDEX idx_status ON users (status);</code></pre>



<p>インデックスを適用することで、特定のカラムの検索処理が最適化され、データ取得速度が向上します。</p>



<h3 class="wp-block-heading">2-2. プリペアドステートメントの活用</h3>



<p>SQLインジェクションの防止だけでなく、クエリのコンパイルコストを削減し、パフォーマンスを向上させることができます。</p>



<pre class="wp-block-code"><code>$stmt = $pdo-&gt;prepare("SELECT name, email FROM users WHERE status = :status");
$stmt-&gt;execute(&#91;'status' =&gt; 'active']);</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. キャッシュを活用した最適化</h2>



<h3 class="wp-block-heading">3-1. OPcache の利用</h3>



<p>PHPのOPcacheを有効にすると、スクリプトのコンパイル結果をキャッシュし、実行速度を大幅に向上させることができます。</p>



<pre class="wp-block-code"><code>opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000</code></pre>



<h3 class="wp-block-heading">3-2. Redis を利用したデータキャッシュ</h3>



<p>Redisを利用してデータベースの負荷を軽減することができます。</p>



<pre class="wp-block-code"><code>$redis = new Redis();
$redis-&gt;connect('127.0.0.1', 6379);
$cacheKey = 'user_list';

if (!$redis-&gt;exists($cacheKey)) {
    $stmt = $pdo-&gt;query("SELECT * FROM users");
    $users = $stmt-&gt;fetchAll(PDO::FETCH_ASSOC);
    $redis-&gt;set($cacheKey, json_encode($users));
} else {
    $users = json_decode($redis-&gt;get($cacheKey), true);
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">4. フロントエンドとの連携による最適化</h2>



<h3 class="wp-block-heading">4-1. Gzip 圧縮の有効化</h3>



<p>サーバー側でGzip圧縮を有効にすると、転送量を削減し、ページの読み込み速度を向上させることができます。</p>



<pre class="wp-block-code"><code>output_buffering = On
zlib.output_compression = On</code></pre>



<h3 class="wp-block-heading">4-2. HTTP/2の導入</h3>



<p>HTTP/2を利用することで、並列リクエストを最適化し、リソースの取得を効率化できます。</p>



<pre class="wp-block-code"><code>server {
    listen 443 ssl http2;
    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">5. PHPフレームワークを活用した最適化</h2>



<h3 class="wp-block-heading">5-1. Laravel の Eager Loading</h3>



<p>ORM（Eloquent）を利用する際、N+1問題を避けるためにEager Loadingを活用すると、クエリ回数を大幅に削減できます。</p>



<pre class="wp-block-code"><code>// NG
$users = User::all();
foreach ($users as $user) {
    echo $user-&gt;posts-&gt;count();
}

// OK
$users = User::with('posts')-&gt;get();
foreach ($users as $user) {
    echo $user-&gt;posts-&gt;count();
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">おわりに</h2>



<p>本記事では、PHPのパフォーマンス最適化において重要なテクニックを幅広く解説しました。実際のプロジェクトに適用することで、Webサイトの高速化とスケーラビリティ向上を実現できます。</p>



<p>「細かい最適化を積み重ねること」が、高速なWebアプリを構築する鍵となります。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/performance-optimization-guide-for-php-apps-the-key-to-building-fast-websites/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ROOT権限パスワード無しでphpMyAdminへログインする</title>
		<link>https://blog.takeho.com/ogmwp9nq2g4yxk338m3mu6j7cvc1g923/</link>
					<comments>https://blog.takeho.com/ogmwp9nq2g4yxk338m3mu6j7cvc1g923/#respond</comments>
		
		<dc:creator><![CDATA[たけほ]]></dc:creator>
		<pubDate>Tue, 30 Jul 2024 21:55:00 +0000</pubDate>
				<category><![CDATA[ウェブ・開発]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://blog.takeho.com/?p=115</guid>

					<description><![CDATA[rootのパスワード不明のままデータベースを今後も稼働させなくてはならない最悪な状況下での一時的な救済処置です。 設定ファイルの確認 config.inc.phpファイルを確認します。 ソースからインストールした場合はそ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>rootのパスワード不明のままデータベースを今後も稼働させなくてはならない最悪な状況下での一時的な救済処置です。</p>



<h3 class="wp-block-heading"><span id="toc1">設定ファイルの確認</span></h3>



<p><strong>config.inc.php</strong>ファイルを確認します。</p>



<p>ソースからインストールした場合はそのディレクトリ内を検索し、yumなどパッケージ管理ツールでインストールした場合は次のディレクトリを確認します。</p>



<pre class="wp-block-code"><code>/etc/phpMyAdmin/</code></pre>



<h3 class="wp-block-heading"><span id="toc2">設定ファイルの更新</span></h3>



<p><strong>config.inc.php</strong>ファイルを編集します。</p>



<pre class="wp-block-code"><code>&lt;?php
以下の設定値を変更し設定値が無ければ追加する
$cfg&#91;'Servers']&#91;$i]&#91;'AllowRoot'] = true;
$cfg&#91;'Servers']&#91;$i]&#91;'AllowNoPasswordRoot'] = true;</code></pre>



<h3 class="wp-block-heading"><span id="toc3">アクセス確認</span></h3>



<p>phpMyAdminへrootのパスワード無しでログインできるか確認して完了</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.takeho.com/ogmwp9nq2g4yxk338m3mu6j7cvc1g923/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
