本ページはプロモーションが含まれています。

v6プラス(MAP-E)の回線で、自宅PCに常駐させたFlask製のWebアプリやAPIを外から使おうとしても、ルーターでポート開放ができずに行き詰まる。よくある悩みです。この記事では、ポート開放せずに公開する今の書き方として、127.0.0.1での待ち受け、systemdによる常駐、Cloudflare Tunnelの3点をコード付きで示します。あわせてConoHa VPS・さくらのVPSの月額と比べ、停電や再起動で何分止まるかを自分で実測するスクリプトも載せます。

v6プラス(MAP-E)で自宅のFlaskアプリを公開できない理由と、共有サーバーが候補外になる理由

MAP-Eでは「IPv4の80番・443番を自宅に向ける」ができない

v6プラスなどのMAP-E方式では、IPv4のアドレスを複数の契約者で共有します。各契約者が使えるポートは一部の範囲に限られます。そのため、以前のように「ルーターで80番・443番ポートを開け、自宅PCに転送して公開する」という構成が、そもそも組めません。

この制約は、IPv4で外から入ってくる通信にかかるものです。自宅から外へ出ていく通信は、普段どおり使えます。後述するCloudflare Tunnelは、この「外向きの通信」だけで公開を成り立たせる仕組みです。自宅から外へ接続を張りっぱなしにし、外部からのアクセスはその接続を通して自宅に届きます。そのためルーターのポート開放が要りません。

共有レンタルサーバーは「常駐できない」ので最初に外れる

「外から常時アクセスできるFlaskアプリ」には、プロセスがずっと起動していることが必要です。調査時点の共有レンタルサーバーは、次のとおりこの条件を満たしません。

サービス

cron

最短間隔

常駐プロセス

エックスサーバー

可

5分

基本的に不可(リソースを大量消費する常駐は停止対象)

ConoHa WING

可

1分

基本的に不可(規約外のリソース消費は停止対象)

ロリポップ!

可(エコノミープランは不可)

5分

不可

cronで決まった時刻に動かすバッチ処理であれば、共有サーバーでも十分に回せます。その書き方はレンタルサーバーcron 5分おきPython実行|26年11月にまとめました。ただ、リクエストを待ち受け続けるWebアプリやAPIは常駐が前提です。したがって候補は「自宅+Cloudflare Tunnel」か「VPS」の2択になります。

以前は「ポート開放+app.run」、今は「127.0.0.1待ち受け+systemd+Cloudflare Tunnel」

実行環境

  • OS:Linux(自宅PC・VPSどちらでも同じ手順。systemdが動いていること)
  • Python:3.10以上を想定(python3 --version で確認)
  • パッケージ:pip install flask waitress

バージョンは固定していません。この記事のコードは各ライブラリの基本的な機能しか使わないためです。導入されたバージョンは pip show flask waitress で確認できます。waitressは、Flask付属の開発用サーバーの代わりに使う本番向けのWSGIサーバーです。WSGIサーバーとは、PythonのWebアプリを常時公開するための待ち受け役のことです。

以前の書き方:0.0.0.0で待ち受けてnohupで放置

# app.py(以前の書き方)
from flask import Flask
app = Flask(__name__)

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

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
nohup python3 app.py &

以前は、すべての通信を受ける 0.0.0.0 で待ち受け、ルーターのポート開放で外から入れていました。この構成には問題が3つあります。まず、MAP-Eではポート開放そのものができません。次に、開発用サーバーを外部に直接さらしています。さらに、nohup で起動したプロセスは、落ちたときも再起動したときも自動では戻りません。

今の書き方1:アプリ本体(app.py)

# /home/username/app/app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/")
def index():
    return "hello"

@app.get("/health")
def health():
    return jsonify(status="ok")

アプリ本体には起動処理を書かず、ルーティングだけを置きます。/health は死活確認用のエンドポイントです。後で停止時間を測るときに使います。@app.get は古いFlaskでは使えません。動かない場合は、pip show flask でバージョンを確認するか、@app.route("/health", methods=["GET"]) と書き換えてください。

