32blogby Studio Mitsu

VPSでFFmpegエンコードサーバーを構築する

VPSにFFmpegをインストールしてエンコードサーバーを構築する方法。ウォッチフォルダ、systemdサービス、簡易APIまで実装する。

by omitsu21 min read

当記事にはアフィリエイト広告が含まれています

目次

VPSでFFmpegエンコードサーバーを構築するには、Ubuntu 24.04にFFmpegと inotify-tools をインストールし、inotifywait -e close_write でウォッチフォルダを監視、systemdサービスで24時間常駐化する。リモートからのジョブ投入にはFastAPIで簡易REST APIを追加する。

ローカルPCで動画をエンコードしていると、CPUが占有されてほかの作業ができなくなる。エンコードが終わるまでの数十分、PCがまともに動かなくなる経験は誰でもあるはずだ。

この記事では、さくらのVPSなどのVPSにFFmpegエンコードサーバーを構築して、ローカルPCの負荷をゼロにする方法を僕が解説する。ウォッチフォルダで自動エンコード、systemdで常駐化、簡易REST APIでリモートからジョブ投入まで、一通り実装する。

この記事でわかること

  • VPSでFFmpegエンコードサーバーを動かす理由とメリット
  • スペック選定の考え方
  • FFmpegのインストールから初期設定まで
  • inotifywaitを使ったウォッチフォルダの実装
  • systemdサービスで24時間常駐化する方法
  • FastAPIで簡易REST APIを構築する方法
  • 本番運用で必要なセキュリティと監視の設定

なぜVPSでエンコードするのか

ローカルエンコードの問題は「PCが使えなくなる」だけじゃない。夜中に走らせておいて翌朝完了しているはずが、スリープしてエンコードが止まっていた、という経験もあるはずだ。

VPSエンコードサーバーの利点は3つある。

ローカルPCが解放される。エンコードはVPS上で動くので、手元のPCはWebブラウジングや開発作業に使い続けられる。ファイルをアップロードしてしまえばそれで終わりだ。

24時間稼働が保証される。VPSはデータセンターで動いている。スリープも電源断もない。長時間エンコードも確実に完走する。

スケールしやすい。処理が追いつかなくなったらVPSのスペックを上げるか、台数を増やせばいい。ローカルPCの買い替えよりずっと柔軟だ。

コスト的には月500〜2,000円程度のVPSで、一般的な動画処理は十分こなせる。専用エンコードPCを買うより格段に安い。

自前のVPSエンコードとAWS MediaConvertのようなマネージドサービスのコスト比較については FFmpeg vs AWS MediaConvert コスト比較 で詳しく書いている。

VPSの選び方とスペック目安

エンコード用VPSはCPUコア数が最重要だ。FFmpegはマルチスレッドで動くので、コア数が多いほど速くなる。

スペックの目安

用途CPUメモリストレージ
テスト・個人用2コア2GB50GB SSD
中規模(日数本処理)4コア4GB100GB SSD
本格運用8コア以上8GB以上200GB+ SSD

GPUエンコード(NVENC、VAAPI)を使いたい場合はGPU搭載プランが必要だが、月額が跳ね上がる。CPU専用でも libx264libx265 で十分な品質は出せる。

国内おすすめVPS

  • ConoHa VPS — 512MB〜のプランがあり、用途に合わせて選びやすい
  • Xserver VPS — 国内最安クラス。2コア/2GBで月数百円台から
  • さくらのVPS — 老舗。安定性が高く長期利用向き

OSはUbuntu 24.04 LTSを選ぶ。標準サポートが2029年4月まであり、apt install ffmpegFFmpeg 6.1.xがそのまま入る。

FFmpegのインストールと初期設定

VPSにSSHでログインしたら、まず必要なパッケージを入れる。

bash
# システムを最新化
sudo apt update && sudo apt upgrade -y

# FFmpeg と関連ツールをインストール
sudo apt install -y ffmpeg inotify-tools python3-pip python3-venv

# バージョン確認
ffmpeg -version

Ubuntu 24.04なら ffmpeg version 6.1.x と表示される。22.04の場合は 4.4.x だが、この記事の手順はどちらでも動く。インストール方法の詳細(ソースビルド含む)は FFmpegインストール完全ガイド を参照。

