Node.js 应用部署 #
项目概述 #
本教程将创建一个完整的 Node.js Express 应用,并部署到 Heroku。
项目结构 #
text
myapp/
├── src/
│ ├── index.js # 应用入口
│ ├── routes/
│ │ ├── index.js # 路由入口
│ │ ├── users.js # 用户路由
│ │ └── api.js # API 路由
│ ├── middleware/
│ │ ├── error.js # 错误处理
│ │ └── auth.js # 认证中间件
│ └── models/
│ └── user.js # 用户模型
├── tests/
│ └── app.test.js # 测试文件
├── package.json
├── Procfile
├── .env.example
├── .gitignore
└── app.json
创建项目 #
初始化项目 #
bash
mkdir myapp && cd myapp
npm init -y
安装依赖 #
bash
npm install express pg redis ioredis dotenv helmet cors morgan
npm install --save-dev nodemon jest supertest
package.json #
json
{
"name": "myapp",
"version": "1.0.0",
"description": "Node.js Express app on Heroku",
"main": "src/index.js",
"engines": {
"node": "20.x"
},
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src/"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"morgan": "^1.10.0",
"pg": "^8.11.3"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"supertest": "^6.3.4"
}
}
应用代码 #
入口文件 #
javascript
// src/index.js
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const { Pool } = require('pg');
const Redis = require('ioredis');
const routes = require('./routes');
const errorHandler = require('./middleware/error');
const app = express();
const PORT = process.env.PORT || 3000;
// 数据库连接
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: false
});
// Redis 连接
const redis = new Redis(process.env.REDIS_URL, {
tls: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: undefined
});
// 中间件
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(morgan('combined'));
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 路由
app.use('/', routes);
// 错误处理
app.use(errorHandler);
// 启动服务器
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
pool.end();
redis.quit();
process.exit(0);
});
});
module.exports = app;
路由 #
javascript
// src/routes/index.js
const express = require('express');
const router = express.Router();
const users = require('./users');
const api = require('./api');
router.get('/', (req, res) => {
res.json({
message: 'Welcome to My App',
version: '1.0.0'
});
});
router.use('/users', users);
router.use('/api', api);
module.exports = router;
javascript
// src/routes/users.js
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: false
});
router.get('/', async (req, res, next) => {
try {
const { rows } = await pool.query('SELECT * FROM users ORDER BY created_at DESC');
res.json(rows);
} catch (error) {
next(error);
}
});
router.get('/:id', async (req, res, next) => {
try {
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[req.params.id]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(rows[0]);
} catch (error) {
next(error);
}
});
router.post('/', async (req, res, next) => {
const { email, name } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
try {
const { rows } = await pool.query(
'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
[email, name]
);
res.status(201).json(rows[0]);
} catch (error) {
if (error.code === '23505') {
return res.status(409).json({ error: 'Email already exists' });
}
next(error);
}
});
module.exports = router;
错误处理 #
javascript
// src/middleware/error.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
module.exports = errorHandler;
配置文件 #
Procfile #
text
web: node src/index.js
release: npm run db:migrate
.env.example #
text
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
SECRET_KEY=your-secret-key
.gitignore #
text
node_modules/
.env
.DS_Store
*.log
coverage/
app.json #
json
{
"name": "myapp",
"description": "Node.js Express app on Heroku",
"repository": "https://github.com/username/myapp",
"keywords": ["nodejs", "express", "heroku"],
"buildpacks": [
{
"name": "heroku/nodejs"
}
],
"env": {
"NODE_ENV": {
"description": "Node environment",
"value": "production"
},
"SECRET_KEY": {
"description": "Secret key",
"generator": "secret"
}
},
"formation": {
"web": {
"quantity": 1,
"size": "eco"
}
},
"addons": [
{
"plan": "heroku-postgresql:mini"
},
{
"plan": "heroku-redis:mini"
}
],
"scripts": {
"postdeploy": "npm run db:migrate"
}
}
数据库迁移 #
迁移脚本 #
javascript
// scripts/migrate.js
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: false
});
async function migrate() {
console.log('Running migrations...');
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
`);
console.log('Migrations completed');
await pool.end();
}
migrate().catch(console.error);
json
// package.json
{
"scripts": {
"db:migrate": "node scripts/migrate.js"
}
}
测试 #
测试文件 #
javascript
// tests/app.test.js
const request = require('supertest');
const app = require('../src/index');
describe('App', () => {
test('GET / should return welcome message', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.body.message).toBe('Welcome to My App');
});
test('GET /health should return status ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('ok');
});
test('GET /users should return array', async () => {
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
部署 #
创建 Heroku 应用 #
bash
# 登录 Heroku
heroku login
# 创建应用
heroku create myapp
# 添加数据库
heroku addons:create heroku-postgresql:mini
# 添加 Redis
heroku addons:create heroku-redis:mini
# 设置环境变量
heroku config:set SECRET_KEY=$(openssl rand -hex 32)
heroku config:set NODE_ENV=production
部署应用 #
bash
# 初始化 Git
git init
git add .
git commit -m "Initial commit"
# 部署
git push heroku main
# 运行迁移
heroku run npm run db:migrate
# 打开应用
heroku open
验证部署 #
bash
# 查看日志
heroku logs --tail
# 检查状态
heroku ps
# 测试 API
curl https://myapp.herokuapp.com/health
curl https://myapp.herokuapp.com/users
持续集成 #
GitHub Actions #
yaml
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
REDIS_URL: redis://localhost:6379
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.13.15
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: myapp
heroku_email: ${{ secrets.HEROKU_EMAIL }}
监控 #
添加监控 #
bash
# 添加日志服务
heroku addons:create papertrail:choklad
# 添加 APM
heroku addons:create newrelic:wayne
# 添加错误追踪
heroku addons:create sentry:developer
配置 New Relic #
javascript
// newrelic.js
exports.config = {
app_name: [process.env.NEW_RELIC_APP_NAME || 'My App'],
license_key: process.env.NEW_RELIC_LICENSE_KEY,
logging: { level: 'info' }
};
javascript
// src/index.js 顶部
if (process.env.NEW_RELIC_LICENSE_KEY) {
require('newrelic');
}
下一步 #
Node.js 应用部署完成后,接下来学习 Python 应用部署 了解 Python 应用的部署!
最后更新:2026-03-28