今の書き方2:起動スクリプト(serve.py)

# /home/username/app/serve.py
from waitress import serve
from app import app

serve(app, host="127.0.0.1", port=8000)

今の書き方で最も大きく変わるのが、待ち受け先です。以前の 0.0.0.0 から、同じPCの中からしか届かない 127.0.0.1 に変えています。外からのアクセスはCloudflare Tunnelが受け止め、このPCの中の8000番へ転送します。そのため、アプリ自体を外部に開く必要がありません。ルーターの設定も、ファイアウォールの穴あけも不要です。

今の書き方3:systemdで常駐させる(myapp.service)

# /etc/systemd/system/myapp.service
[Unit]
Description=My Flask app
After=network-online.target
Wants=network-online.target

[Service]
User=username
WorkingDirectory=/home/username/app
ExecStart=/home/username/app/.venv/bin/python /home/username/app/serve.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service

systemdは、Linuxでプロセスの起動と監視を受け持つ仕組みです。各ブロックの役割は次のとおりです。

  • [Unit]:ネットワークが使える状態になってから起動するよう指定します。
  • [Service]:一般ユーザー username の権限で、仮想環境のPythonから serve.py を起動します。Restart=always を指定しているので、プロセスが落ちても5秒後に自動で立ち上げ直します。
  • [Install]:enable したときに、OS起動時の自動起動に登録されます。

このファイルを /etc/systemd/system/ に置くにはroot権限(sudo)が必要です。自宅PCでもVPSでも自分がroot権限を持てるので、ここは問題になりません。共有サーバーではroot権限がなく、常駐も禁止されています。共有サーバーが候補外になるのは、ここが理由です。

今の書き方4:Cloudflare Tunnelで127.0.0.1:8000を公開する

自宅PCに、Cloudflareが提供する cloudflared を入れます。作業の流れは次の4段階です。

  1. Cloudflareアカウントで認証する
  2. トンネルを作成する
  3. 公開するホスト名(例:app.example.com)と転送先 http://127.0.0.1:8000 を結びつける
  4. cloudflared 自体もサービスとして常駐させる

コマンド名・設定ファイルの書式・無料プランで使える範囲・公開に使うドメインの条件は、変更されることがあります。この記事では具体的なオプション名を載せません。cloudflared --version と cloudflared --help で手元のバージョンと使えるサブコマンドを確認し、Cloudflare公式ドキュメントの「Cloudflare Tunnel」の手順に沿って進めてください。押さえておく点は1つです。cloudflared もsystemdのサービスとして自動起動に登録しておかないと、再起動後にアプリは動いているのに外から見えない状態になります。

停電・再起動で何分止まるか:自宅運用の停止時間を1分刻みで実測する

自宅運用の停止時間は、PCの電源設定・ルーターの起動の速さ・回線の種類で大きく変わります。人の環境で測った数値を当てはめても、あまり意味がありません。そこで、自分の環境で実測する仕組みを用意します。

healthcheck.py:1分ごとに応答を記録する

# /home/username/app/healthcheck.py
import datetime
import urllib.error
import urllib.request

URL = "http://127.0.0.1:8000/health"
LOG = "/home/username/app/health.log"

now = datetime.datetime.now().isoformat(timespec="seconds")
try:
    with urllib.request.urlopen(URL, timeout=5) as r:
        state = "OK" if r.status == 200 else f"NG{r.status}"
except (urllib.error.URLError, OSError):
    state = "NG"

with open(LOG, "a", encoding="utf-8") as f:
    f.write(f"{now}\t{state}\n")
# crontab -e で追加(毎分実行)
* * * * * /home/username/app/.venv/bin/python /home/username/app/healthcheck.py

