ラズパイ 読む時間 約8分

ラズパイにセルフチェックと自己再生の機能を加えてみた:止まったプログラムや NAS を自分で直し、電源・熱・SD カードも自分で確認する

ラズパイが 1 時間に 1 回と起動のあとに、自分の電源・熱・SD カード・メモリー・NAS・時刻を確認し、止まったプログラムや画面は自分で起こし直す仕組みです。人に知らせず、自動で安定して動いている状態に戻すことを目的にしました。Python の標準の機能だけ・sudo なし・1 回 3 秒。

ラズパイにセルフチェックと自己再生の機能を加えてみた:止まったプログラムや NAS を自分で直し、電源・熱・SD カードも自分で確認する

ラズパイを 24 時間動かしていると、いろいろなことが起きます。Wi-Fi が切れる、動かしていたプログラムがいつの間にか止まっている、停電のあとに一部だけ動いていない、SD カードが傷んでくる。そのたびに、人が気づいて直すのは大変です。

この記事では、ラズパイが自分の状態を確認して(セルフチェック)、直せるものは直す(自己再生)仕組みを加えます。1 時間に 1 回と、起動したあとに、電源・熱・SD カード・メモリー・NAS・時刻・止まってはいけないプログラムを確認して、止まっているものは自分で起こし直します。

この記事の前にできていること

この記事でできるようになること

  • 1 時間に 1 回、ラズパイが自分の状態を 11 の項目で確認する(1 回 3 秒)
  • 止まっているプログラムや画面を、自分で起こし直す
  • 再起動や停電のあとに、必要なものが全部動いているかを確認して、前回いつ止まったかを記録する

知らせない、自分で戻る

最初は、おかしいことがあったらスピーカーやメールで知らせる形を考えました。ですが、知らせが来ても、たいていは「プログラムを起こし直す」「NAS をつなぎ直す」くらいの、決まった直し方しかありません。それならラズパイが自分でやればよく、人に知らせる必要はありません。

そこで、この仕組みは知らせを出さず、自動で安定して動いている状態に戻すことを目的にしました。自分では直せないもの(SD カードの傷み・電源の電圧不足など)だけは、結果のファイルに赤で残ります。わたしの家では、AI(Claude)が毎朝この結果を読み、赤が残っていたときだけ教えてくれる形にしています(これはこの仕組みの外の、わたしの家だけの使い方です)。

確認する項目

11 の項目を、4 つのまとまりで確認します。止まったものやつながりが切れたものは、自動で直します。

本体

  • 電源と熱:電圧不足や、熱で速さを落としていないか
  • 温度:70℃ で注意・80℃ で異常
  • メモリー:空きと、不足で止められたものがないか
  • ウォッチドッグ:固まったら自動で再起動する仕組みがオンか

保存

  • SD カード:「読むだけ」に切り替わっていないか・エラー・空き
  • ログの大きさ:10MB を超えたものがないか
  • OS の更新:更新の数と、情報がいつのものか

つながり

  • NAS:読めるか(読めなければ、つなぎ直す)自動で直す
  • 時刻:ネットの時刻と合っているか

動いているもの

  • 止まってはいけないもの:設定ファイルに書いた画面やプログラムが動いているか(止まっていれば、起こし直す)自動で直す
  • 起動の記録:いつ起動したか・前回いつ止まったか

結果は、項目ごとに「正常・注意・異常」の 3 つで残ります。自動で直したものは「注意」、直せないもの(SD カードの傷み・電源など)は「異常」です。

家のラズパイの結果

家のラズパイでは、結果を「ラズパイのホーム」(機能をカードで並べたページ)に出しています。● が正常、▲ が注意(自分で直したもの)、✕ が異常です。

自己診断のカード。電源と熱・温度 55.8℃・SD カード・メモリー・NAS・時刻・常に動かすもの(MeCab の API・ネットのつながりの記録)は正常の印。常に動かすもの:スクレイピングは「止まっていたので起こし直した」の注意の印。ログの大きさ・OS の更新・ウォッチドッグはオン・起動の記録
  • 止まっていた画面を、自分で起こし直した:試しに、スクレイピングの画面(自分だけのスクレイピング環境)をわざと止めてから点検したら、自分で起こし直して、数秒で開けるようになりました。カードには「止まっていたので起こし直した」と出て、次の点検で正常に戻ります。
  • ウォッチドッグは、はじめからオンだった:Raspberry Pi OS は、はじめから「1 分固まったら自動で再起動する」仕組みがオンになっていました(/usr/lib/systemd/system.conf.d/40-rpi-enable-watchdog.conf)。ラズパイ全体が固まったときの守りは、何もしなくても入っています。
  • 電源と熱:「起動してから、速さを落としたことがある」の記録が 1 つ残っていました(電圧不足の記録はなし)。今は正常なので、熱か負荷で一時的に絞ったようです。
  • OS の更新:更新の情報が 35 日前のままでした。sudo apt update をしばらくしていない、という合図です。

手順 1:止まってはいけないものを、設定ファイルに書く

起こし直す対象は、プログラムの中に書かずに、1 つずつ「設定ファイル」(JSON)に書きます。あとから機能を足すときは、設定ファイルを 1 つ置くだけです。

mkdir -p ~/raspi-home/apps ~/raspi-home/data

画面(Web のページ)なら URL で、画面のないプログラムなら名前で、動いているかを確認します。例として、スクレイピングの画面と、画面のないプログラムの 2 つです。

{
  "name": "スクレイピング",
  "keepalive": {
    "url": "http://127.0.0.1:8090/",
    "start": ["~/scraper/venv/bin/python", "app.py"],
    "cwd": "~/scraper",
    "log": "~/scraper/data/app.log",
    "wait": 10
  }
}
{
  "name": "ネットのつながりの記録",
  "keepalive": {
    "process": "notify/pinglog.py",
    "start": ["/usr/bin/python3", "/home/pi/notify/pinglog.py"],
    "wait": 3
  }
}

ここだけ自分の環境に変える(設定ファイル)

  • url:画面の URL。ラズパイの中から確認するので、ふつうは 127.0.0.1 と番号でよい(画面が家の LAN のアドレスだけで待ち受けているときは、そのアドレス)。開けなければ止まっていると見なす(404 などが返るだけなら、動いていると見なす)
  • process:画面のないプログラムは、動いているコマンドの一部(pgrep -f で探す文字)
  • start:起こし直すときのコマンド。1 つずつ分けて書く(ここに書いたものしか動かさない)
  • wait:起こしてから、動いたかを確認するまでの秒数

手順 2:セルフチェックのプログラムを設置する

~/raspi-home/selfcheck.py に、次のプログラムを設置します。Python 3 の標準の機能だけで動くので、追加のインストールはいりません。sudo も使いません。

#!/usr/bin/env python3
"""ラズパイのセルフチェックと自己再生
- 1 時間に 1 回(cron)と、起動の 3 分後(--boot)に動く。結果は data/selfcheck.json に書き、ラズパイのホームの「自己診断」のカードに出る。
- 確認するもの:電源と熱・温度・SD カード・メモリー・NAS・時刻・常に動かすもの・ログの大きさ・OS の更新の情報・ウォッチドッグ・起動の記録。
- 自分で直すもの:止まっている画面やプログラムを起こす(設定ファイル apps/*.json の keepalive に書いたものだけ)・NAS は読みに行って自動でつなぎ直させる。
- 直せないもの(SD の傷み・電源・OS の更新・ウォッチドッグの設定)は、結果のファイルに残すだけ(知らせはしない)。sudo は使わない。
- Python 3 の標準のものだけで動く。
"""
import glob
import json
import os
import re
import shutil
import subprocess
import sys
import time
from datetime import datetime
BASE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(BASE, "data")
OUT = os.path.join(DATA, "selfcheck.json")
LOG = os.path.join(DATA, "selfcheck.log")
HEARTBEAT = os.path.join(DATA, "heartbeat")
BOOTLOG = os.path.join(DATA, "boot.log")
ORDER = {"ok": 0, "info": 1, "warn": 2, "ng": 3}
LOGS_TO_WATCH = ["~/backup/backup.log", "~/notify/notify.log", "~/notify/home_report.log", "~/notify/nas_catalog.log",
                 "~/scraper/data/fetch.log", "~/scraper/data/app.log", "~/raspi-home/data/*.log"]
def now():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def fmt(ts):
    return datetime.fromtimestamp(ts).strftime("%m/%d %H:%M")
def run(argv, timeout=10):
    try:
        r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
        return r.returncode, r.stdout.strip()
    except Exception as e:
        return 99, "%s" % type(e).__name__
def log(msg):
    with open(LOG, "a", encoding="utf-8") as f:
        f.write("%s\t%s\n" % (now(), msg))
# ---- 確認するもの(1 つずつ (名前, 状態, 文) を返す) ----
def check_power():
    rc, out = run(["vcgencmd", "get_throttled"])
    m = re.search(r"0x([0-9a-fA-F]+)", out)
    if not m:
        return "電源と熱", "info", "読めませんでした"
    v = int(m.group(1), 16)
    now_flags = {0: "電圧が足りない", 1: "速さを落としている", 2: "絞っている", 3: "熱で抑えている"}
    past_flags = {16: "電圧が足りなかった", 17: "速さを落としたことがある", 18: "絞ったことがある", 19: "熱で抑えたことがある"}
    cur = [t for b, t in now_flags.items() if v & (1 << b)]
    past = [t for b, t in past_flags.items() if v & (1 << b)]
    if cur:
        return "電源と熱", "ng", "今:" + "・".join(cur) + "(電源のアダプターや熱を見てください)"
    if v & (1 << 16):
        return "電源と熱", "warn", "起動してから:" + "・".join(past) + "(電源のアダプターを見てください)"
    if past:
        return "電源と熱", "ok", "今は正常(起動してから:" + "・".join(past) + ")"
    return "電源と熱", "ok", "正常"
def check_temp():
    try:
        t = int(open("/sys/class/thermal/thermal_zone0/temp").read()) / 1000
    except (OSError, ValueError):
        return "温度", "info", "読めませんでした"
    st = "ng" if t >= 80 else "warn" if t >= 70 else "ok"
    return "温度", st, "%.1f℃%s" % (t, "(80℃で速さが落ちます)" if st != "ok" else "")
def dmesg():
    rc, out = run(["dmesg"], timeout=10)
    return out if rc == 0 else ""
def check_sd(kmsg, kmsg_ok=True):
    opts = ""
    for line in open("/proc/mounts"):
        p = line.split()
        if len(p) > 3 and p[1] == "/":
            opts = p[3]
    du = shutil.disk_usage("/")
    free_gb = du.free / 1024 ** 3
    errs = len(re.findall(r"(?i)(mmc\S*:.*(error|timeout)|I/O error.*mmcblk|EXT4-fs error)", kmsg))
    if "ro" in opts.split(","):
        return "SD カード", "ng", "読むだけに切り替わっています(カードの傷みの合図・新しいカードの用意を)"
    if errs:
        return "SD カード", "warn", "起動してから読み書きのエラーが %d 件・空き %.1f GB" % (errs, free_gb)
    if free_gb < 1:
        return "SD カード", "ng", "空きが %.1f GB しかありません" % free_gb
    if free_gb < 2:
        return "SD カード", "warn", "空きが %.1f GB です" % free_gb
    if not kmsg_ok:
        return "SD カード", "ok", "読み書きできる・空き %.1f GB(起動のログが読めないので、エラーの数は見ていない)" % free_gb
    return "SD カード", "ok", "読み書きできる・エラーなし・空き %.1f GB" % free_gb
def check_memory(kmsg, kmsg_ok=True):
    mem = {l.split(":")[0]: int(l.split()[1]) for l in open("/proc/meminfo")}
    avail = mem.get("MemAvailable", 0) // 1024
    ooms = len(re.findall(r"(?i)(out of memory|oom-kill)", kmsg))
    if ooms:
        return "メモリー", "warn", "メモリー不足で止められたプログラムが %d 回(起動してから)・空き %d MB" % (ooms, avail)
    if avail < 100:
        return "メモリー", "warn", "空きが %d MB です" % avail
    if not kmsg_ok:
        return "メモリー", "ok", "空き %d MB(起動のログが読めないので、止められたものの数は見ていない)" % avail
    return "メモリー", "ok", "空き %d MB・不足で止められたものなし" % avail
def check_nas():
    # /mnt/nas は使うときに自動でつなぐ設定(x-systemd.automount)。読みに行くことが、つなぎ直しの試みになる
    rc, _ = run(["ls", "/mnt/nas"], timeout=20)
    if rc == 0:
        return "NAS", "ok", "/mnt/nas が読める"
    time.sleep(10)
    rc, _ = run(["ls", "/mnt/nas"], timeout=20)
    if rc == 0:
        log("NAS:1 回目は読めず、2 回目で読めた(つなぎ直し)")
        return "NAS", "warn", "1 回目は読めず、つなぎ直して読めた"
    return "NAS", "ng", "/mnt/nas が読めません(NAS の電源・LAN を見てください)"
def check_time():
    rc, out = run(["timedatectl", "show", "-p", "NTPSynchronized", "--value"])
    if out == "yes":
        return "時刻", "ok", "ネットの時刻と合っている"
    return "時刻", "warn", "ネットの時刻と合わせられていません(ログやバックアップの日時がずれることがあります)"
def keepalive_targets():
    out = []
    for f in sorted(glob.glob(os.path.join(BASE, "apps", "*.json"))):
        try:
            a = json.load(open(f, encoding="utf-8"))
        except Exception:
            continue
        if a.get("keepalive"):
            out.append((a.get("name", os.path.basename(f)), a["keepalive"]))
    return out
def alive(k):
    if k.get("url"):
        try:
            import urllib.error
            import urllib.request
            with urllib.request.urlopen(k["url"], timeout=5) as r:
                return r.status < 500
        except urllib.error.HTTPError as e:
            return e.code < 500  # 404・403 などは、画面は動いている(止まってはいない)
        except Exception:
            return False
    if k.get("process"):
        rc, _ = run(["pgrep", "-f", k["process"]])
        return rc == 0
    return True
def check_keepalive():
    rows = []
    for name, k in keepalive_targets():
        if alive(k):
            rows.append(("常に動かすもの:" + name, "ok", "動いている"))
            continue
        if not k.get("start"):
            rows.append(("常に動かすもの:" + name, "ng", "止まっています(起こし方が設定ファイルにありません)"))
            continue
        cwd = os.path.expanduser(k.get("cwd", "~"))
        logf = open(os.path.expanduser(k.get("log", os.path.join(DATA, "keepalive.log"))), "a")
        subprocess.Popen([os.path.expanduser(x) for x in k["start"]], cwd=cwd, stdout=logf, stderr=logf, stdin=subprocess.DEVNULL, start_new_session=True)
        time.sleep(k.get("wait", 8))
        if alive(k):
            log("起こし直した:%s" % name)
            rows.append(("常に動かすもの:" + name, "warn", "止まっていたので起こし直した(%s)" % datetime.now().strftime("%m/%d %H:%M")))
        else:
            log("起こせなかった:%s" % name)
            rows.append(("常に動かすもの:" + name, "ng", "止まっていて、起こし直しても動きません"))
    return rows
def check_logs():
    big = []
    for pat in LOGS_TO_WATCH:
        for f in glob.glob(os.path.expanduser(pat)):
            try:
                s = os.path.getsize(f)
            except OSError:
                continue
            if s > 10 * 1024 * 1024:
                big.append("%s(%.0f MB)" % (f.replace(os.path.expanduser("~"), "~"), s / 1024 / 1024))
    if big:
        return "ログの大きさ", "warn", "10 MB を超えた:" + "・".join(big)
    return "ログの大きさ", "ok", "どれも 10 MB 以下"
def check_updates():
    files = glob.glob("/var/lib/apt/lists/*Release")
    if not files:
        return "OS の更新", "info", "更新の情報が見つかりません"
    age = (time.time() - max(os.path.getmtime(f) for f in files)) / 86400
    rc, out = run(["apt", "list", "--upgradable"], timeout=30)
    n = len([l for l in out.splitlines() if "/" in l]) if rc == 0 else -1
    text = "更新できるもの %d 件(更新の情報は %d 日前)" % (n, age) if n >= 0 else "数えられませんでした"
    if age > 30:
        return "OS の更新", "info", text + "。sudo apt update で情報を新しくしてください"
    return "OS の更新", "info" if n > 0 else "ok", text
def check_watchdog():
    rc, out = run(["systemctl", "show", "-p", "RuntimeWatchdogUSec", "--value"])
    if out in ("0", "infinity", "") or out.startswith("0"):
        return "ウォッチドッグ", "info", "オフ(固まったときに自動で再起動しない・オンにするのは sudo の設定)"
    span = out.replace("min", " 分").replace("s", " 秒") if re.fullmatch(r"\d+(min|s)", out) else out
    return "ウォッチドッグ", "ok", "オン(%s固まったら再起動)" % span
