Hướng dẫn deploy dự án Git lên Railway (chi tiết từng bước)
App này là Node.js + Express + MySQL + EJS. Railway host được cả web (Node) lẫn MySQL trong 1 project → phù hợp hơn Vercel (Vercel serverless không host MySQL, không chạy cron).
File
railway.json
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "npm start",
"healthcheckPath": "/health",
"healthcheckTimeout": 100,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}
ở gốc repo đã cấu hình sẵn: chạy
npm start, health check/health, tự restart khi lỗi. Bạn chỉ cần làm theo các bước dưới.
⭐ Điều QUAN TRỌNG NHẤT phải nhớ
Một project Railway có 2 service riêng biệt:
| Service | Icon | Vai trò | Biến cần đặt ở đây |
|---|---|---|---|
| boconcept | GitHub (mèo) | Web/API (chạy node index.js) |
TẤT CẢ biến DB_*, JWT_SECRET… |
| MySQL | Cá heo 🐬 | Database | Railway tự sinh (MYSQLHOST…), KHÔNG đụng vào |
❌ Lỗi phổ biến nhất: đặt biến DB_* vào service MySQL → web không đọc được →
báo connect ECONNREFUSED 127.0.0.1:3306. Biến DB_* BẮT BUỘC nằm trên service boconcept.
Bước 1 — Đẩy code lên GitHub
Railway deploy từ repo GitHub. (Repo này đã có sẵn remote HieuNM2809/boconcept.)
git add .
git commit -m "chore: railway deploy"
git push origin main
Bước 2 — Tạo project trên Railway
- Vào https://railway.app → đăng nhập bằng GitHub.
- New Project → Deploy from GitHub repo → chọn repo
boconcept. - Railway tự nhận diện Node (Nixpacks) và build theo
railway.json. - Lần deploy đầu sẽ Failed vì chưa có MySQL — bình thường, làm tiếp Bước 3.
Bước 3 — Thêm database MySQL
- Trong project → nút New (hoặc Create) → Database → Add MySQL.
- Đợi service MySQL chuyển trạng thái Online (chấm xanh).
- Railway tự sinh các biến trên service MySQL:
MYSQLHOST,MYSQLPORT,MYSQLUSER,MYSQLPASSWORD,MYSQLDATABASE,MYSQL_PUBLIC_URL… — để nguyên, không sửa.
Bước 4 — Đặt biến môi trường cho service boconcept (KHÔNG phải MySQL)
- Ở sơ đồ, bấm vào ô boconcept (icon GitHub) — KHÔNG bấm ô MySQL cá heo.
- Mở tab Variables.
- Bấm Raw Editor → dán nguyên khối dưới đây → Save:
DB_HOST=mysql.railway.internal
DB_PORT=3306
DB_NAME=railway
DB_USER=root
DB_PASS=<lấy từ MYSQLPASSWORD của service MySQL>
DB_DIALECT=mysql
JWT_SECRET=<chuỗi ngẫu nhiên dài, tự sinh>
TOKEN_EXPIRES_IN=24h
CORS_ORIGIN=*
BODY_LIMIT=1mb
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=300
ADMIN_USER=admin
ADMIN_PASS=<mật khẩu admin mạnh>
Giải thích từng biến (lấy giá trị ở đâu):
| Biến | Giá trị | Nguồn |
|---|---|---|
DB_HOST |
mysql.railway.internal |
= MYSQLHOST của service MySQL. Đây là host nội bộ, chỉ 2 service Railway thấy nhau — nhanh & miễn phí. Không dùng host public ở đây. |
DB_PORT |
3306 |
= MYSQLPORT |
DB_NAME |
railway |
= MYSQLDATABASE (Railway mặc định tên DB là railway). Dễ quên — bắt buộc có. |
DB_USER |
root |
= MYSQLUSER |
DB_PASS |
(chuỗi Railway sinh) | = MYSQLPASSWORD. Copy từ tab Variables của service MySQL. |
DB_DIALECT |
mysql |
Cố định (app dùng MySQL). |
JWT_SECRET |
(ngẫu nhiên) | Tự sinh, xem Bước 4b. |
TOKEN_EXPIRES_IN |
24h |
Mặc định dự án. |
Vì sao phải đổi tên
MYSQLHOST→DB_HOST? Vì code đọcprocess.env.DB_HOST(xemconfig/mysql.js), không đọcMYSQLHOST. Nên phải tạo biếnDB_*trỏ tới đúng giá trị.
Bước 4b — Sinh JWT_SECRET ngẫu nhiên
Chạy trên máy bạn (có Node):
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
Copy kết quả dán vào JWT_SECRET.
✅ Cách kiểm tra đã đặt đúng service
- Đúng: panel Variables ghi tiêu đề boconcept + icon GitHub.
- Sai: ghi MySQL + icon cá heo → xoá biến ở đó, làm lại trên boconcept.
- Trên service boconcept không nên thấy các dòng
MYSQLHOST/MYSQLPASSWORD(đó là của MySQL).
Lưu xong Railway tự redeploy.
Bước 5 — Nạp schema + seed vào MySQL (chỉ làm 1 lần)
DB Railway lúc mới tạo trống rỗng — chưa có bảng nào. Phải nạp doc/schema.sql + doc/seed.sql.
Vì host nội bộ không truy cập được từ máy bạn, phải dùng URL public.
-
Vào service MySQL → tab Variables → copy
MYSQL_PUBLIC_URL(dạngmysql://root:<pass>@<host>.proxy.rlwy.net:<port>/railway). -
Cách A — dùng script Node có sẵn
mysql2trong project (khỏi càimysqlclient):Tạo file
scripts/load-db.js:const fs = require('fs'); const mysql = require('mysql2/promise'); (async () => { const conn = await mysql.createConnection({ uri: process.env.MYSQL_PUBLIC_URL, // dán URL public vào biến môi trường khi chạy multipleStatements: true, charset: 'utf8mb4', }); for (const f of ['doc/schema.sql', 'doc/seed.sql']) { await conn.query(fs.readFileSync(f, 'utf8')); console.log('Applied', f); } await conn.end(); console.log('DONE'); })().catch(e => { console.error(e.message); process.exit(1); });Chạy (thay URL public thật):
MYSQL_PUBLIC_URL="mysql://root:xxx@yyy.proxy.rlwy.net:12345/railway" node scripts/load-db.js -
Cách B — nếu máy có sẵn
mysqlclient:mysql --default-character-set=utf8mb4 -h <PUBLIC_HOST> -P <PUBLIC_PORT> -u root -p<PASS> railway < doc/schema.sql mysql --default-character-set=utf8mb4 -h <PUBLIC_HOST> -P <PUBLIC_PORT> -u root -p<PASS> railway < doc/seed.sql
⚠️ Luôn dùng
utf8mb4, nếu không tiếng Việt sẽ thành?/�. Kết quả đúng: tạo 9 bảng, seed 7 categories + 3 products + 1 api_client (demo-client).
Bước 6 — Xác nhận deploy thành công
Vào service boconcept → tab Deploy Logs. Phải thấy:
> node index.js
Kết nối MySQL thành công
Server đang chạy tại http://localhost:3000
Nếu vẫn thấy ECONNREFUSED 127.0.0.1:3306 → biến DB_* chưa nằm trên service boconcept
(quay lại Bước 4).
Bước 7 — Bật domain công khai (lấy link web)
- Service boconcept → Settings → Networking → Generate Domain.
- Railway cấp link dạng
https://<tên>.up.railway.app→ đây là link trang web của bạn.
Kiểm tra:
https://<domain>/health→ trảokhttps://<domain>/→ trang chủ BoConcept (có sản phẩm)https://<domain>/api/products→ JSON danh sách sản phẩm
(Tuỳ chọn) Chạy cron jobs
npm run schedule cần process riêng. Thêm service thứ 2:
New → GitHub Repo (cùng repo) → Settings → Deploy → Start Command = npm run schedule,
rồi copy các biến DB_* y như service boconcept.
Bảng lỗi thường gặp
| Log / triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
connect ECONNREFUSED 127.0.0.1:3306 |
Biến DB_* đặt sai service (trên MySQL) hoặc chưa đặt |
Đặt DB_* trên service boconcept (Bước 4) |
ER_BAD_DB_ERROR: Unknown database |
Thiếu/sai DB_NAME |
Đặt DB_NAME=railway |
Access denied for user |
Sai DB_USER/DB_PASS |
Copy lại từ MYSQLUSER/MYSQLPASSWORD |
Trang chủ 500 / trống |
Chưa nạp schema/seed | Làm Bước 5 |
Tiếng Việt thành ?/� |
Nạp SQL thiếu utf8mb4 |
Nạp lại với --default-character-set=utf8mb4 |
| App restart liên tục | Thiếu JWT_SECRET hoặc DB chưa Online |
Kiểm tra biến + trạng thái MySQL |
npm warn config production… |
Chỉ là cảnh báo | Bỏ qua, vô hại |
⚠️ Bảo mật
- Không commit
.envlên GitHub (repo đã thêm.envvào.gitignore). Bí mật chỉ đặt trong tab Variables của Railway. - Nếu mật khẩu DB từng bị lộ (dán vào chat, commit nhầm…): vào service MySQL → Settings đổi
mật khẩu, rồi cập nhật lại
DB_PASStrên boconcept. - Cân nhắc tắt public networking của MySQL sau khi nạp xong schema (chỉ để mạng nội bộ).
All rights reserved