このスクリプトは標準ライブラリだけで書いています。/health にアクセスし、応答があれば OK、なければ NG を、時刻と一緒に1行ずつ追記します。記録の結果は2種類の停止に分かれます。PC自体が止まっていれば、記録が途切れます。PCは動いていてアプリが落ちていれば、NG が並びます。

analyze_downtime.py:ログから停止区間を拾う

# /home/username/app/analyze_downtime.py
import datetime

LOG = "/home/username/app/health.log"
GAP = datetime.timedelta(seconds=90)

rows = []
with open(LOG, encoding="utf-8") as f:
    for line in f:
        ts, state = line.rstrip("\n").split("\t")
        rows.append((datetime.datetime.fromisoformat(ts), state))

ng_start = None
for (prev_ts, _), (ts, state) in zip(rows, rows[1:]):
    if ts - prev_ts > GAP:
        print(f"記録なし(PC停止) {prev_ts} → {ts}  {ts - prev_ts}")
    if state != "OK" and ng_start is None:
        ng_start = ts
    if state == "OK" and ng_start is not None:
        print(f"応答なし(アプリ停止) {ng_start} → {ts}  {ts - ng_start}")
        ng_start = None

隣り合う記録の間隔が90秒を超えていれば、cronが動いていなかった区間、つまりPC停止として出力します。NG が始まってから次に OK に戻るまでは、アプリ停止として出力します。毎分の記録なので、測れる精度は1分単位です。

試し方は簡単です。sudo reboot で再起動する、あるいはコンセントを抜いて戻すと、そのときの停止時間がそのまま出てきます。コンセントを抜いた後に電源が入らない場合は、BIOS/UEFIに「AC電源が戻ったら自動で起動する」という設定があるか確認してください。項目名は機種ごとに異なるので、マザーボードやPCのマニュアルを見るのが確実です。この設定がないと、停電が復旧しても人が電源ボタンを押すまでアプリは止まったままです。

外から見えているかは別の場所から測る

上の記録は、PCの中から見た結果です。ルーター・回線・トンネルの停止は拾えません。外部からの到達性を測るには、別の環境で同じ healthcheck.py を動かします。その際、URL は https://app.example.com/health に変えてください。PCの中の記録と外からの記録を比べれば、停止の原因が「PC・アプリ側」か「回線・トンネル側」かを切り分けられます。

常駐アプリが外部サイトを取りに行くときの配慮:robots.txt・利用規約・アクセス間隔

常駐アプリやAPIの中で、他のサイトから情報を取得する場合もあるでしょう。常駐させると「いつでも取りに行ける」状態になるため、相手のサーバーへの配慮を最初から組み込んでおきます。

import time
from urllib import robotparser

UA = "my-bot"
rp = robotparser.RobotFileParser("https://example.com/robots.txt")
rp.read()

target = "https://example.com/list"
if rp.can_fetch(UA, target):
    # ここで1回だけ取得し、結果は保存して使い回す
    delay = rp.crawl_delay(UA)
    time.sleep(delay if delay else 10)
  • robots.txt:取得してよいパスかどうかを can_fetch で確認します。待ち時間(Crawl-delay)の指定があれば、それに従います。
  • 利用規約:自動取得を禁止しているサイトや、APIの利用を求めているサイトは、その指示に従います。robots.txtで許可されていても、規約で禁止されていれば取得しません。
  • アクセス間隔:リクエストのたびに相手サイトへ取りに行く作りにはしません。決まった時刻に1回取得し、結果を保存して使い回します。連続して取得するときは、上の例のように十分な間隔を空けます。
  • 制限を回避しない:共有サーバーのcron間隔や常駐禁止を、sleep() でプロセスを延命させてすり抜ける方法が使われることがあります。しかしこれは規約違反になりえます。リソース制限で停止されたり、アカウントを凍結されたりするリスクもあります。常駐が必要なら、常駐が許されている自宅かVPSを選ぶのが正しい解決です。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

ConoHa VPS・さくらのVPSの月額と自宅+Cloudflare Tunnelの比較:止まる条件で選ぶ

月額・スペックの比較

以下は調査時点で確認できた目安です(「〜」は最安構成からの表記)。料金やキャンペーンは改定されることがあるので、契約前に各社公式の料金ページで現行額を必ず確認してください。

構成

月額の目安

スペック

root権限・常駐

補足

ConoHa VPS 1GB

約680円〜

仮想2コア/メモリ1GB/SSD 50GB

可・可

時間課金と長期割引あり。最大2ヶ月無料などのキャンペーンを定期的に実施

ConoHa VPS 2GB

約1,360円〜

仮想3コア/メモリ2GB/SSD 100GB

可・可

NVMe SSDによる高いI/O性能が特徴

さくらのVPS 1G

約687円〜

仮想1コア/メモリ1GB/SSD 50GB

可・可

基本的に無料期間なし

さくらのVPS 2G

約1,375円〜

仮想2コア/メモリ2GB/SSD 100GB

可・可

国内データセンター

自宅+Cloudflare Tunnel(無料プラン)

電気代のみ(常時稼働で数千円/月になることも)

手持ちのPC次第

可・可

無料プランの範囲は公式ドキュメントで確認

自宅の電気代は、平均消費電力(W)× 24 × 30 ÷ 1000 × 電力単価(円/kWh) で月額を見積もれます。電力単価は電気の検針票か契約先のサイトで確認できます。手持ちのPCを常時稼働させると、VPSの1GBプランより高くつくこともあります。騒音・排熱・置き場所の問題も、自宅運用のコストとして見込んでおく必要があります。

それぞれが止まる条件

構成

止まる条件

自動で戻るか

自宅+Cloudflare Tunnel

停電/ルーター・ONUの再起動/回線のメンテナンス/OS更新に伴う再起動/PCの故障

systemdへの登録とAC復帰時の自動起動を設定していれば戻る。停電中と回線断の間は戻れない

VPS(ConoHa・さくら)

事業者のメンテナンス/自分で行うOS更新の再起動/1GBプランでのメモリ不足

systemdに登録していれば再起動後に戻る

共有レンタルサーバー

そもそも常駐できない

候補外

選び方

  • 自宅+Cloudflare Tunnelでよい場合:自分や身内だけが使うツールで、停電や回線断で数分〜数時間止まっても困らない。手持ちのPCを流用でき、電気代の見積もりがVPSの月額を下回る。
  • VPSを選ぶべき場合:取引先や外部サービスから呼ばれるAPI、Webhookの受け口など、止まると相手側の処理が失敗するもの。家の電源・回線事情に左右されたくないもの。Webhookの受け口をどこに置くかはLINE Webhook エックスサーバーかVPSか|26年11月でも比較しています。
  • 迷ったら:まず自宅+Cloudflare Tunnelで動かし、上の analyze_downtime.py で1〜2ヶ月分の停止時間を記録します。その停止時間を許容できなければ、VPSに移します。コードは serve.py とsystemdのユニットファイルをそのまま持っていけば動きます。移行で書き換えが要るのは、パスとトンネルの設定だけです。

まとめ:ポート開放に頼らず、常駐と自動復帰を先に作る

v6プラス(MAP-E)回線では、ポート開放による自宅公開はできません。今の書き方は次の3点です。アプリは127.0.0.1で待ち受ける。systemdの Restart=always と enable で常駐と自動起動を任せる。外部への公開はCloudflare Tunnelの外向き接続で行う。共有レンタルサーバーは常駐が認められていないので、候補外です。自宅運用は電気代だけで済む反面、停電・回線断・AC復帰の設定漏れで止まります。その停止時間は healthcheck.py と analyze_downtime.py で自分の環境の実測値として把握できます。止まると相手が困るAPIやWebhookなら、ConoHa VPS・さくらのVPSの1GBプラン(調査時点で月額約680円〜)に置くのが確実です。料金は契約前に公式ページで確認してください。