📋 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:
- Go to your GitLab instance (e.g.,
https://your-gitlab.com) - Click on your Project
- In the left sidebar, click Settings (gear icon)
- Click CI/CD in the settings menu
- 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)
- Click your profile picture (top right) → Preferences
- In left sidebar, click Access Tokens
- Create new token:
- Token name:
CI/CD Deployment Token - Scopes: Check
read_registryandwrite_registry
- Token name:
- Click Create personal access token
- Copy the token immediately (shown only once!)
- Go to Project → Settings → Repository
- Expand Deploy tokens section
- Create token with
read_registryandwrite_registryscopes - 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:
- Go to Project → Settings → CI/CD → Runners
- Click New project runner
- 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_keyson VPS. - Docker Permission Denied: Ensure the user is added to
dockergroup 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