📋 Overview

This guide will walk you through deploying a Node.js application to a VPS using Docker, GitLab CI/CD, and Nginx. By the end, you'll have:

  • ✅ A Node.js app running in Docker containers
  • ✅ Automated CI/CD pipeline with GitLab
  • ✅ Nginx reverse proxy configured
  • ✅ Automatic deployment on code push

📦 Prerequisites

What You Need

  • VPS Server: Ubuntu 20.04+ or similar Linux distribution
  • SSH Access: Root or sudo access to your VPS
  • GitLab Account: Access to a GitLab instance (self-hosted or GitLab.com)
  • Domain/IP: Your server's IP address or domain name
  • Node.js Application: Your application code in a GitLab repository
Before starting, ensure you have SSH access to your VPS and can log in as root or a user with sudo privileges.

🖥️ VPS Initial Setup

Step 1: Connect to Your VPS

1 SSH into your server:
ssh root@your-server-ip
# or
ssh user@your-server-ip

Step 2: Update System Packages

2 Update and upgrade your system:
apt update && apt upgrade -y

Step 3: Install Basic Tools

3 Install essential packages:
apt install -y curl wget git vim
Your VPS is now updated and ready. You should be able to run basic commands.

🐳 Docker Installation & Configuration

Step 1: Install Docker

1 Install Docker using the official script:
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

Step 2: Verify Docker Installation

2 Check Docker version:
docker --version
docker compose version
You should see Docker version 20.10+ and Docker Compose version 2.0+

Step 3: Add User to Docker Group (Optional)

3 If not using root, add your user to docker group:
usermod -aG docker $USER
# Log out and back in for changes to take effect

Step 4: Test Docker

4 Run a test container:
docker run hello-world
If you see "Hello from Docker!" message, Docker is working correctly!
Docker is installed and working. You can now run Docker containers.

🌐 Nginx Installation & Configuration

Step 1: Install Nginx

1 Install Nginx:
apt install -y nginx

Step 2: Start and Enable Nginx

2 Start Nginx service:
systemctl start nginx
systemctl enable nginx

Step 3: Verify Nginx is Running

3 Check Nginx status:
systemctl status nginx
You should see "active (running)" status

Step 4: Configure Nginx for Your Application

4 Create Nginx configuration file:
nano /etc/nginx/sites-available/your-app-name
Add the following configuration:
server {
    listen 80;
    server_name your-server-ip-or-domain;

    # Logging
    access_log /var/log/nginx/your-app-access.log;
    error_log /var/log/nginx/your-app-error.log;

    # Increase body size for file uploads
    client_max_body_size 100M;

    # Proxy settings
    location / {
        proxy_pass http://localhost:9002;
        proxy_http_version 1.1;
        
        # Headers
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
        
        # WebSocket support (if needed)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
Replace your-server-ip-or-domain with your actual server IP (e.g., 88.00.xx.xx) or domain name. Replace localhost:9002 with your application's port.

Step 5: Enable the Configuration

5 Create symbolic link to enable the site:
ln -s /etc/nginx/sites-available/your-app-name /etc/nginx/sites-enabled/

Step 6: Test and Reload Nginx

6 Test Nginx configuration:
nginx -t
If test passes, reload Nginx:
systemctl reload nginx
Nginx is installed and configured. Your app will be accessible via Nginx once the application is running.

🔧 GitLab CI/CD Setup

Part 1: Generate SSH Keys on VPS

1 SSH into your VPS and generate SSH key pair:
ssh root@your-server-ip
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t ed25519 -C "gitlab-ci-deploy" -f ~/.ssh/gitlab_ci_deploy -N ""
This creates two files:
  • ~/.ssh/gitlab_ci_deploy - Private key (keep secret!)
  • ~/.ssh/gitlab_ci_deploy.pub - Public key
2 Add public key to authorized_keys:
cat ~/.ssh/gitlab_ci_deploy.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
3 Display the private key (you'll need this for GitLab):
cat ~/.ssh/gitlab_ci_deploy
Copy the entire output including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----

Part 2: GitLab Navigation Guide

Finding GitLab CI/CD Variables

1 Navigate to your GitLab project:
  1. Go to your GitLab instance (e.g., https://your-gitlab.com)
  2. Click on your Project
  3. In the left sidebar, click Settings (gear icon)
  4. Click CI/CD in the settings menu
  5. Expand the Variables section

Adding GitLab CI/CD Variables

2 Click "Add variable" and add these variables one by one:
Variable Name Value Protected Masked
SSH_PRIVATE_KEY Paste the private key from step 3 above ✅ Yes ✅ Yes
DEPLOY_SERVER_HOST Your server IP (e.g., 88.00.xx.xx) ✅ Yes ❌ No
DEPLOY_USER SSH username (usually root) ✅ Yes ❌ No
DEPLOY_PATH Deployment path (e.g., /root/apps/your-app-name) ✅ Yes ❌ No
CI_REGISTRY_USER Your GitLab username ✅ Yes ❌ No
CI_REGISTRY_PASSWORD GitLab token (see below) ✅ Yes ✅ Yes

Getting GitLab Token (CI_REGISTRY_PASSWORD)

3 Option A: Personal Access Token (Recommended)
  1. Click your profile picture (top right) → Preferences
  2. In left sidebar, click Access Tokens
  3. Create new token:
    • Token name: CI/CD Deployment Token
    • Scopes: Check read_registry and write_registry
  4. Click Create personal access token
  5. Copy the token immediately (shown only once!)
Option B: Deploy Token
  1. Go to Project → Settings → Repository
  2. Expand Deploy tokens section
  3. Create token with read_registry and write_registry scopes
  4. Copy both username and token

Part 3: Set Up GitLab Runner

Check if Shared Runners are Available

1 Navigate to: Project → Settings → CI/CD → Runners
  • Check if "Shared runners" are enabled and available
  • If not available, you need to set up a project runner (see below)

Setting Up Project Runner (If Needed)

2 On your VPS, install GitLab Runner:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash
apt-get install gitlab-runner -y
3 Get registration token from GitLab:
  1. Go to Project → Settings → CI/CD → Runners
  2. Click New project runner
  3. Copy the registration token shown
4 Register the runner on your VPS:
gitlab-runner register

When prompted, enter:

  • GitLab URL: Your GitLab instance URL (e.g., https://your-gitlab.com)
  • Registration token: The token from step 3
  • Description: vps-runner
  • Tags: vps (or leave empty)
  • Executor: docker
  • Default Docker image: docker:24
5 Configure runner for Docker-in-Docker:
nano /etc/gitlab-runner/config.toml

Find the [[runners]] section and ensure:

[[runners]]
  ...
  executor = "docker"
  [runners.docker]
    privileged = true
    image = "docker:24"

Then restart the runner:

gitlab-runner restart
GitLab Runner is installed and registered. Your pipeline should now be able to run jobs.

⚙️ Application Configuration

Step 1: Create Dockerfile

1 Create Dockerfile in your project root:
# Multi-stage build for Node.js application

# Stage 1: Build stage
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Stage 2: Production stage
FROM node:18-alpine

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create app directory
WORKDIR /app

# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Copy dependencies from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copy application code
COPY --chown=nodejs:nodejs . .

# Create necessary directories and log files
RUN mkdir -p uploads allImages logs && \
    touch access.log logs/access.log logs/error.log logs/combined.log && \
    chown -R nodejs:nodejs uploads allImages logs access.log && \
    chmod -R 755 uploads allImages logs access.log

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 9002

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD node -e "require('http').get('http://localhost:9002/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]

# Start the application
CMD ["node", "index.js"]

Step 2: Create docker-compose.prod.yml

2 Create docker-compose.prod.yml:
services:
  app:
    image: your-app-name:latest
    container_name: your-app-name
    restart: unless-stopped
    env_file:
      - .env
    network_mode: host
    volumes:
      - ./uploads:/app/uploads
      - ./allImages:/app/allImages
      - ./logs:/app/logs
    depends_on:
      redis:
        condition: service_healthy

  redis:
    image: redis:7-alpine
    container_name: redis-service
    restart: unless-stopped
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 5

Step 3: Create .gitlab-ci.yml

3 Create .gitlab-ci.yml in your project root:
stages:
  - build
  - deploy

variables:
  DOCKER_IMAGE_NAME: $CI_registry_IMAGE:$CI_COMMIT_REF_SLUG
  TAG_LATEST: $CI_registry_IMAGE:latest

services:
  - docker:24-dind

build:
  stage: build
  image: docker:24
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker build -t "$DOCKER_IMAGE_NAME" .
    - docker push "$DOCKER_IMAGE_NAME"
    - |
      if [ "$CI_COMMIT_BRANCH" = "main" ]; then
        docker tag "$DOCKER_IMAGE_NAME" "$TAG_LATEST"
        docker push "$TAG_LATEST"
      fi
  only:
    - main
    - develop

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk update && apk add openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan "$DEPLOY_SERVER_HOST" >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - |
      ssh $DEPLOY_USER@$DEPLOY_SERVER_HOST "
        mkdir -p $DEPLOY_PATH
        cd $DEPLOY_PATH
        
        # Login to registry
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
        
        # Pull latest image
        docker pull $TAG_LATEST
        
        # Stop and remove existing container
        docker compose -f docker-compose.prod.yml down app || true
        
        # Start new container
        docker compose -f docker-compose.prod.yml up -d app
        
        # Cleanup unused images
        docker image prune -f
      "
  only:
    - main

🚀 Deployment Process

1 Commit and push your changes:
git add .
git commit -m "Add Docker and CI/CD configuration"
git push origin main
2 Monitor the pipeline:

Go to Project → Build → Pipelines in GitLab to see your pipeline running.

  • Build Stage: Builds the Docker image and pushes to Container Registry
  • Deploy Stage: SSHs into your VPS, pulls the image, and restarts the container
Once the pipeline succeeds, your application should be live at http://your-server-ip!

🔍 Troubleshooting

Common Issues

  • SSH Connection Failed: Check if correct private key is added to GitLab variables and public key is in authorized_keys on VPS.
  • Docker Permission Denied: Ensure the user is added to docker group or use root.
  • Port Already in Use: Check if another service is using port 9002 or 80.
  • Pipeline Fails: Check the job logs in GitLab for error messages.
View Logs on VPS:
docker logs -f your-app-name
# or
docker compose -f docker-compose.prod.yml logs -f app