This article is available in Indonesian

🇮🇩 Baca dalam Bahasa Indonesia

May 30, 2026

🇬🇧 English

Fleet Management Part 8: DevSecOps, CI/CD & Production Deployment

Complete DevSecOps pipeline with GitHub Actions. Security scanning, Docker, CI/CD, monitoring, and production deployment for enterprise applications.

8 min read

What is DevSecOps?

Traditional development looks like this:

Developers build → QA tests → Security audits → Operations deploys (weeks) (days) (days) (hours)

DevSecOps integrates security and operations into every step of development:

Code → Lint → Test → Security Scan → Build → Deploy → Monitor ↑ ↑ ↑ ↑ ↑ ↑ Automated Automated Automated Automated Automated Automated

The key idea: Security is not a phase — it's a practice. Every commit is linted, tested, scanned, and deployed automatically. Issues are caught in minutes, not weeks.


Git Workflow: Branching Strategy

For our fleet management system with 4-6 developers, we use GitHub Flow (simplified Git Flow):

main ─────────────────────────────────────────→ Production │ ↑ ├── feature/fleet-dashboard ─────────┤ (Pull Request + Review) │ │ ├── feature/nestjs-api ──────────────┤ (Pull Request + Review) │ │ ├── fix/gps-timeout ─────────────────┘ (Pull Request + Review)

Branch Naming Convention

feature/ → New features (feature/driver-management) fix/ → Bug fixes (fix/gps-data-loss) hotfix/ → Critical production (hotfix/auth-bypass) chore/ → Maintenance (chore/update-dependencies) docs/ → Documentation (docs/api-swagger)

Commit Convention (Conventional Commits)

feat(fleet): add vehicle tracking map component fix(telemetry): resolve GPS data timeout on slow connections docs(api): add swagger documentation for fleet endpoints refactor(auth): extract JWT validation into guard test(driver): add unit tests for license expiry check chore(deps): update NestJS to v11

Why this matters: Automated release notes, clear git history, and easy to search. When a bug appears, you can quickly find which commit introduced it.


CI/CD Pipeline with GitHub Actions

Here's our complete pipeline:

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # ============================================
  # Stage 1: Code Quality
  # ============================================
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint          # ESLint
      - run: npm run type-check    # TypeScript compiler check
      - run: npm run format:check  # Prettier check

  # ============================================
  # Stage 2: Tests
  # ============================================
  test:
    runs-on: ubuntu-latest
    needs: lint
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: fleet_test
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run test:unit     # Unit tests
      - run: npm run test:e2e      # E2E tests
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/fleet_test
          REDIS_URL: redis://localhost:6379

  # ============================================
  # Stage 3: Security Scanning
  # ============================================
  security:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      # Dependency vulnerability scanning
      - run: npm audit --audit-level=high

      # SAST (Static Application Security Testing)
      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/typescript
            p/nodejs
            p/sql-injection

  # ============================================
  # Stage 4: Build & Deploy
  # ============================================
  deploy:
    runs-on: ubuntu-latest
    needs: [test, security]
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t fleet-api:${{ github.sha }} .

      - name: Deploy to production
        run: |
          ssh deploy@${{ secrets.SERVER_IP }} << 'EOF'
            cd /var/www/fleet-api
            git pull origin main
            npm ci --production
            npm run build
            pm2 restart fleet-api
          EOF

What Each Stage Does

StageWhat It ChecksTimeBlocks Deploy?
LintCode style, TypeScript errors~30s✅ Yes
TestUnit + E2E tests pass~2 min✅ Yes
SecurityVulnerability scanning~1 min✅ Yes
DeployBuild + deploy to server~3 minN/A

Security Scanning in Detail

1. Dependency Audit

# Check for known vulnerabilities in dependencies
npm audit

# Fix automatically where possible
npm audit fix

# For production, fail on high/critical
npm audit --audit-level=high

Senior Tip: Run npm audit weekly, not just in CI. Dependencies get new vulnerabilities discovered all the time.

2. Static Application Security Testing (SAST)

SAST scans your source code for security issues without running it:

// Semgrep catches these patterns:

// ❌ SQL Injection vulnerability
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// ✅ Safe — parameterized query
const query = 'SELECT * FROM users WHERE id = $1';
await pool.query(query, [userId]);

// ❌ XSS vulnerability
element.innerHTML = userInput;

// ✅ Safe — use textContent
element.textContent = userInput;

// ❌ Hardcoded secrets
const API_KEY = 'sk_live_abc123secret';

// ✅ Safe — use environment variables
const API_KEY = process.env.API_KEY;

3. Container Scanning

# Scan Docker images for OS-level vulnerabilities
- name: Scan Docker image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: fleet-api:${{ github.sha }}
    severity: 'CRITICAL,HIGH'
    exit-code: '1'  # Fail if critical issues found

Docker: Containerization

NestJS Dockerfile (Multi-stage Build)

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production (smaller image)
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --production && npm cache clean --force
COPY --from=builder /app/dist ./dist

# Security: Don't run as root
RUN addgroup -g 1001 appgroup && \
    adduser -S -u 1001 -G appgroup appuser
USER appuser

EXPOSE 3001
CMD ["node", "dist/main.js"]

