Prerequisites
- VPS server (Ubuntu/Debian) (Preferred 6gb memory)
- Domain name (optional)
- GitHub repository with Next.js project
- Basic terminal knowledge
Goals
- Auto deployment with Github Actions (single build)
- Custom domain with SSL/HTTPS encryption
Steps
1. Connect into your SSH server with root user
ssh root@<your_vps_ip_address>
ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions -N ""
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
# you need to copy the content of this file for next step
cat ~/.ssh/github_actions
2. Install packages, docker and project directory
Update system and install packages
apt update && apt upgrade -y
apt install -y curl wget git ufw
Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
Install Docker Compose
curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Setup firewall
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Create project directory and clone repository
mkdir -p /var/www/<your_project_dir_name>
cd /var/www/<your_project_dir_name>
# Clone your repository (replace with your actual repository URL)
git clone https://github.com/yourusername/your-repo.git .
Note: If you have a private repository, you'll need to set up SSH keys:
# Generate SSH key for GitHub access (as root)
ssh-keygen -t rsa -b 4096 -C "your-email@example.com" -f /root/.ssh/github_repo
# Display public key to add to GitHub
cat /root/.ssh/github_repo.pub
# Add to SSH config
echo "Host github.com
HostName github.com
User git
IdentityFile /root/.ssh/github_repo" >> /root/.ssh/config
# Test connection
ssh -T git@github.com
3. Setup Docker, Nginx, Github Actions in Next.js Project
Dockerfile
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
docker-compose.yml
version: '3.8'
services:
nextjs-app:
build: .
container_name: <container_name>
restart: unless-stopped
ports:
- '3000:3000'
environment:
- NODE_ENV=production
volumes:
- ./logs:/app/logs
networks:
- app-network
nginx:
image: nginx:alpine
container_name: nginx-proxy
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot:ro
depends_on:
- nextjs-app
networks:
- app-network
networks:
app-network:
driver: bridge
nginx.conf
events {
worker_connections 1024;
}
http {
upstream nextjs {
server nextjs-app:3000;
}
server {
listen 80;
server_name <your_domain_name>.com www.<your_domain_name>.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name <your_domain_name>.com www.<your_domain_name>.com;
ssl_certificate /etc/letsencrypt/live/<your_domain_name>.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<your_domain_name>.com/privkey.pem;
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
}
.github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
script: |
cd /var/www/<your_project_dir_name>
git pull origin master
docker-compose down
docker-compose build --no-cache
docker-compose up -d
docker system prune -f
Push into git
git add .
git commit -m "chore: setup deployment"
git push origin master
4. Github Setup for Secrets
Go to GitHub Repository → Settings → Secrets and variables → Actions
Add these secrets:
-
HOST
: Your VPS IP address -
USERNAME
: root -
PRIVATE_KEY
: Content from ~/.ssh/github_actions file on previous step
5. Domain and SSL Setup
Setup DNS Records
In your domain registrar, point your domain to your VPS IP:
Type: A Record
Name: @ (or leave blank)
Value: YOUR_VPS_IP
Type: A Record
Name: www
Value: YOUR_VPS_IP
Important: Wait for DNS propagation (5-30 minutes) before proceeding.
Phase 1: Initial HTTP-only Nginx Configuration
First, create a temporary HTTP-only nginx config to obtain SSL certificates:
# Create temporary HTTP-only config
sudo nano /var/www/<your_project_dir_name>/nginx-temp.conf
Add this temporary configuration:
events {
worker_connections 1024;
}
http {
upstream nextjs {
server nextjs-app:3000;
}
server {
listen 80;
server_name <your_domain_name>.com www.<your_domain_name>.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
}
Update docker-compose.yml for temporary config
# Update the nginx volumes in docker-compose.yml temporarily
# Change this line:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# To:
# - ./nginx-temp.conf:/etc/nginx/nginx.conf:ro
Start services with HTTP-only config
cd /var/www/<your_project_dir_name>
docker-compose up -d
Install Certbot and obtain SSL certificates
# Install certbot
apt install certbot python3-certbot-nginx -y
# Create certbot directory
mkdir -p /var/www/certbot
# Stop nginx container temporarily
docker-compose stop nginx
# Get SSL certificates using standalone mode
certbot certonly --standalone \
--preferred-challenges http \
-d <your_domain_name>.com \
-d www.<your_domain_name>.com
# Restart nginx container
docker-compose start nginx
Phase 2: Update to HTTPS Configuration
Now update your nginx.conf
file with the HTTPS configuration:
events {
worker_connections 1024;
}
http {
upstream nextjs {
server nextjs-app:3000;
}
server {
listen 80;
server_name <your_domain_name>.com www.<your_domain_name>.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name <your_domain_name>.com www.<your_domain_name>.com;
ssl_certificate /etc/letsencrypt/live/<your_domain_name>.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<your_domain_name>.com/privkey.pem;
# SSL security settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
}
Update docker-compose.yml back to HTTPS config
# Revert the nginx volumes in docker-compose.yml
# Change back to:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
Final deployment with HTTPS
# Pull latest changes
git pull origin master
# Restart with HTTPS configuration
docker-compose down
docker-compose build --no-cache
docker-compose up -d
# Clean up
rm nginx-temp.conf
docker system prune -f
Setup automatic SSL renewal
# Add cron job for automatic renewal (as root)
echo "0 12 * * * /usr/bin/certbot renew --quiet && docker-compose restart nginx" | crontab -
Test your deployment
# Test HTTP redirect
curl -I http://<your_domain_name>.com
# Test HTTPS
curl -I https://<your_domain_name>.com
# Check SSL certificate
openssl s_client -connect <your_domain_name>.com:443 -servername <your_domain_name>.com
6. Troubleshooting Common Issues
Check container status
# View running containers
docker ps
# Check container logs
docker logs <container_name>
docker-compose logs -f
Common fixes
# If containers won't start
docker-compose down
docker system prune -f
docker-compose up -d
# If SSL issues persist
sudo certbot certificates
sudo certbot renew --dry-run
# Check nginx configuration
docker exec nginx-proxy nginx -t
# Restart specific services
docker-compose restart nginx
docker-compose restart nextjs-app
Verify deployment
- ✅ HTTP redirects to HTTPS:
curl -I http://yourdomain.com
- ✅ HTTPS works:
curl -I https://yourdomain.com
- ✅ Containers running:
docker ps
- ✅ SSL certificate valid: Check browser or use SSL checker online
Achieved:
- Auto deployment with Github Actions (single build) ✅ (~5 mins process)
- Custom domain with SSL/HTTPS encryption ✅
- Secure server setup with firewall ✅
- Automatic SSL certificate renewal ✅
Top comments (0)