次に作業用のディレクトリ構成を作る。

bash
# エンコードサーバー用のディレクトリ構造を作成
sudo mkdir -p /opt/encoder/{watch,processing,done,failed,logs}

# 実行ユーザーを作成(rootで動かさない)
sudo useradd -r -s /bin/false encoder

# ディレクトリのオーナーを変更
sudo chown -R encoder:encoder /opt/encoder

ディレクトリの役割は次のとおりだ。

  • watch/ — ここにファイルを置くとエンコードが始まる
  • processing/ — エンコード中のファイルが一時的に移動される
  • done/ — エンコード完了後のファイルが入る
  • failed/ — 失敗したファイルが入る
  • logs/ — ログファイルの置き場
SCP / APIUploadwatch/Queueinotifyprocessing/FFmpeg encodeencodedone/Output

FFmpegのエンコードオプションは用途によって変わる。汎用的な設定をシェルスクリプトにまとめておくと管理しやすい。

bash
# /opt/encoder/encode.sh を作成
sudo tee /opt/encoder/encode.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail

INPUT="$1"
BASENAME=$(basename "$INPUT" | sed 's/\.[^.]*$//')
OUTPUT="/opt/encoder/done/${BASENAME}_encoded.mp4"

ffmpeg -i "$INPUT" \
  -c:v libx264 \
  -preset slow \
  -crf 23 \
  -c:a aac \
  -b:a 128k \
  -movflags +faststart \
  "$OUTPUT" \
  2>> /opt/encoder/logs/ffmpeg.log

echo "Done: $OUTPUT"
EOF

sudo chmod +x /opt/encoder/encode.sh
sudo chown encoder:encoder /opt/encoder/encode.sh

-crf 23 は品質と圧縮率のバランスが取れた値だ。数値を下げると高品質・大容量、上げると低品質・小容量になる。18〜28が実用的な範囲だ。CRFの仕組みや用途別の推奨値については FFmpeg動画圧縮の完全ガイド で詳しく解説している。

複数ファイルを一括処理したい場合は PythonでFFmpegバッチ処理を自動化する方法 も参考になる。

ウォッチフォルダで自動エンコード

inotifywait コマンドでウォッチフォルダを実装する。ファイルが watch/ に配置されたタイミングで自動的にエンコードを開始する。

bash
# /opt/encoder/watch.sh を作成
sudo tee /opt/encoder/watch.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail

WATCH_DIR="/opt/encoder/watch"
PROCESSING_DIR="/opt/encoder/processing"
FAILED_DIR="/opt/encoder/failed"
LOG="/opt/encoder/logs/watch.log"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}

log "Encoder watch started. Watching: $WATCH_DIR"

inotifywait -m -e close_write --format '%f' "$WATCH_DIR" | while read -r FILENAME; do
  FILEPATH="${WATCH_DIR}/${FILENAME}"

  # 対応拡張子のみ処理(mp4, mkv, mov, avi)
  EXT="${FILENAME##*.}"
  EXT_LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')
  if [[ ! "$EXT_LOWER" =~ ^(mp4|mkv|mov|avi)$ ]]; then
    log "Skipped (unsupported format): $FILENAME"
    continue
  fi

  log "Detected: $FILENAME"

  # processing ディレクトリへ移動
  PROC_PATH="${PROCESSING_DIR}/${FILENAME}"
  mv "$FILEPATH" "$PROC_PATH"
  log "Moved to processing: $FILENAME"

  # エンコード実行
  if /opt/encoder/encode.sh "$PROC_PATH"; then
    rm -f "$PROC_PATH"
    log "Success: $FILENAME"
  else
    mv "$PROC_PATH" "${FAILED_DIR}/${FILENAME}"
    log "Failed: $FILENAME — moved to failed/"
  fi
done
EOF

sudo chmod +x /opt/encoder/watch.sh
sudo chown encoder:encoder /opt/encoder/watch.sh

close_write イベントを使う理由は、create イベントだとファイルが書き込み途中でも発火してしまうからだ。close_write は書き込みが完了してファイルが閉じられたときに発火する。SCPやrsyncでファイルを転送する場合も、転送完了後に正しく動く。

動作確認はこう行う。

bash
# watch.sh をテスト実行
sudo -u encoder /opt/encoder/watch.sh &

