hanzochang
hanzochang
はじめに
対象読者
前提知識
サンプルリポジトリ
Railwayとは
課題:Railway.appでのcron制限
Railway.appの制限事項
実際のユースケース例
解決策:独自cronサービスの構築
アーキテクチャ概要
技術スタック
実装手順
プロジェクトセットアップ
package.jsonの設定
プロジェクト構造
環境変数の管理
APIクライアントの実装
APIクライアント
Workerクライアント
サービス層の実装
APIサービス
Workerサービス
Cronスケジューラーの実装
メインエントリーポイント
Railway.appでのデプロイ
Dockerfileの作成
.dockerignoreの設定
環境変数の設定
デプロイ手順
ログ出力の最適化
リトライ機能の実装
まとめ
注意点
https://s3.ap-northeast-1.amazonaws.com/hanzochang.com/_v2/article-ogp-019.png

Railway.appで複数のcronを実行する方法

Railway.appでは単一のcronしか実行できないという制限がありますが、独自のcronサービスを作成することで複数のcronジョブを実行する方法を解説します。Node.js + TypeScriptでのcronサービスの実装から、Railway.appでのデプロイまでを網羅的に説明します。

公開日2025.07.17

更新日2025.07.17

はじめに

Railway.comではcron機能も提供されています。 しかし、Railway.appのcronは一つのコマンドと実行スケジュールのペアしか登録できないため、複数の異なるcronジョブを実行したい場合には制限があります。

この記事では、Railway.appで複数のcronジョブを実行するための実用的な解決策として、独自のcronサービスを作成する方法を詳しく解説します。

対象読者

  • Railway.appを使用している開発者
  • 複数のcronジョブを定期実行したい方
  • Node.js/TypeScriptでのバックエンド開発経験がある方

前提知識

  • Node.js/TypeScriptの基本的な知識
  • APIの概念(RESTful API、HTTPリクエストなど)
  • Dockerの基本的な理解
  • Railway.appの基本的な使用経験

サンプルリポジトリ

サンプルリポジトリ

Railwayとは


https://railway.com/


Railway.comはvercelのように、簡単にサービスをホスティングが可能なツールです。 vercelと比較しさまざまなサービスを積むことができます。


vercelはaws lambdaのラッパーであることもあり、利用ライブラリによってはホストできない場合があります。例えばブラウザ利用するAIエージェントに欠かせないplaywrightは、vercelではデプロイできません。railwayなら動きます。


GCPやAWSやAZUREを使うとなると何かと設定に多少なりと時間はかかるので、 すぐに設定できるrailwayは何かと重宝します。

ですが、cronが複数できない、など細かな融通が効かないのも事実です。 (2025/07時点)

課題:Railway.appでのcron制限

Railway.appの制限事項

Railway.appはcron機能を提供していますが、以下の制限があります:

  • 単一cronのみ: 一つのコマンドと実行スケジュールのペアしか登録できない
  • 複数ジョブの管理: 異なる実行間隔や処理内容のcronジョブを同時に運用するのが困難
  • エラーハンドリング: 個別のcronジョブでのエラー処理が限定的

実際のユースケース例

例えば、以下のような複数のcronジョブを実行したい場合:

  • 30秒間隔: ヘルスチェック処理
  • 1分間隔: データ同期処理
  • 10分間隔: クリーンアップ処理

これらを Railway.app の標準cron機能だけで実現するのは困難です。

解決策:独自cronサービスの構築

アーキテクチャ概要

独自のcronサービスを作成することで、この問題を解決できます:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Cron Service  │───▶│   API Service   │    │  Worker Service │
│   (Railway)     │    │   Instance      │    │   Instance      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                    複数エンドポイントへの
                    定期的なリクエスト

技術スタック

  • Runtime: Node.js 18+
  • Language: TypeScript
  • Cron Library: node-cron
  • HTTP Client: axios
  • Container: Docker

実装手順

プロジェクトセットアップ