def check_boot(is_boot):
    up = float(open("/proc/uptime").read().split()[0])
    booted = time.time() - up
    text = "起動 %s(%d 日 %d 時間前)" % (fmt(booted), up // 86400, up % 86400 // 3600)
    if is_boot:
        prev = None
        try:
            prev = float(open(HEARTBEAT).read().strip())
        except (OSError, ValueError):
            pass
        reason = "わからない(停電か、手動の再起動)"
        # 家の Wi-Fi のつなぎ直しの仕組み(wifi_heal.py)があるときだけ、その再起動かを見分ける。なければ「わからない」のまま
        try:
            last_heal = json.load(open(os.path.expanduser("~/notify/wifi_heal.json"))).get("last_reboot", 0)
            if 0 < booted - last_heal < 600:
                reason = "Wi-Fi のつなぎ直しの再起動"
        except (OSError, ValueError):
            pass
        line = "%s\t起動\t最後に確認した %s\t理由:%s" % (fmt(booted), fmt(prev) if prev else "-", reason)
        with open(BOOTLOG, "a", encoding="utf-8") as f:
            f.write(line + "\n")
        log("起動の記録:" + line)
    try:
        last = open(BOOTLOG, encoding="utf-8").read().strip().splitlines()[-1].split("\t")
        text += "・前回の止まり:最後に確認したのが %s・%s" % (last[2].replace("最後に確認した ", ""), last[3])
    except (OSError, IndexError):
        pass
    return "起動の記録", "ok", text
def main():
    is_boot = "--boot" in sys.argv
    os.makedirs(DATA, exist_ok=True)
    kmsg = dmesg()
    kmsg_ok = kmsg != ""
    items = []
    for fn in (check_power, check_temp, lambda: check_sd(kmsg, kmsg_ok), lambda: check_memory(kmsg, kmsg_ok), check_nas, check_time):
        items.append(fn())
    items += check_keepalive()
    for fn in (check_logs, check_updates, check_watchdog, lambda: check_boot(is_boot)):
        items.append(fn())
    prev = {}
    try:
        prev = {i["name"]: i["state"] for i in json.load(open(OUT, encoding="utf-8"))["items"]}
    except (OSError, ValueError, KeyError):
        pass
    for name, st, text in items:
        if st == "ng" and prev.get(name) != "ng":
            log("赤になった:%s:%s" % (name, text))
    worst = max((st for _, st, _ in items), key=lambda s: ORDER[s])
    tmp = OUT + ".part"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump({"checked_at": now(), "boot": is_boot, "state": worst,
                   "items": [{"name": n, "state": s, "text": t} for n, s, t in items]}, f, ensure_ascii=False, indent=1)
    os.replace(tmp, OUT)
    with open(HEARTBEAT, "w") as f:
        f.write(str(time.time()))
    if is_boot:
        log("起動のあとの点検:%s(%d 項目)" % (worst, len(items)))
if __name__ == "__main__":
    main()

ここだけ自分の環境に変える(selfcheck.py)

  • LOGS_TO_WATCH:大きさを見るログのファイル(自分のバックアップや通知のログ)
  • NAS を使っていなければ、main() の中の check_nas を消す
  • 温度の線(70℃・80℃)や、SD の空きの線(2GB・1GB)は、check_temp・check_sd の中の数字で変えられる

手順 3:手動で 1 回動かす

chmod 700 ~/raspi-home/selfcheck.py
python3 ~/raspi-home/selfcheck.py
python3 -c "import json;d=json.load(open('/home/pi/raspi-home/data/selfcheck.json'));[print(i['state'],i['name'],i['text']) for i in d['items']]"

次のように、項目ごとに 1 行ずつ出れば成功です(家のラズパイの結果)。

ok 電源と熱 今は正常(起動してから:速さを落としたことがある)
ok 温度 54.8℃
ok SD カード 読み書きできる・エラーなし・空き 23.4 GB
ok メモリー 空き 682 MB・不足で止められたものなし
ok NAS /mnt/nas が読める
ok 時刻 ネットの時刻と合っている
ok 常に動かすもの:ネットのつながりの記録 動いている
ok 常に動かすもの:スクレイピング 動いている
ok ログの大きさ どれも 10 MB 以下
info OS の更新 更新できるもの 0 件(更新の情報は 35 日前)。sudo apt update で情報を新しくしてください
ok ウォッチドッグ オン(1 分固まったら再起動)
ok 起動の記録 起動 10/21 18:05(20 日 3 時間前)

自分で直したことや、赤になったことは、~/raspi-home/data/selfcheck.log に 1 行ずつ残ります。

2026-11-10 21:08:13	起こし直した:スクレイピング

手順 4:1 時間ごとと、起動のあとに動くようにする

crontab -e を開いて、最後に次の 2 行を足します。1 行目は毎時 5 分の点検、2 行目は起動の 3 分後の点検です。2 行目の --boot を付けたときだけ、前回いつ止まったかを記録します。

5 * * * * /usr/bin/flock -n /tmp/raspi-selfcheck.lock /usr/bin/python3 $HOME/raspi-home/selfcheck.py >/dev/null 2>&1
@reboot sleep 180 && /usr/bin/flock -n /tmp/raspi-selfcheck.lock /usr/bin/python3 $HOME/raspi-home/selfcheck.py --boot >/dev/null 2>&1
  • flock -n:前の点検がまだ動いていたら、次の点検は何もしません(同じ時間に 2 つ動かないように)。
  • 起動の 3 分後にしているのは、ネットや NAS がつながるのを待つためです。

停電や再起動のあとに、必要なものを動かす

ラズパイの中のプログラムは、動き出し方がいくつかあります。家のラズパイでは、次の 3 つが混ざっていました。

動き出し方 途中で止まったとき
cron で 1 分ごとに起動を試す(flock つき) 1 分以内に自分で戻る
cron の @reboot(起動のときに 1 回だけ) 止まったまま
/etc/fstab(NAS など) 起動のときにつながらないと、外れたまま

2 つ目と 3 つ目は、起動のあとや途中で止まると、そのままになります。セルフチェックの設定ファイルに書いておけば、起動の 3 分後と 1 時間ごとの点検で、止まっていれば起こし直し、NAS は読みに行ってつなぎ直させます。停電から戻ったあとも、自動で全部動いている状態に戻ります。

つまずきやすい所

  • 電源と熱が「読めませんでした」になる:vcgencmd がない OS です。Raspberry Pi OS なら入っています。
  • SD カードのエラーが数えられない:dmesg が読めないと 0 件になります。dmesg | tail がエラーにならないかを見ます。
  • 起こし直しても「動きません」になる:設定ファイルの start のコマンドを、そのままターミナルで実行してみて、エラーを見ます。cwd(どのフォルダで動かすか)の書き忘れが多いです。
  • OS の更新の情報が古い:sudo apt update で新しくします。この仕組みは sudo を使わないので、更新はしません。

分からないときは、このページを AI に渡してください

このページの URL を AI(Claude など)に渡して「この手順を手伝って」と頼めば、一緒に進めてくれます。第 4 回の形にしてあれば、AI が毎朝 selfcheck.json を読んで、赤だけ教えてもらうこともできます。頼み方の例です。

  • 「(このページの URL)の設定ファイルを、~/notify/mail_notify.py のプログラムの分も作って」
  • 「(このページの URL)の selfcheck.json を読んで、赤や黄の項目があれば理由と直し方を教えて」

まとめ

この記事でできるようになったこと

  • ラズパイが 1 時間に 1 回、自分の状態を 11 の項目で確認する
  • 止まった画面やプログラムは、設定ファイルに書いておけば自動で起こし直す
  • 停電や再起動のあとも、自動で全部動いている状態に戻り、いつ止まったかを記録する

人に知らせるより、自分で戻るほうが、24 時間動かすラズパイには合っていました。自分では直せないもの(SD カードの傷みや電源)だけは赤で残るので、壊れる前に新しい SD カードや電源を用意しておけます。

次に読む

この記事を書いた人

フリースタイル

Web の仕事 25 年以上。WordPress の保守・引っ越しと、PHP の業務の仕組みづくり。 プロフィール →

まずは、お気軽にご相談ください。

相談と見積りは無料です。急ぎのときは、フォームで「急ぎ」を選んでください。