# 別ターミナルでテストファイルを配置
cp /path/to/test.mp4 /opt/encoder/watch/

# ログを確認
tail -f /opt/encoder/logs/watch.log

systemdサービスで常駐化する

ウォッチスクリプトを手動で起動するのは運用として成立しない。systemdサービスとして登録して、OS起動と同時に自動スタートさせる。

ini
# /etc/systemd/system/encoder.service を作成
[Unit]
Description=FFmpeg Encoding Watch Service
After=network.target

[Service]
Type=simple
User=encoder
Group=encoder
ExecStart=/opt/encoder/watch.sh
Restart=on-failure
RestartSec=5s
StandardOutput=append:/opt/encoder/logs/systemd.log
StandardError=append:/opt/encoder/logs/systemd.log

# セキュリティ強化
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/encoder
PrivateTmp=true

[Install]
WantedBy=multi-user.target

サービスを有効化する。

bash
# サービスファイルをsystemdに読み込ませる
sudo systemctl daemon-reload

# 起動時の自動スタートを有効化
sudo systemctl enable encoder.service

# 今すぐ起動
sudo systemctl start encoder.service

# 状態確認
sudo systemctl status encoder.service

Active: active (running) と表示されれば成功だ。ログは journalctl でも確認できる。

bash
# リアルタイムログ確認
sudo journalctl -u encoder.service -f

# 過去ログの確認
sudo journalctl -u encoder.service --since "1 hour ago"

簡易REST APIでリモート投入

SCPでファイルをウォッチフォルダに転送するのは確実だが、もっと手軽にエンコードを投入したい場合はREST APIが便利だ。FastAPIで簡単なAPIサーバーを作る。

まずFastAPIをインストールする。

bash
# Python仮想環境の作成
python3 -m venv /opt/encoder/venv

# FastAPIとUvicornをインストール
/opt/encoder/venv/bin/pip install "fastapi[standard]" python-multipart

APIサーバーのコードを書く。

python
# /opt/encoder/api.py
import os
import shutil
from pathlib import Path

from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from fastapi.security import APIKeyHeader
from fastapi import Security, Depends

app = FastAPI(title="FFmpeg Encoding API")

# 環境変数からAPIキーを取得
API_KEY = os.environ.get("ENCODER_API_KEY", "")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

WATCH_DIR = Path("/opt/encoder/watch")
DONE_DIR = Path("/opt/encoder/done")
FAILED_DIR = Path("/opt/encoder/failed")
PROCESSING_DIR = Path("/opt/encoder/processing")


def verify_api_key(key: str = Security(api_key_header)) -> str:
    if not API_KEY:
        raise HTTPException(status_code=500, detail="API key not configured")
    if key != API_KEY:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return key


@app.post("/encode")
async def submit_encode(
    file: UploadFile = File(...),
    _: str = Depends(verify_api_key),
) -> JSONResponse:
    """動画ファイルをウォッチフォルダに投入する"""
    allowed_exts = {".mp4", ".mkv", ".mov", ".avi"}
    ext = Path(file.filename).suffix.lower()

    if ext not in allowed_exts:
        raise HTTPException(
            status_code=400,
            detail=f"Unsupported file type: {ext}. Allowed: {allowed_exts}",
        )

    dest = WATCH_DIR / file.filename
    with dest.open("wb") as f:
        shutil.copyfileobj(file.file, f)

    return JSONResponse(
        status_code=202,
        content={"status": "queued", "filename": file.filename},
    )


@app.get("/status")
async def get_status(_: str = Depends(verify_api_key)) -> JSONResponse:
    """各フォルダのファイル数を返す"""
    return JSONResponse(
        content={
            "watching": len(list(WATCH_DIR.iterdir())),
            "processing": len(list(PROCESSING_DIR.iterdir())),
            "done": len(list(DONE_DIR.iterdir())),
            "failed": len(list(FAILED_DIR.iterdir())),
        }
    )


@app.get("/health")
async def health_check() -> JSONResponse:
    """ヘルスチェック(認証不要)"""
    return JSONResponse(content={"status": "ok"})

APIサーバーもsystemdサービスとして登録する。

bash
# APIサービスのsystemdユニットファイル
sudo tee /etc/systemd/system/encoder-api.service > /dev/null << 'EOF'
[Unit]
Description=FFmpeg Encoding API Server
After=network.target

[Service]
Type=simple
User=encoder
Group=encoder
WorkingDirectory=/opt/encoder
Environment="ENCODER_API_KEY=your-secret-key-here"
ExecStart=/opt/encoder/venv/bin/uvicorn api:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable encoder-api.service
sudo systemctl start encoder-api.service

APIキーは ENCODER_API_KEY 環境変数で設定する。ランダムな文字列を使う。

bash
# ランダムなAPIキーを生成
openssl rand -hex 32

APIの使い方はこうだ。

bash
# ファイルを投入
curl -X POST http://your-vps-ip:8000/encode \
  -H "X-API-Key: your-secret-key-here" \
  -F "file=@/path/to/video.mp4"

# ステータス確認
curl http://your-vps-ip:8000/status \
  -H "X-API-Key: your-secret-key-here"

外部からアクセスする場合は、Nginxをリバースプロキシとして立ててHTTPS化することを強く推奨する。

本番運用の注意点

ディスク容量の管理

動画ファイルはディスクを圧迫しやすい。done/ フォルダのファイルは定期的に削除するか、転送後に手動で消す運用を決めておく。cronで自動削除する場合は次のとおりだ。

bash
# 7日以上前のファイルを削除するcron(crontab -e で設定)
0 3 * * * find /opt/encoder/done -type f -mtime +7 -delete

リソース制限

エンコードが同時に複数走るとCPUが飽和する。ウォッチスクリプトにセマフォを入れて同時実行数を制限することを検討しよう。nice コマンドでエンコードプロセスの優先度を下げるのも有効だ。

bash
# encode.sh の ffmpeg 実行を nice で低優先度に変更
nice -n 10 ffmpeg -i "$INPUT" ...

ログローテーション

/opt/encoder/logs/ 以下のログは放置すると肥大化する。logrotateで管理する。

bash
# /etc/logrotate.d/encoder を作成
sudo tee /etc/logrotate.d/encoder > /dev/null << 'EOF'
/opt/encoder/logs/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
}
EOF

ファイル転送方法

ローカルPCからウォッチフォルダへファイルを転送する最もシンプルな方法はSCPだ。

bash
# ローカルからVPSのウォッチフォルダへ転送
scp /path/to/video.mp4 user@your-vps-ip:/opt/encoder/watch/

SSHの公開鍵認証を設定しておけば、パスワード入力なしで転送できる。頻繁に使うなら ~/.ssh/config にエイリアスを登録しておくと快適だ。

エンコード後の保存・共有

エンコードが終わったファイルの保存先も決めておこう。用途に応じて選び分ける。

クラウドストレージ

サービス無料容量特徴
Google Drive15 GBGoogleアカウントで共有が簡単
Dropbox2 GBチームでの共同作業に強い
OneDrive5 GBWindowsとの統合が良好
Backblaze B210 GB月$6/TBと非常に安い(大量バックアップ向き)

大量の動画を長期保存するならBackblaze B2が最もコスパが良い。少数ファイルの共有ならGoogle Driveで十分だ。

動画専用プラットフォーム

サービス特徴向いている用途
YouTube無料・無制限・広大なリーチ公開動画、長尺コンテンツ
Vimeo広告なし、高品質ストリーミングポートフォリオ、クライアント共有
Bunny.net CDN格安CDN、動画ストリーミング対応自前のサービスに組み込む場合

ローカルNAS

動画データを自前で管理したい場合はSynologyやQNAPのNASが選択肢になる。JellyfinやPlexを入れればFFmpegベースのオンデマンドトランスコードも可能だ。

エンコード結果をWeb配信するなら、次のステップとして HLSストリーミング配信 も検討してみてほしい。VPSでエンコード → CDNで配信、という流れが組める。

関連記事:

FAQ

FFmpegはCPUコアをいくつ使うのか?

libx264libx265 のソフトウェアエンコードでは、FFmpegは利用可能な全CPUスレッドを使う。4コアのVPSなら4スレッドがデフォルトだ。他プロセスに余力を残したい場合は -threads N で制限できる。ハードウェアエンコード(NVENC、QSV)はGPUが処理するのでコア数はあまり関係ない。詳しくは GPUエンコードガイド を参照。

HLS/DASHストリーミング出力に使える?

使える。encode.sh のFFmpegコマンドを -hls_time 6 -hls_list_size 0 output.m3u8 に変えれば、ウォッチフォルダに置くだけでHLSセグメントが自動生成される。マルチビットレートの適応型ストリーミングについては HLSストリーミング配信ガイド で解説している。

月500円のVPSでも動画エンコードは実用的?

個人利用で1080pの動画をたまにエンコードする程度なら、2コアのVPSで十分だ。10分の1080p動画を -preset slow -crf 23 でエンコードすると、だいたい15〜25分で完了する。4Kやバッチ処理なら4コア以上を検討しよう。VPSはスペックを即座に変更できるのが強みだ。

大容量ファイルを高速に転送するには?

SCPは手軽だが、数GBの動画ファイルだと転送に時間がかかる。rsync --partial --progress なら転送が途中で切れても再開できる。大量ファイルの場合は rclone でローカルフォルダとVPSのウォッチフォルダを同期するのも有効だ。転送前に ffmpeg -c copy で不要なストリームを除去して軽くするテクニックもある。

エンコード中にVPSが再起動したらどうなる?

systemdサービスはOS起動後に自動で再スタートする。ただし、エンコード中だったファイルは processing/ に残り、出力ファイルは不完全な状態で done/ に残る。watch.sh の起動時に processing/ 内のファイルを watch/ に戻す処理を追加しておくと、自動的に再エンコードされる。

FFmpegをソースからビルドすべき?

通常はaptパッケージで十分だ。ソースビルドが必要になるのは、デフォルトに含まれないコーデック(libfdk-aaclibsvtav1 など)を使いたい場合だけだ。aptインストールとソースビルドの両方を FFmpegインストール完全ガイド でカバーしている。

エンコード進捗をリモートから確認するには?

/status APIエンドポイントでキューの状態を確認できる。個別エンコードのリアルタイム進捗が必要なら、FFmpegコマンドに -progress pipe:1 を追加して出力をパースする方法がある。VPS上で watch -n 5 curl -s http://localhost:8000/status を叩くのも手軽だ。

この構成でSaaSレベルの動画処理は可能?

ウォッチフォルダ + systemdのアーキテクチャは、低〜中程度のボリュームなら安定して動く。複数ユーザーの同時処理、ジョブの優先度管理、リトライ処理が必要なSaaSレベルなら、ファイルシステムベースのトリガーではなくメッセージキュー(Redis + Celery、RabbitMQ等)に切り替えるべきだ。このガイドはエンコードレイヤーの構築で、オーケストレーションは別の話になる。

まとめ

VPSにFFmpegエンコードサーバーを構築する手順を解説した。

  • なぜVPSか: ローカルPCを解放、24時間稼働、柔軟なスケール
  • スペック目安: 個人用は2コア/2GB、本格運用は8コア以上
  • FFmpegインストール: apt install ffmpeg inotify-tools で一発
  • ウォッチフォルダ: inotifywait -e close_write で転送完了を正確に検知
  • systemd: encoder.service で起動時自動スタート、クラッシュ時自動再起動
  • REST API: FastAPI + APIキー認証でリモートからジョブ投入
  • 本番運用: ディスク管理、リソース制限、ログローテーション

最小構成から始めて、必要に応じてAPIを拡張していくのがおすすめだ。僕もこの構成からスタートして、徐々に拡張した。VPSのスペックを上げれば同じコードのまま処理速度が上がるのも、このアーキテクチャの利点だ。


この記事で紹介したFFmpegコマンドを覚えるのが大変なら、ffmpeg-quick を試してみてほしい。npx ffmpeg-quick compress input.mp4 のようにプリセット一発で実行できるOSSのCLIツールだ。

さくらのVPS

老舗の安定感。エンコード用途にも使える国内VPS

  • 2GBプラン 月額1,594円〜(石狩)
  • 東京・大阪・石狩の3リージョン
  • SSD 100GB・仮想3コア
XServer VPS

国内シェアNo.1のエックスサーバーが提供する高性能VPS

  • NVMe SSD・AMD EPYC搭載の高速サーバー
  • 2GBプラン 月額990円〜(3コア / 50GB SSD)
  • 初期費用無料