まず、プロジェクトの基本構造を作成します:

mkdir awesome-cron
cd awesome-cron
npm init -y

package.jsonの設定

{
  "name": "awesome-cron",
  "version": "1.0.0",
  "description": "Multi-cron service for Railway.app",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx watch --env-file=.env src/index.ts"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "dotenv": "^16.3.0",
    "node-cron": "^3.0.3"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/node-cron": "^3.0.11",
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

プロジェクト構造

推奨するフォルダ構成:

src/
├── consts/
│   └── application.const.ts
├── api-clients/
│   ├── api.api-client.ts
│   └── worker.api-client.ts
├── services/
│   ├── api.service.ts
│   └── worker.service.ts
├── cron/
│   └── scheduler.ts
└── index.ts

環境変数の管理

src/consts/application.const.ts:

const applicationConst = {
  api: {
    baseUrl: process.env.API_URL,
    bearerToken: process.env.API_BEARER_TOKEN,
  },
  worker: {
    baseUrl: process.env.WORKER_URL,
    bearerToken: process.env.WORKER_BEARER_TOKEN,
  },
}

export { applicationConst }

APIクライアントの実装

APIクライアント

src/api-clients/api.api-client.ts:

import axios, { AxiosInstance } from 'axios'
import { applicationConst } from '@/consts/application.const'

type ClientOptions = {
  headers?: Record<string, string>
}

export const apiClient = (options: ClientOptions = {}): AxiosInstance => {
  const baseUrl = applicationConst.api.baseUrl

  return axios.create({
    baseURL: baseUrl,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${applicationConst.api.bearerToken}`,
      ...options.headers,
    },
  })
}

Workerクライアント

src/api-clients/worker.api-client.ts:

import axios, { AxiosInstance } from 'axios'
import { applicationConst } from '@/consts/application.const'

type ClientOptions = {
  headers?: Record<string, string>
}

export const workerApiClient = (options: ClientOptions = {}): AxiosInstance => {
  const baseUrl = applicationConst.worker.baseUrl

  return axios.create({
    baseURL: baseUrl,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${applicationConst.worker.bearerToken}`,
      ...options.headers,
    },
  })
}

サービス層の実装

APIサービス

src/services/api.service.ts:

import { apiClient } from '@/api-clients/api.api-client'

const healthCheck = async () => {
  const client = apiClient()
  return await client.post('/api/cron/health-check', {})
}

const syncData = async () => {
  const client = apiClient()
  return await client.post('/api/cron/sync-data', {})
}

const cleanup = async () => {
  const client = apiClient()
  return await client.post('/api/cron/cleanup', {})
}

export const apiService = {
  healthCheck,
  syncData,
  cleanup,
}

Workerサービス

src/services/worker.service.ts:

import { workerApiClient } from '@/api-clients/worker.api-client'

const processTask = async () => {
  const client = workerApiClient()

  try {
    const response = await client.post('/api/process-task', {})
    return response.data
  } catch (error) {
    console.error('Error in processTask:', error)
    throw error
  }
}

export const workerService = {
  processTask,
}

Cronスケジューラーの実装

src/cron/scheduler.ts:

import * as cron from 'node-cron'
import { apiService } from '@/services/api.service'
import { workerService } from '@/services/worker.service'

export const setupCronJobs = () => {
  console.log('Setting up cron jobs...')

  // 30秒間隔: ヘルスチェック処理
  cron.schedule('*/30 * * * * *', async () => {
    console.log('Running 30-second health check...')

    try {
      await apiService.healthCheck()
      console.log('Health check completed successfully')
    } catch (error) {
      console.error('Error in health check:', error)
    }
  })

  // 1分間隔: データ同期処理
  cron.schedule('* * * * *', async () => {
    console.log('Running 1-minute data sync...')

    try {
      await Promise.all([apiService.syncData(), workerService.processTask()])
      console.log('Data sync completed successfully')
    } catch (error) {
      console.error('Error in data sync:', error)
    }
  })

  // 10分間隔: クリーンアップ処理
  cron.schedule('*/10 * * * *', async () => {
    console.log('Running 10-minute cleanup...')

    try {
      await apiService.cleanup()
      console.log('Cleanup completed successfully')
    } catch (error) {
      console.error('Error in cleanup:', error)
    }
  })

  console.log('Cron jobs have been set up successfully')
}

