本ページはプロモーションが含まれています。
「1日数回動かすスクレイピングや集計を何本も抱えているが、VPSに毎月払うほどではない」。そんなときは、自宅サーバーで systemd timer を使う選択肢があります。私は故障したノートPC(省電力の古いノートPC・Linux)1台で、systemd timer を十数本を電気代だけで回しています。この記事では、その構成とsystemd timerの設定、停電で落ちたときの備え、そして「ここを超えたらVPSへ移す」という条件をまとめます。
省電力の古い故障ノートPCでsystemd timer を十数本を回している構成
使っているマシンと前提条件
使っているのは、バッテリーが壊れて取り外したノートPCです。ACアダプタだけで動いているので、停電やコードが抜けたら即座に電源が落ちます。この弱点を前提に組んでいます。
項目 | 内容 |
|---|---|
CPU | 低消費電力の2コアCPU |
メモリ | 4GB |
OS | Ubuntu系のLinux |
定期実行 | systemd timer を十数本(ユーザー単位の user timer) |
電源 | ACのみ(バッテリー取り外し済み) |
月額 | 電気代のみ(サーバー代0円) |
十数本が4GBに収まっている理由
十数本といっても、どれも1日に1〜数回だけ起動し、数分から十数分で終わる処理です。常駐プロセスはありません。このスペックで問題になるのは同時に走ったときだけです。たとえばheadless Chrome(画面を出さないブラウザ)を使うSeleniumの処理と、別の重い処理が重なると、メモリ4GBではすぐにスワップ(メモリ不足時にディスクへ逃がす動作)が始まり、全体が止まったように遅くなります。
そこで、実行時刻を1時間ずつずらしたうえで、後述する flock を使い同時に走れるのは常に1本に制限しています。重なった処理は順番待ちになるだけで、飛ばされはしません。
電気代の見積もり方
消費電力はマシンごとの差が大きいので、他人の数字を当てにせず、ワットチェッカー(コンセントに挿して消費電力を測る機器)で自分のマシンを測るのが確実です。
月の電力量(kWh) = 平均消費電力(W) × 24 × 30 ÷ 1000
月の電気代(円) = 月の電力量(kWh) × 契約している電力単価(円/kWh)Raspberry Pi 4 のようなシングルボードコンピューターなら、常時稼働でも月数十円から数百円程度に収まるとされています。逆に、古いデスクトップPCを流用すると電気代がかさみ、VPSの月額を上回ることもあります。ノートPCは省電力向けの設計なので、流用元としては扱いやすい部類です。
以前はcrontab、今はsystemd timerのPersistent=trueで「止まっていた回」を拾う
cronだと止まっていた時間の分が消える
以前は定期実行といえば crontab -e で、こう書いていました。
0 9 * * * cd /home/username/jobs && .venv/bin/python scrape.pycronは「その時刻に起きていれば実行する」仕組みです。9:00に停電で落ちていた日や、スリープしていた日は、その回が何の記録も残さずに飛びます。電源が不安定な自宅PCとは相性がよくありません。
今はserviceとtimerの2ファイルで書く
systemd timerでは「何を実行するか(.service)」と「いつ実行するか(.timer)」を別ファイルに分けて書きます。ユーザー単位で動かす場合は ~/.config/systemd/user/ に置きます。
~/.config/systemd/user/myjob-scrape.service
[Unit]
Description=daily scrape job
[Service]
Type=oneshot
WorkingDirectory=/home/username/jobs
ExecStart=/usr/bin/flock -w 3600 /home/username/jobs/.agent.lock /home/username/jobs/.venv/bin/python scrape.py~/.config/systemd/user/myjob-scrape.timer
[Unit]
Description=run scrape every morning
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target各ブロックの役割は次のとおりです。
Type=oneshot:1回実行して終わる処理であることを示します。常駐しないスクリプトはこれで足ります。WorkingDirectory:スクリプト内の相対パスがここを基準に解決されます。cronで書いていたcdの代わりです。ExecStart:実行コマンドです。先頭は絶対パスで書く必要があります。仮想環境(venv)のPythonを直接指定すると、source activateは不要になります。OnCalendar:実行時刻です。*-*-* 07,19:00:00のようにすれば1日2回になります。Persistent=true:この記事の要点です。電源が落ちていた間に予定時刻を過ぎた場合、次に起動したときに1回実行してくれます。cronで消えていた回を取り戻せます。
有効化と、自宅PCならではの追加設定
systemctl --user daemon-reload
systemctl --user enable --now myjob-scrape.timer
systemctl --user list-timers # 次回・前回の実行時刻を確認
# ログアウトしても user timer が止まらないようにする
sudo loginctl enable-linger username
# ノートPCを常時稼働させるならスリープを無効化
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.targetノートPCの場合は、/etc/systemd/logind.conf に HandleLidSwitch=ignore も設定します。これを入れないと、フタを閉じた時点でサスペンドします。loginctl enable-linger を忘れると、ログアウトした瞬間に user timer が全部止まるので注意してください。sudoが必要なのは上記の linger・mask・logind.conf の3つだけで、timerの作成自体は一般ユーザーでできます。元に戻すときは sudo systemctl unmask ... と sudo loginctl disable-linger username を実行します。
Pythonのバージョンは python3 -V で確認し、仮想環境は python3 -m venv .venv で作ります。以下のスクレイピング例では pip install requests だけが必要です。
停電復帰時の一斉発火をflockで順番待ちにする
Persistent=trueには副作用がある
Persistent=true は便利ですが、半日停電していた場合、復帰した瞬間にその間に逃した全timerが一斉に起動します。十数本が同時に走ると、低消費電力・メモリの少ない構成ではまず処理しきれません。さらに、同じAPIを使う処理が重なると、相手側の毎分あたりの回数制限にも引っかかります。
flockで「同時に1本」を外から強制する
以前は、スクリプトごとに「実行中ファイルがあれば終了する」といった重複実行ガードを書いていました。しかしこの方法は、スクリプトが異常終了するとロックファイルが残って以後ずっと動かなくなる、という問題を抱えがちです。今は上のserviceファイルのように、全serviceの ExecStart を同じロックファイルの flock で包むだけにしています。
flock ロックファイル コマンド:ロックを取れた1本だけがコマンドを実行し、残りはロックが空くまで待ちます。-w 3600:最大3600秒まで待ちます。それを超えると実行せずに失敗で終わります。待ち行列の一番後ろでも1時間以内に回ってくる本数かどうかで値を決めます。- ロックはプロセスが終了するとOSが自動で解放します。処理が異常終了してもロックが残らないので、手作りの実行中ファイルより安全です。
十数本すべてに同じロックパスを書くのがポイントです。1本でも別のパスを書くと、その処理だけが並走します。別のプロジェクトのtimerを同じマシンで動かす場合も、ロックパスを揃えておきます。
落ちたあとに「自動起動が外れていないか」を外から見る
電源断のような異常終了のあと、ユニットファイルは残っているのに timers.target.wants/ 配下のリンクだけが消え、timerが disabled になっていたことがありました。この場合、エラーは何も出ず、処理が静かに止まります。そこで私は、全timerの状態を見る点検スクリプトも1日1回実行しています。標準ライブラリだけで動きます。
import subprocess
import time
PREFIX = "myjob-"
IGNORED = {"myjob-paused.timer"} # 意図的に止めているもの
MAX_GAP = 26 * 3600 # 1日1回の処理を想定。周期に合わせて変える
def sh(*args):
r = subprocess.run(["systemctl", "--user", *args],
capture_output=True, text=True)
return r.stdout.strip()
def list_timers():
out = sh("list-unit-files", f"{PREFIX}*.timer", "--no-legend")
return [line.split()[0] for line in out.splitlines() if line]
problems = []
now = time.time()
for t in list_timers():
if t in IGNORED:
continue
if sh("is-enabled", t) != "enabled":
problems.append(f"{t}: 自動起動が外れている")
continue
last = sh("show", t, "-p", "LastTriggerUSec", "--value", "--timestamp=unix")
if not last.startswith("@"):
continue # 再起動直後は空になるので異常扱いしない
gap = now - int(last[1:])
if gap > MAX_GAP:
problems.append(f"{t}: {gap / 3600:.0f}時間発火していない")
print("\n".join(problems) or "OK")list_timers():名前がmyjob-で始まるtimerを列挙します。命名を揃えておくと、点検対象を自動で拾えます。is-enabled:リンクが消えていればdisabledが返ります。復旧はsystemctl --user enable --now 名前.timerです。LastTriggerUSec:最後に発火した時刻です。--timestamp=unixを付けると@1234567890の形で返るので、そのまま計算に使えます。再起動直後は空になるため、未発火を即座に異常とは判定しません。
結果は自分が普段見る通知先に送ってください。止まっても誰も教えてくれないのが自宅サーバーの一番の弱点で、それを補うのがこの点検です。
取得先サイトに迷惑をかけない書き方:robots.txtとアクセス間隔
復帰時に順番待ちが起きると、同じサイトへのアクセスが短時間に集中しがちです。flockで直列化していても、1本の中で連続アクセスしていれば相手の負担になります。毎日動かすスクリプトには、最低限次の配慮を入れています。
import time
import urllib.robotparser
import requests
BASE = "https://example.com"
UA = "my-collector/1.0"
rp = urllib.robotparser.RobotFileParser(BASE + "/robots.txt")
rp.read()
urls = [BASE + "/news/1", BASE + "/news/2"]
for url in urls:
if not rp.can_fetch(UA, url):
continue # 禁止されているパスは取りに行かない
r = requests.get(url, headers={"User-Agent": UA}, timeout=30)
r.raise_for_status()
# ここで r.text を解析する
time.sleep(5) # 1件ごとに間隔を空ける- robots.txt:
urllib.robotparserで事前に確認し、禁止されたパスには触れません。 - 利用規約:自動取得を禁じているサイトや、公式APIが用意されているサイトでは、そちらに従います。
- アクセス間隔:1件ごとに数秒空けます。早く終わらせるための並列化はしません。
timeout:付けないと、相手が応答しないときに処理が終わらず、flockの待ち行列全体が詰まります。自宅PCで直列運用するなら必須です。
アクセス制限やbot対策を回避する工夫はしません。弾かれたら、その取得方法自体を見直すべきだという合図です。
月額0円の自宅PCで足りる条件、VPSやレンタルサーバーへ移す条件
3つの置き場所の比較
料金は調査時点で公表されていた掲載値で、多くは長期契約時の最安表示です。改定やキャンペーンが頻繁にあるので、契約前に必ず各社の公式料金ページで確認してください。
置き場所 | 月額の目安 | cron | 常駐 | root権限 | 止まる条件 |
|---|---|---|---|---|---|
自宅PC(流用) | 電気代のみ | 可(systemd timerも可) | 可 | あり | 停電・回線断・故障・誤ってシャットダウン |
エックスサーバー スタンダード | 990円〜(12ヶ月契約)+初期費用3,300円 | 可・最短5分 | 不可 | なし | CPU負荷の高い処理は強制終了されうる |
ロリポップ! ハイスピード | 550円〜(36ヶ月契約) | 可・最短10分 | 不可 | なし | 同上 |
Xserver VPS 2GB | 830円〜(36ヶ月契約) | 可 | 可 | あり | メモリ不足・自分の設定ミス |
ConoHa VPS 2GB | 847円〜(36ヶ月契約・時間課金あり) | 可 | 可 | あり | 同上 |
さくらのVPS 1G | 880円 | 可 | 可 | あり | 同上(メモリ1GBなのでブラウザ自動操作は厳しい) |
VPSの多くは、長期契約にすると単月契約より大幅に安くなります。「月800円台」という表示は36ヶ月契約前提のことが多い点に注意してください。
自宅PCのままでよい条件
- 処理がすべて「時刻が来たら動いて終わる」型で、数時間遅れても困らない
- flockで直列化した状態で、1日の処理が待ち時間込みで回りきる
- 外からリクエストを受ける必要がない(Webhookの受け口や公開APIを置かない)
- 個人情報など、電源断でデータが壊れると困るものを扱わない
この4つを満たすなら、私の構成のように故障ノートPCでも十分回ります。Persistent=true で取りこぼしを防ぎ、flockで重なりを防ぎ、点検スクリプトで停止に気づく。この3点が揃えば、自宅PCの弱点はほぼ埋まります。
VPSへ移すべき条件
- 止まると実害が出る:時刻どおりに実行しないと意味がない処理(締め切りのある取得、決まった時刻の通知など)。停電時に動かないことが許されない
- 常駐が要る:ボットやWebhookの受信など、24時間応答し続ける処理。自宅の回線はポートを開けられないことも多く、外から届かない場合があります
- 同時実行が避けられない:直列化すると1日のうちに回りきらない本数になった場合。メモリを増やせるVPSのほうが素直です
- データを守る必要がある:データベースを置く、個人情報を扱う場合。UPS(停電時に電源を保つ装置)の無い自宅PCには置かないほうが安全です
Seleniumを毎日動かす処理を置き場所別に比べた話はSelenium毎日実行 VPS・自宅・共有比較|26年10月に、常駐型の処理についてはDiscordボット常駐はどこで?VPSと自宅PC比較|26年10月にまとめています。
レンタルサーバーのcronが向く処理
エックスサーバーやロリポップ!のcronは、軽い取得処理を5分または10分以上の間隔で回すだけなら十分使えます。ただし常駐はできず、root権限もありません。共有サーバーなのでCPUを長く使う処理は止められる可能性があります。すでにWebサイト用に契約しているなら、軽い処理だけをそちらに逃がす使い方は有効です。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
結論:1日数回の処理を十数本なら自宅PC+systemd timerで足りる
1日に数回だけ動いて終わる処理を十数本回す程度なら、低消費電力・メモリの少ない構成の故障ノートPCでも、月額0円(電気代のみ)で回せます。条件は3つです。Persistent=true で停電中に逃した回を復帰後に拾うこと、全serviceを同じロックの flock で包んで一斉発火を順番待ちにすること、そしてtimerが外れていないかを外から点検することです。
一方で、時刻どおりに動かないと困る処理、24時間の常駐、外からの受信、個人情報を含むデータベースのどれかが出てきたら、そのときがVPSへ移す目安です。月800円台からの長期契約プランが選択肢になります。軽いcronで済む処理だけなら、契約済みのレンタルサーバーで足ります。まず自宅PCで動かしてみて、上の条件のどれかを超えたものから順に外へ出すのが、無駄なく始められる順番です。