Why multi-stage? The build stage has devDependencies (300MB+). The production stage has only what's needed to run (~80MB). Smaller image = faster deploys + smaller attack surface.

Docker Compose for Local Development

# docker-compose.yml
version: '3.8'

services:
  fleet-api:
    build: ./apps/api
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgresql://dev:dev@postgres:5432/fleet
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis

  admin:
    build: ./apps/admin
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=mysql
      - DB_DATABASE=fleet_admin
    depends_on:
      - mysql

  postgres:
    image: postgis/postgis:16-3.4
    environment:
      POSTGRES_DB: fleet
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    volumes:
      - postgres_data:/var/lib/postgresql/data

  mysql:
    image: mysql:8
    environment:
      MYSQL_DATABASE: fleet_admin
      MYSQL_ROOT_PASSWORD: dev
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:
  mysql_data:

Production Deployment

Nginx Reverse Proxy Configuration

# /etc/nginx/sites-available/fleet
server {
    listen 80;
    server_name fleet.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name fleet.example.com;

    ssl_certificate /etc/letsencrypt/live/fleet.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/fleet.example.com/privkey.pem;

    # Security headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000" always;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

    location /api/ {
        limit_req zone=api burst=50 nodelay;
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /admin/ {
        proxy_pass http://127.0.0.1:8000;
        # Only allow internal IPs
        allow 10.0.0.0/8;
        deny all;
    }
}

Process Management with PM2

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'fleet-api',
      script: 'dist/main.js',
      instances: 'max',      // Use all CPU cores
      exec_mode: 'cluster',  // Cluster mode for load balancing
      env: {
        NODE_ENV: 'production',
        PORT: 3001,
      },
      max_memory_restart: '500M',
      log_date_format: 'YYYY-MM-DD HH:mm:ss',
    },
    {
      name: 'telemetry-service',
      script: 'dist/main.js',
      cwd: './apps/telemetry',
      instances: 2,
      env: {
        NODE_ENV: 'production',
        PORT: 3002,
      },
    },
  ],
};

Monitoring & Observability

Health Check Endpoint

// src/health/health.controller.ts
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private redis: RedisHealthIndicator,
  ) {}

  @Get()
  async check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
      () => this.redis.pingCheck('redis'),
      () => this.checkDiskSpace(),
      () => this.checkMemoryUsage(),
    ]);
  }
}

Structured Logging

// Use structured JSON logs — not console.log
this.logger.log({
  event: 'delivery_created',
  deliveryId: delivery.id,
  vehicleId: delivery.vehicleId,
  driverId: delivery.driverId,
  timestamp: new Date().toISOString(),
});

// This makes logs searchable and filterable
// "Show me all delivery events for vehicle X in the last hour"

Incident Response Playbook

When something goes wrong in production, having a documented process prevents panic:

SeverityExampleResponse TimeWho
P0 - CriticalSystem down, no tracking15 minutesOn-call + Tech Lead
P1 - HighGPS data delayed > 5 min1 hourOn-call developer
P2 - MediumAdmin panel slow4 hoursAssigned developer
P3 - LowUI bug, cosmetic issueNext sprintAny developer

Post-Mortem Template

After every P0/P1 incident, write a post-mortem:

## Incident: GPS Data Loss (2026-05-15)

### Timeline
- 14:32 — Alert: Telemetry service stopped processing GPS data
- 14:35 — On-call acknowledged, began investigation
- 14:42 — Root cause found: Redis connection pool exhausted
- 14:45 — Fix applied: Increased connection pool from 10 to 50
- 14:48 — Service recovered, backlog cleared by 14:55

### Root Cause
Redis connection pool was set to 10 (default). Under peak load
(200 trucks × 6 updates/minute = 1200 ops/min), connections
were exhausted faster than they were released.

### Action Items
- [x] Increase Redis connection pool to 50
- [x] Add Redis connection pool monitoring to dashboard
- [ ] Add circuit breaker for Redis operations
- [ ] Load test with 2x expected traffic

Series Recap

Congratulations! Over 8 parts, we've built a complete enterprise Fleet Management System:

PartTopicKey Skills Demonstrated
1System ArchitectureSDLC, architecture design, tech stack decisions
2Next.js DashboardTypeScript, React, SSR, component architecture
3NestJS BackendNestJS, DI, DTOs, clean architecture
4Laravel AdminPHP, Laravel, Filament, event-driven architecture
5Database DesignPostgreSQL, MySQL, Redis, polyglot persistence
6SOLID PrinciplesClean code, design patterns, code review
7MicroservicesService boundaries, saga pattern, API gateway
8DevSecOpsCI/CD, security scanning, Docker, deployment

This isn't just theory — these are the patterns and practices I use daily as a senior full-stack developer. The ability to architect, build, secure, and deploy enterprise applications end-to-end is what separates senior developers from those who just write code.


This concludes the Fleet Management System series. If you found this helpful, check out my other series on Laravel E-Learning and React Native Event Management. Feel free to reach out with questions!

Continue Reading

Previous article

← Previous Article

Fleet Management Part 7: From Monolith to Microservices

Next Article →

Rebuilding My Laravel E-Learning App: A Journey from 5.2 to Modern Laravel

Next article