メインエントリーポイント

src/index.ts:

import 'dotenv/config'
import { setupCronJobs } from '@/cron/scheduler'

console.log('Starting awesome-cron service...')

setupCronJobs()

console.log('awesome-cron service is running. Press Ctrl+C to stop.')

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('Received SIGINT. Gracefully shutting down...')
  process.exit(0)
})

process.on('SIGTERM', () => {
  console.log('Received SIGTERM. Gracefully shutting down...')
  process.exit(0)
})

Railway.appでのデプロイ

Dockerfileの作成

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

.dockerignoreの設定

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.eslintcache
dist

環境変数の設定

Railway.appの管理画面で以下の環境変数を設定:

  • API_URL: APIサービスのベースURL
  • API_BEARER_TOKEN: API用のBearerトークン
  • WORKER_URL: WorkerサービスのベースURL
  • WORKER_BEARER_TOKEN: Worker用のBearerトークン

デプロイ手順

  1. Gitリポジトリの準備

    git init
    git add .
    git commit -m "Initial commit: Cron service implementation"
    git remote add origin <your-repository-url>
    git push -u origin main
  2. Railway.appでプロジェクト作成

    • Railway.appの管理画面でNew Projectを選択
    • GitHubリポジトリを連携
    • 環境変数を設定
  3. デプロイの確認

    • ログでcronジョブの実行を確認
    • 各APIエンドポイントが正常に呼び出されていることを確認

ログ出力の最適化

各cronジョブでの実行結果を適切にログ出力することで、問題の早期発見が可能です:

const logExecutionResult = (taskName: string, success: boolean, error?: any) => {
  const timestamp = new Date().toISOString()
  if (success) {
    console.log(`[${timestamp}] ✅ ${taskName} completed successfully`)
  } else {
    console.error(`[${timestamp}] ❌ ${taskName} failed:`, error?.message || error)
  }
}

リトライ機能の実装

ネットワークエラーなどの一時的な問題に対する耐性を向上させるため、リトライ機能を実装することをお勧めします:

const executeWithRetry = async (
  operation: () => Promise<any>,
  maxRetries: number = 3,
  delay: number = 1000,
) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation()
    } catch (error) {
      if (attempt === maxRetries) throw error
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`)
      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }
}

まとめ

Railway.appで複数のcronジョブを実行するために、独自のcronサービスを構築する方法を解説しました。

注意点

  • リソース使用量: 常時稼働するサービスなので、Railway.appの使用量に注意
  • エラー処理: 各APIエンドポイントのエラーを適切にハンドリング
  • 環境変数管理: 本番環境での環境変数の適切な管理

この実装により、Railway.appでも複数のcronジョブを効率的に実行できるようになります。プロジェクトの要件に応じて、実行間隔やエンドポイントをカスタマイズして活用してください。

picture
hanzochang - 半澤勇大
慶應義塾大学卒業後、Webプランナーとして勤務。 ナショナルクライアントのキャンペーンサイトの企画・演出を担当。 その後開発会社に創業メンバーとして参加。 Fintech案件や大手企業のDXプロジェクトに関わり、その後個人事業主として独立し、 2023年にWeb3に特化した開発会社として法人化しました。 現在はWeb3アプリ開発を中心にAI開発フローの整備を行っています。
また、趣味で2017年ごろより匿名アカウントでCryptoの調査等を行い、 ブロックチェーンメディアやSNSでビットコイン論文等の図解等を発信していました。
X (Twitter)

お問い合わせはこちらから

ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。