
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とは

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サービスのベースURLAPI_BEARER_TOKEN
: API用のBearerトークンWORKER_URL
: WorkerサービスのベースURLWORKER_BEARER_TOKEN
: Worker用のBearerトークン
デプロイ手順
-
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
-
Railway.appでプロジェクト作成
- Railway.appの管理画面でNew Projectを選択
- GitHubリポジトリを連携
- 環境変数を設定
-
デプロイの確認
- ログで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ジョブを効率的に実行できるようになります。プロジェクトの要件に応じて、実行間隔やエンドポイントをカスタマイズして活用してください。
お問い合わせはこちらから
ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。