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