本文へ移動
hanzochang

技術と実践のノート

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

公開

著者:株式会社hanzochang

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

はじめに

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サービスを作成することで、この問題を解決できます:

text
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Cron Service  │───▶│   API Service   │    │  Worker Service │
│   (Railway)     │    │   Instance      │    │   Instance      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘

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

                    複数エンドポイントへの
                    定期的なリクエスト
技術スタック
  • Runtime: Node.js 18+
  • Language: TypeScript
  • Cron Library: node-cron
  • HTTP Client: axios
  • Container: Docker

実装手順

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

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

shell
mkdir awesome-cron
cd awesome-cron
npm init -y
shell
mkdir awesome-cron
cd awesome-cron
npm init -y
package.jsonの設定
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"
  }
}
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"
  }
}

プロジェクト構造

推奨するフォルダ構成:

text
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
text
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:

typescript
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 }
typescript
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:

typescript
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,
    },
  })
}
typescript
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:

typescript
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,
    },
  })
}
typescript
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:

typescript
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,
}
typescript
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:

typescript
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,
}
typescript
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:

typescript
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')
}
typescript
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:

typescript
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)
})
typescript
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の作成

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"]
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の設定

text
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.eslintcache
dist
text
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リポジトリの準備

    shell
    git init
    git add .
    git commit -m "Initial commit: Cron service implementation"
    git remote add origin <your-repository-url>
    git push -u origin main
    
    shell
    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ジョブでの実行結果を適切にログ出力することで、問題の早期発見が可能です:

typescript
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)
  }
}
typescript
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)
  }
}
リトライ機能の実装

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

typescript
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))
    }
  }
}
typescript
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ジョブを効率的に実行できるようになります。プロジェクトの要件に応じて、実行間隔やエンドポイントをカスタマイズして活用してください。

この記事を書いた人

Webプランナー、開発会社の創業メンバーを経て、Fintech・DX・Web3の企画・開発に従事。2021年に独立し、2023年に株式会社hanzochangを設立。現在はAIを活用した業務改善と、自社サービス「指定管理者制度AI」の開発・運用に取り組んでいます。

半澤勇大のXアカウント(新しいタブで開く)

この記事の株式会社hanzochangについて
少し紹介させてください!

この記事に関連する当社のサービス

業務改善・AI活用支援

転記・集計・資料作成にかかる時間を減らしませんか?

現在の業務と使っているシステムを整理し、AIが役立つ作業から改善します。試作で効果を確かめ、現場で使うアプリの開発・運用まで支援します。

サービスの詳細を見る
Jくんが多様な業務アプリを開発するイラスト。背景に多数のアプリが並ぶ。
  • 業務の整理

    手間のかかる作業と、改善する順番を明確に。

  • 試作・アプリ開発

    実際の手順に合わせて、使える仕組みを開発。

  • 導入・運用支援

    使い方の共有から、導入後の改善まで対応。

業務改善・AI活用について、お気軽にご相談ください。