1. Why Running Builds Directly on Production VPS Servers Inevitably Causes Downtime
Have you ever found yourself in that challenging dilemma where you want to architect a modern, resilient web platform using Next.js and NestJS, but are constrained by a lean infrastructure budget that limits you to a cost-effective VPS with 2 vCPUs and 4 GB RAM?
That exact nerve-wracking ordeal is what I encountered when I first launched our interactive web platform into production. On paper, those server specifications are more than capable of serving thousands of incoming visitors per hour during normal runtime conditions (runtime traffic).
However, the real operational nightmare began the moment one command was executed in the VPS terminal: npm run build.
The instant compilation and bundling began directly on the server, a cascade of severe system failures erupted:
- CPU Locked at 100%: Every CPU cycle was starved by TypeScript transpilation and module minification.
- Memory Spiked Beyond Physical RAM: RAM usage surged until zero headroom remained for kernel processes and background daemons.
- SSH Terminated & Site Dropped: The server abruptly stopped responding, active visitors saw 502/504 errors, and SSH sessions disconnected instantly.
- Total Kernel Freezeℹ (OOM Starvationℹ): The server entered a hard freezeℹ under Out-Of-Memory stress, leaving no alternative but a forced hard rebootℹ through the hosting provider's cloud dashboard.
In a state of panic, I briefly considered a shortcut: building locally on my laptop and committing the .next artifacts to Git. However, that introduced new disasters: repository bloat of hundreds of megabytes in days, extreme security risks of leaking local secrets, and native binary incompatibilities between macOS and Ubuntu Linux.
The realization was clear: we needed a disciplined engineering paradigm: Never burden a production VPS with code compilation.
Solution Blueprint: Automated CI/CD Pipelineℹ ("Build Outside, Run Inside") To liberate our VPS from resource-hungry build cycles, we architected an automated 5-pillar pipeline:
- 1GitHub Actions Cloud Runners: Shifting the heavy compute workload of building, compiling, and bundling to high-performance GitHub runners without consuming 1% of VPS CPU.
- 2Multi-Stage Dockerfile & Standalone Next.js: Packaging the application with
output: "standalone"to bundle only essential runtime dependencies, slashing image sizes from 1.2 GB to ~118 MB. - 3GitHub Container Registryℹ (ghcr.ioℹ): Storing container artifacts privately and securely with deterministic tagging based on timestamps and Git short SHAs.
- 4SSH Rollout & Docker Compose: The VPS simply pulls pre-built images over encrypted SSH and updates services seamlessly via
docker compose. - 5Automated Health Checks & Webhook Alertsℹ: Validating application readiness via periodic pollingℹ of
/api/healthbefore confirming deployment success, alongside rich Discord notifications.
Semua Alur: Live Automated Pipeline
Alur end-to-end otomatis dari commit developer hingga rilis produksi
Setiap git push ke branch production secara otomatis memicu orkestrasi cloud GitHub Actions. Beban kompilasi diisolasi di cloud runner, image standalone dipublikasikan ke GHCR, ditarik via SSH ke VPS, diverifikasi via health check, dan dilaporkan ke Discord.
> Webhook event dispatched: push to refs/heads/production > GitHub Actions workflow 'Deploy Frontend to VPS' queued. > Pipeline status: RUNNING (Live automated orchestration).
2. Deployment Topology & Operational Comparison Matrix
Before walking through the step-by-step configuration, let us contrast the operational performance differences between manual in-place deployments and our automated CI/CD pipelineℹ:
3. Core Architectural Components
A. GitHub Actions (Orchestration Engine)
Acts as the central conductor triggered automatically on pushes to the production branch, or on-demand via workflow_dispatch with runner flexibility (ubuntu-latest or self-hosted).
B. Multi-Stage Dockerfile with Next.js Standalone Mode
Next.js compiles using output: "standalone". It traces imports and isolates essential modules into a minimal container. The container executes under a hardened non-root user (nextjs:nodejs).
C. GitHub Container Registry (ghcr.io)
Container images are published securely to GHCRℹ. Each release is tagged with composite identifier YYYYMMDD-HHMMSS-SHA alongside a pointer tag latest. An automated retention policy preserves the 3 latest production releases to control storage overhead.
D. SSH Runner & Docker Compose Rollout
The GitHub runner connects over encrypted SSH using VPS_SSH_KEY, logs into GHCRℹ, pulls the specified image, and redeploys containers via docker compose up -d --remove-orphans.
E. Automated Health Checks & Multi-Channel Alerting
The pipeline polls GET /api/health up to 6 times. Upon receiving an HTTP 200 OK response, a green embed card dispatches to Discord. If verification fails, the deployment aborts and alerts trigger immediately.
4. Step-by-Step Implementation Guide
Step 1: Configure Next.js Standalone Mode
In your next.config.mjs file, enable output: "standalone":
This instructs Next.js to trace all required dependencies and copy them into .next/standalone, eliminating bulky node_modules folders from production containers.
Step 2: Create a Production Multi-Stage Dockerfile
Create a Dockerfile at the root of your repository with a 3-stage architecture (Stage deps, Stage builder, and Stage runner):
Add a .dockerignore file to ensure local secrets and build cache do not enter the build context:
Step 3: Implement the Health Check Endpoint
Create a route handler at src/app/api/health/route.ts:
This endpoint allows the CI/CD pipelineℹ to verify that Node.js has initialized cleanly and is ready to ingest traffic before completing deployment.
Step 4: Configure VPS Server, Directories, and GHCR Access
1. Generate SSH Keypair for Deployment
On your local machine, generate a dedicated deployment SSH key:
Append the public key content (~/.ssh/github_actions_vps.pub) to ~/.ssh/authorized_keys on your VPS.
2. Create the Deployment Directory on VPS
Log into your VPS and configure the application directory:
3. Create docker-compose.yml on VPS
Inside /var/www/portfolio, create docker-compose.yml:
Step 5: Configure GitHub Actions Workflow (deploy.yml)
Create .github/workflows/deploy.yml in your repository:
5. Three Vital Lessons from the Trenches (War Stories)
A. The Danger of the latest Tag vs Deterministic Tagging
Relying solely on the latest tag is a dangerous anti-pattern. If a newly deployed image introduces a regression bug, rolling back becomes chaotic because latest always points to the newest commit. With composite tags like YYYYMMDD-HHMMSS-SHA (e.g. 20260927-142010-78ab9c4), images are immutable. Rolling back takes 2 seconds by updating the IMAGE_TAG variable in .env to the prior release hash.
B. Preventing Secret Leaks in Docker Layers
Never inject database credentials or secret keys into Dockerfile ENV instructions! Any ENV instruction is permanently baked into the image layer metadata and readable via docker inspect.
- Public variables needed for client compilation are passed via
build-args. - Sensitive secrets are injected strictly at runtime via environment files on the VPS.
C. Automated Server Disk Hygiene
The docker image prune -f step executed at the end of the deployment SSH script safely cleans up dangling images without deleting active production containers, keeping server disk usage steady below 40%.
6. Real Production Metrics: Before vs After
Adopting this GHCRℹ and Docker Compose pipeline delivered dramatic operational improvements:
7. Key DevOps Production Takeaways
Infrastructure budget constraints often push us to become more disciplined, creative engineers. Two fundamental principles we always live by:
- 1A Budget VPS Is No Reason to Abandon Modern Tech Stacks: Next.js and NestJS thrive smoothly on an economical 2 vCPU server, provided your deployment architecture is sound and avoids in-place compilation.
- 2Separation of Concerns Applies to Infrastructure: Production servers exist to serve visitor traffic, not to build software artifacts.
By offloading builds to GitHub Actions and securing images in GitHub Container Registryℹ, your VPS can focus 100% on its primary mission: delivering high-performance, uninterrupted web experiences for your users.