Taryana
Back to All Technical Articles/Building an Automated CI/CD Pipeline to VPS with GitHub Actions, GHCR, and Docker Compose
DevOpsCI/CDDockerGitHub ActionsGHCRNext.jsLinux

Building an Automated CI/CD Pipeline to VPS with GitHub Actions, GHCR, and Docker Compose

Lessons from real production war stories handling server freezes and OOM crashes during build: how offloading compilation to GitHub Actions and GHCR keeps a cost-effective 2 vCPU VPS cool and stable.

Taryana•2026-09-27•12 min read

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 (): The server entered a under Out-Of-Memory stress, leaving no alternative but a forced 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.

Executive Summary & Core Insight

Solution Blueprint: Automated ("Build Outside, Run Inside") To liberate our VPS from resource-hungry build cycles, we architected an automated 5-pillar pipeline:

  1. 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.
  2. 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.
  3. 3GitHub (): Storing container artifacts privately and securely with deterministic tagging based on timestamps and Git short SHAs.
  4. 4SSH Rollout & Docker Compose: The VPS simply pulls pre-built images over encrypted SSH and updates services seamlessly via docker compose.
  5. 5Automated Health Checks & : Validating application readiness via periodic of /api/health before confirming deployment success, alongside rich Discord notifications.
Diagram Arsitektur Pipeline CI/CD Otomatis: GitHub Actions → GHCR → VPSDevOps & Docker Pipeline
Memindahkan beban kompilasi Next.js ke cloud runner agar VPS hemat 2 vCPU tetap stabil dan adem
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
PushPush ImagePull ImageSSH Ed25519 Tunnel200 OK1. Dev WorkstationLocal RepositoryTarget Branch:origin/productionCommit Action:git push (Web Hook)Kompilasi Lokal:Zero Build Overhead(Tidak makan RAM/CPU)Webhook Triggered2. GitHub Actions RunnerCloud Isolated (ubuntu-latest)1Checkout & Setup BuildxMulti-stage cache inline2Next.js Standalone Build1.2 GB ➔ ~118 MB Minimalis3Deterministic TaggingYYYYMMDD-HHMMSS-SHABeban CPU VPS: 0% (Bebas OOM)GitHub Cloud Menanggung Semua KompilasiBuild Artifacts Sealed (~48s)3. GHCR (ghcr.io)Private Docker RegistryImage Repository:portfolio-frontendSize: ~118 MB (Compressed)Tags Strategy:• latest (active pointer)• 20260927-SHA (immutable)Automated Pruning:Simpan 3 Rilis TerakhirDigest Verified & Secured4. Production VPSUbuntu LTS (2 vCPU / 4 GB RAM)SSH Rollout Execution:docker compose pulldocker compose up -dContainer Runtime:portfolio_frontend:3000Next.js Standalone (node server.js)Otomasi Perawatan:Watchtower & Prune ImageCPU: 5-10% Sesaat (Stabil)5. Alert✓Health 200Verified💬DiscordWebhook✉SMTP AlertLive ✅

Semua Alur: Live Automated Pipeline

Alur end-to-end otomatis dari commit developer hingga rilis produksi

Tahap 0All dari 05

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.

Eksekusi Perintah / Terminal SnapshotBash / Shell / Cloud Runner
$git push origin production
> Webhook event dispatched: push to refs/heads/production
> GitHub Actions workflow 'Deploy Frontend to VPS' queued.
> Pipeline status: RUNNING (Live automated orchestration).
Alur Lengkap: Build & kompilasi terisolasi 100% di cloud runner. VPS produksi bebas dari OOM dan server freeze.
Zero VPS CPU Load10x Faster (~85s)

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 :

Engineering DimensionManual In-Place Server DeploymentAutomated + PipelineReal Production Impact
VPS Server RAM LoadSpikes up to 3.8 GB (Severe Risk)Flat ~80 MB (Compiled in Cloud)Zero server freeze risk
CPU Usage During Release100% Locked (Entire site goes down)5% - 10% Brief Surge (Container Spin-up)Site remains smooth for visitors
Release Cycle Duration10 - 15 minutes per manual update~85 seconds (Automated push to live)10x Faster & Fully Hands-Off
Application Package Size1.2 GB (Bulky node_modules folder)~118 MB (Next.js Standalone)90% Disk Storage Savings
Source Code SecurityRaw code & .env files stored on serverOnly binary image on VPS; code is safeEnterprise-grade security
Rollback SimplicityVery difficult (Requires checkout & rebuild)Instant 2 Seconds (Just switch image tag)Panic-free, instant rollbacks

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 . 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 , 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":

JAVASCRIPT Reference7 baris kode
> /** @type {import('next').NextConfig} */ | const nextConfig = { ...Klik untuk melihat implementasi lengkap →

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):

DOCKERFILE Reference44 baris kode
> # Stage 1: Base dependencies | FROM node:22-alpine AS deps ...Klik untuk melihat implementasi lengkap →

Add a .dockerignore file to ensure local secrets and build cache do not enter the build context:

TEXT Reference10 baris kode
> Dockerfile | .dockerignore ...Klik untuk melihat implementasi lengkap →

Step 3: Implement the Health Check Endpoint

Create a route handler at src/app/api/health/route.ts:

TypeScript CRDT Reference13 baris kode
> import { NextResponse } from "next/server"; ...Klik untuk melihat implementasi lengkap →

This endpoint allows the 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:

BASH Reference1 baris kode
> ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_vps ...Klik untuk melihat implementasi lengkap →

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:

BASH Reference3 baris kode
> sudo mkdir -p /var/www/portfolio | sudo chown -R $USER:$USER /var/www/portfolio ...Klik untuk melihat implementasi lengkap →

3. Create docker-compose.yml on VPS

Inside /var/www/portfolio, create docker-compose.yml:

YAML Reference27 baris kode
> services: | portfolio-frontend: ...Klik untuk melihat implementasi lengkap →

Step 5: Configure GitHub Actions Workflow (deploy.yml)

Create .github/workflows/deploy.yml in your repository:

YAML Reference178 baris kode
> name: Deploy Frontend to VPS ...Klik untuk melihat implementasi lengkap →

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 and Docker Compose pipeline delivered dramatic operational improvements:

Operational MetricManual In-Place Server DeploymentAutomated + PipelineProduction Impact
VPS Server RAM UsageSpikes to 3.8 GB ( Crash)Flat ~80 MB100% Free of Server Freezes
CPU Load During Release100% Frozen / Kernel Panic5% - 10% Brief SurgeZero disruption to visitors
Release Cycle Duration10 - 15 minutes per update~85 seconds (Automated)10x Faster Deployment Speed
Container Image Size1.2 GB (Full node_modules)~118 MB (Next.js Standalone)90% Disk Storage Savings
Audit Trail & RollbackProne to local discrepanciesDeterministic SHA Tagging100% Audit Trail & 2s Rollback

7. Key DevOps Production Takeaways

Infrastructure budget constraints often push us to become more disciplined, creative engineers. Two fundamental principles we always live by:

  1. 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.
  2. 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 , your VPS can focus 100% on its primary mission: delivering high-performance, uninterrupted web experiences for your users.

CI/CD Pipeline Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →