Final Project: Deploy & CI/CD
From code commit to the user’s browser, there are several steps in between—deployment, CI/CD, monitoring, and performance optimization—and this is the final step in getting your application truly “live.”
1. What You'll Learn
- The Complete Vercel Deployment Process (vercel.json + PostgreSQL + Custom Domain + Environment Variables)
- Self-hosted Docker Solution (Multi-stage Dockerfile Build + docker-compose.yml + Nginx + PM2)
- GitHub Actions CI/CD Pipeline (lint → test → build → deploy → Lighthouse CI)
- PostHog Event Tracking + @vercel/speed-insights + Sentry Error Monitoring
- Practical Performance Optimization (Lighthouse 90+/100+ next/image lazy loading + bundle analysis)
- Summary of the Next.js 16 Full-Stack Learning Path and Advanced Directions
2. A True Story of a DevOps Engineer
(1) Pain Point: Every deployment is like "stepping on a landmine"
Diana is a DevOps engineer at Acme Corp, responsible for the deployment and operations of TaskFlow. The team deploys 20 times a month, but every deployment is a nerve-wracking experience: manually uploading code via SSH resulted in missing environment variables (causing the production database to become unreachable); npm run build—it worked locally but caused an OOM (out of memory) error on the server; user errors weren’t discovered until two hours after deployment (due to a lack of monitoring alerts); and no one noticed that the Lighthouse score dropped from 85 to 62. The most recent deployment caused a 30-minute outage, resulting in over 200 complaints to customer service.
(2) A Solution for Automated CI/CD + Dual-Deployment + Monitoring
Automate quality checks with GitHub Actions + zero-configuration deployment with Vercel + self-hosted Docker alternatives + comprehensive monitoring with PostHog/Sentry.
# One line GitHub Actions Trigger Fully Automated Deployment
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: amondnet/vercel-action@v20
(3) Revenue
| Dimension | Manual Deployment | CI/CD Automation |
|---|---|---|
| Deployment Time | 45 minutes (including manual checks) | 8 minutes (fully automated) |
| Risk of Downtime | High (missing files/incorrect configuration) | Low (consistency guaranteed) |
| Issue Detected | User complaint 2 hours later | Real-time Sentry alert |
| Lighthouse Score | Manually run once a month | Automatic PR detection |
| Rollback Speed | 15-minute manual restore | 1-minute Vercel Rollback |
| Team Efficiency | DevOps Bottlenecks | Self-Service Deployment |
3. Deploying to Vercel
(1) Vercel Architecture
graph TB
subgraph "Vercel Edge Network"
A[Global CDN] --> B[Edge Functions]
B --> C[Serverless Functions]
C --> D[Vercel Postgres]
end
subgraph "GitHub"
E[Source Code] -->|Push| F[GitHub Actions]
F -->|Auto Deploy| A
end
subgraph "Monitoring"
G[Vercel Analytics]
H[Sentry]
I[PostHog]
end
C --> G
C --> H
B --> I
style A fill:#cce5ff
style E fill:#d4edda
style F fill:#ffeeba
▶ Example: vercel.json configuration
Output:
JSON configuration with keys: framework ("nextjs"), buildCommand, outputDirectory, installCommand, regions (iad1/hkg1/gru1), env, crons, headers (security headers + cache rules), and redirects.
{
"framework": "nextjs",
"buildCommand": "npm run build",
"outputDirectory": ".next",
"installCommand": "npm install",
"regions": ["iad1", "hkg1", "gru1"],
"env": {
"NEXT_PUBLIC_APP_URL": "https://taskflow.vercel.app"
},
"crons": [
{
"path": "/api/cron/daily-digest",
"schedule": "0 8 * * *"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
}
]
},
{
"source": "/uploads/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"redirects": [
{
"source": "/app",
"destination": "/dashboard",
"permanent": true
}
]
}
Output:
JSON configuration applied successfully.
▶ Example: Deploying with the Vercel CLI
Output:
Command completed.
# 1. Installation Vercel CLI
npm install -g vercel
# 2. Log In Vercel
vercel login
# 3. Related Projects
vercel link
# 4. Set Environment Variables
vercel env add DATABASE_URL
vercel env add AUTH_SECRET
vercel env add GOOGLE_CLIENT_ID
vercel env add GOOGLE_CLIENT_SECRET
vercel env add AUTH_URL
vercel env add NEXT_PUBLIC_POSTHOG_KEY
vercel env add SENTRY_DSN
# 5. Deployment Preview
vercel
# 6. Deployment Production
vercel --prod
# 7. View the deployment log
vercel logs
Output:
added 127 packages in 3.2s
38 packages are looking for funding
run `npm fund` for details
Environment variable set.
Environment variable set.
Environment variable set.
Environment variable set.
Environment variable set.
Environment variable set.
Environment variable set.
Vercel CLI 28.0.0
? Set up and deploy "C:\Users\admin\taskflow"? [Y/n] Y
? Which scope? Acme Corp
? Link to existing project? No
? What's your project's name? taskflow
? In which directory is your code? ./
Auto-detected Project Settings (Next.js):
- Build Command: npx next build
- Output Directory: .next
- Node.js Version: 20.x
✅ Production: https://taskflow.vercel.app [7s]
▶ Example: Connecting to Vercel Postgres
# 1. Create Postgres Database
vercel env add DATABASE_URL
# 2. Installation Vercel Postgres SDK
npm install @vercel/postgres
# 3. Update Prisma Data Sources
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Vercel Postgres Automatic Connection Pool Management
// On Vercel, enable shadowDatabaseUrl to avoid migration conflicts
}
Output:
Prisma schema updated. Run npx prisma generate to sync client.
// src/lib/db.ts
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
4. Self-hosted Docker
(1) Self-hosted architecture
graph TB
subgraph "Docker Host"
A[Nginx<br/>Reverse Proxy] --> B[Node.js App<br/>(PM2)]
B --> C[PostgreSQL<br/>(Container)]
end
subgraph "External"
D[DNS: taskflow.example.com]
end
D --> A
A -->|SSL Termination| B
style A fill:#d4edda
style B fill:#cce5ff
style C fill:#f8d7da
▶ Example: Multi-stage builds in Dockerfile
Output:
Sending build context to Docker daemon 4.096kB
Step 1/8 : FROM node:20-alpine AS deps
---> 1dd67de0f936
...
Successfully built a7b3c2d1e0f9
Successfully tagged taskflow:latest
# ============================================
# Stage 1: Dependencies
# ============================================
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production --ignore-scripts
RUN npm ci --only=development --ignore-scripts
# ============================================
# Stage 2: Build
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
ENV NEXT_OUTPUT=standalone
RUN npx prisma generate
RUN npm run build
# ============================================
# Stage 3: Production Runner
# ============================================
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
Output:
Sending build context to Docker daemon 4.096kB
Step 1/8 : FROM node:20-alpine AS deps
---> 1dd67de0f936
...
Successfully built a7b3c2d1e0f9
Successfully tagged taskflow:latest
▶ Example: docker-compose.yml
Output:
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://taskflow:password@db:5432/taskflow
- AUTH_SECRET=${AUTH_SECRET}
- AUTH_URL=https://taskflow.example.com
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- NEXTAUTH_URL=https://taskflow.example.com
- NODE_ENV=production
depends_on:
db:
condition: service_healthy
restart: unless-stopped
networks:
- taskflow-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=taskflow
- POSTGRES_USER=taskflow
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "5432:5432"
restart: unless-stopped
networks:
- taskflow-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskflow"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
restart: unless-stopped
networks:
- taskflow-network
volumes:
postgres-data:
networks:
taskflow-network:
driver: bridge
Output:
Config sections: version, services, volumes, networks.
▶ Example: Nginx Configuration
Output:
Sections: version, services, volumes, networks.
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream taskflow_app {
server app:3000;
}
server {
listen 80;
server_name taskflow.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name taskflow.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 1000;
gzip_comp_level 6;
# Static assets (cache forever)
location /_next/static {
proxy_pass http://taskflow_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
expires 365d;
add_header Cache-Control "public, immutable";
}
# Uploads
location /uploads {
proxy_pass http://taskflow_app;
proxy_set_header Host $host;
expires 30d;
add_header Cache-Control "public, immutable";
}
# API routes (no cache)
location /api {
proxy_pass http://taskflow_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_no_cache 1;
proxy_cache_bypass 1;
}
# Everything else
location / {
proxy_pass http://taskflow_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
Output:
Nginx configuration applied. Reload to activate.
5. GitHub Actions CI/CD
(1) Pipeline Architecture
graph LR
A[Push to main] --> B[Lint]
B --> C[Type Check]
C --> D[Unit Test]
D --> E[Build]
E --> F[Deploy to Vercel]
F --> G[Lighthouse CI]
G --> H{Score >= 90?}
H -->|Yes| I[Success]
H -->|No| J[Rollback]
style A fill:#cce5ff
style F fill:#d4edda
style G fill:#ffeeba
style J fill:#f8d7da
6. Production Monitoring
(1) Monitoring Architecture
graph TB
subgraph "User Metrics"
A["PostHog<br/>Events & Funnels"]
B["Vercel Speed Insights<br/>Real User Monitoring"]
end
subgraph "Error Tracking"
C["Sentry<br/>Errors & Performance"]
end
subgraph "Performance"
D["Lighthouse CI<br/>Score Trends"]
E["Bundle Analyzer<br/>Bundle Size"]
end
subgraph "Alerts"
F["Sentry Alerts<br/>PagerDuty"]
G["GitHub Checks<br/>PR Comment"]
end
A --> F
B --> D
C --> F
D --> G
E --> G
style A fill:#d4edda
style B fill:#cce5ff
style C fill:#f8d7da
7. Performance Optimization
(1) Comparison of Optimization Strategies
| Strategy | Metrics | Before Optimization | After Optimization | Tools |
|---|---|---|---|---|
| next/image lazy | LCP | 3.2s | 1.5s | <Image loading="lazy"> |
| Bundle Split | TTI | 4.5s | 2.8s | @next/bundle-analyzer |
| Font Optimization | CLS | 0.25 | 0.02 | next/font display:swap |
| PPR Static Shell | FCP | 2.1s | 0.4s | experimental.ppr |
| Image CDN | LCP | 2.8s | 1.2s | Vercel Image Optimization |
| Caching strategy | TTFB | 800ms | 120ms | revalidateTag + CDN |
8. Complete Example: Deployment and Validation in a Production Environment
# ============================================
# TaskFlow Complete Production Deployment Process
# ============================================
# --- Plan A: Vercel Deployment ---
# 1. Pre-Build Checks
npm run lint && npx tsc --noEmit && npm run test -- --run
# 2. Build and Verify
npm run build
# Output: ✓ Compiled successfully in 12.8s
# 3. Deployment Preview
vercel
# Output: https://taskflow-git-feature-abc123.vercel.app
# 4. Run E2E Test
npx playwright test --url=https://taskflow-git-feature-abc123.vercel.app
# 5. Lighthouse Inspection
npx lhci autorun
# 6. Deployment Production
vercel --prod
# Output: https://taskflow.vercel.app (Production)
# 7. Configure a Custom Domain
vercel domains add taskflow.example.com
# Output: ✓ Domain taskflow.example.com added
# 8. Verification
curl -I https://taskflow.example.com/api/health
# Output: HTTP/2 200
# --- Plan B: Docker Self-hosted ---
# 1. Build Docker Image
docker build -t taskflow:latest .
# 2. Start the service
docker compose up -d
# 3. Run the database migration
docker compose exec app npx prisma migrate deploy
# 4. Run Seeding
docker compose exec app npx prisma db seed
# 5. Verification
curl http://localhost/api/health
# Output: {"status":"healthy","timestamp":"2026-07-06T10:00:00Z"}
# 6. View Logs
docker compose logs -f app
# 7. Monitoring Metrics
# - PostHog: https://app.posthog.com/project/taskflow
# - Sentry: https://sentry.io/organizations/acme/projects/taskflow
# - Vercel Dashboard: https://vercel.com/acme/taskflow
# --- Performance Validation ---
# Lighthouse Report
# Performance: 94/100 ✅
# Accessibility: 97/100 ✅
# Best Practices: 93/100 ✅
# SEO: 100/100 ✅
# Core Web Vitals (RUM)
# LCP: 1.2s ✅ (Objective < 2.5s)
# FID: 12ms ✅ (Objective < 100ms)
# CLS: 0.02 ✅ (Objective < 0.1)
9. Review and Advanced Topics in the Learning Path
(1) Next.js 16 Full-Stack Learning Roadmap
graph TB
subgraph "Phase 1: Basics (6 lessons)"
A1["01-02: Introduction + Environment"]
A2["03-04: Routing + Layout"]
A3["05-06: Navigation + Parallel Routes"]
end
subgraph "Phase 2: Data (7 lessons)"
B1["07-08: RSC + Data Collection"]
B2["09-10: SSG/ISR/PPR"]
B3["11-12: Cache + Server Actions"]
B4["13-14: Actions Advanced + API Routes"]
end
subgraph "Phase 3: Advanced (6 lessons)"
C1["15: Certification Auth.js"]
C2["16: Database Prisma"]
C3["17: Streaming"]
C4["18: Image/Font"]
C5["19: SEO/Metadata"]
C6["20: i18n/Middleware"]
end
subgraph "Phase 4: Production (6 lessons)"
D1["21-22: Unit + E2E Test"]
D2["23-24: CI/CD + Docker"]
D3["25-26: Performance + Safety/Migration"]
end
subgraph "Phase 5: Comprehensive Projects (4 lessons)"
E1["27: Initialization + Certification"]
E2["28: Dashboard + PPR"]
E3["29: CRUD + Server Actions"]
E4["30: Deployment + CI/CD + Surveillance"]
end
A1 --> A2 --> A3 --> B1
B1 --> B2 --> B3 --> B4 --> C1
C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> D1
D1 --> D2 --> D3 --> E1
E1 --> E2 --> E3 --> E4
style A1 fill:#d4edda
style E4 fill:#cce5ff
(2) List of Skills
| Skill | Corresponding Course | Level of Proficiency |
|---|---|---|
| App Router File System Routing | #03–#06 | Mastery |
| Server Components Mindset | #07 | Mastery |
| Data Retrieval and Caching | #08–#11 | Mastery |
| Server Actions CRUD | #12-#13, #29 | Mastery |
| Certification and Authorization | #15, #27 | Proficient |
| Prisma ORM Database | #16, #27 | Proficient |
| Testing (Vitest + Playwright) | #21-#22 | Proficient |
| CI/CD Pipeline | #23, #30 | Proficient |
| Vercel + Docker Dual-Deployment Solution | #24, #30 | Proficient |
| Performance Monitoring and Optimization | #25, #30 | Proficient |
| Security and Migration | #26 | Learn |
| PPR + Cache Components | #10-#11 | Mastery (Exclusive) |
(3) Areas for Further Study
| Topic | Learning Content | Recommended Resources |
|---|---|---|
| AI Integration | Vercel AI SDK + RAG + Streaming | vercel/ai (10k⭐) |
| Micro Frontends | Module Federation + Turborepo | Nx / Turborepo Official |
| GraphQL | Apollo + Next.js + Codegen | Apollo Client Docs |
| Real-time Apps | WebSocket + Socket.io + Liveblocks | Liveblocks Next.js Integration |
| Edge Computing | Edge Functions + Durable Objects | Cloudflare Workers |
| Platform Engineering | Backstage + Internal Developer Portal | Spotify Backstage |
❓ FAQ
node_modules.@latest/next's standalone mode outputs only the minimum set of files required for runtime (the server.js and .next subdirectories), excluding unnecessary packages from node_modules. When used with Docker multistage builds, the final image contains only the Next.js runtime, significantly reducing its size and attack surface.📖 Summary
- Zero-configuration Vercel deployment:
vercel --prodGo live with a single command; supports environment variables, domain names, and PostgreSQL - Three self-hosted Docker containers: Nginx (reverse proxy + SSL) + Node.js app (PM2 daemon) + PostgreSQL (persistence)
- A multi-stage Dockerfile build compresses a 1.2GB image down to 358MB, containing only runtime files
- GitHub Actions CI/CD: quality → build → deploy → lighthouse four-stage automated pipeline
- Set a score threshold of 90+ in Lighthouse CI to automatically check pull requests and prevent performance degradation
- PostHog tracks user events (page views, button clicks, conversion funnels), while Sentry captures runtime errors
@vercel/speed-insights+@vercel/analyticsZero-configuration access to real-user monitoring- Bundle Analyzer visualizes bundle size; Target < 300 kB initial JS
- TaskFlow is a comprehensive test of Next.js 16's full-stack capabilities: from routing, RSC, PPR, and Server Actions to Prisma, Auth.js, and CI/CD
📝 Exercises
-
Basic Exercise (⭐): Deploy TaskFlow to Vercel (free account), configure a custom domain (optional), and verify that
/api/healthreturns a 200 status code. Configure theNEXT_PUBLIC_POSTHOG_KEYenvironment variable and confirm that PostHog receives the pageview event. -
Advanced Exercise (⭐⭐): Test the self-hosted Docker solution locally—build a Docker image, start
docker-compose(App + Nginx + PostgreSQL), configure a self-signed SSL certificate, run database migrations, and verify HTTPS access. Commitdocker-compose.yml,nginx.conf, andDockerfileto the project repository. -
Challenge (⭐⭐⭐): Implement full function monitoring for TaskFlow: Define 5 key events in PostHog (task_created / task_completed / user_signup / file_uploaded / project_created) and add attributes (duration, browser, geographic location); Configure alert rules in Sentry (send Slack notifications and PagerDuty alerts when the error rate exceeds 5%); add Playwright E2E test steps to GitHub Actions (Deploy → E2E → Lighthouse in sequence); finally, submit a README.md file containing: a deployment architecture diagram, screenshots of the monitoring dashboard, a Lighthouse report, and a CI/CD status badge.



