Deploying a Secure Next.js App on a Linux VPS with Nginx & Docker
Letβs go beyond development and start thinking like a DevOps engineer.
The goal of this article is to provide a simple and fast approach to setting up a secure Linux VPS to host a Next.js application using Nginx and Docker. For this guide, weβll use AlmaLinux 9 on a basic Namecheap VPS.
π§ Prerequisites
- MacBook or Windows PC (this guide uses macOS)
- An existing project (Next.js recommended)
- Domain with DNS managed via Cloudflare
π₯οΈ Install AlmaLinux 9 on VPS
Install AlmaLinux 9 using your hosting providerβs control panel. For this article, a Namecheap Basic VPS is used.
Once installation is complete, open a terminal on your local machine.
π Generate SSH Key (Local PC)
ssh-keygen -t ed25519 -C "VPS-server"
When prompted, provide a name for the key:
/Users/YOUR_USER_NAME/.ssh/vps-server
Display the public key:
cat /Users/YOUR_USER_NAME/.ssh/vps-server.pub
π Login to VPS (First Time)
ssh root@YOUR_SERVER_IP
After a successful login, you should see the server terminal.
βοΈ Install Nano Editor
sudo dnf install nano -y
π Configure SSH Key Authentication (VPS)
mkdir -p ~/.ssh
nano ~/.ssh/authorized_keys
Paste the public key from your local machine.
Set correct permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
From now on, you can log in without a password:
ssh -i /Users/YOUR_USER_NAME/.ssh/vps-server root@SERVER_IP
π₯ Firewall Configuration (firewalld)
sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
Allow essential services (DO NOT skip SSH):
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
β οΈ Warning: If SSH is not allowed, you will be locked out.
π‘οΈ Install Fail2Ban (Brute-force Protection)
While Cloudflare protects against DDoS attacks, server-level services such as SSH still require protection.
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit configuration:
sudo nano /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 3600
[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = true
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status
π Enable Automatic Security Updates
sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
sudo systemctl status dnf-automatic.timer
π Set timezone
timedatectl list-timezones
sudo timedatectl set-timezone Asia/Colombo
Verify: timedatectl
π Install Nginx
sudo dnf update -y
sudo dnf install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
π¦ Create Nginx Server Block
sudo nano /etc/nginx/conf.d/abc.com.conf
server {
listen 443 ssl;
server_name abc.com www.abc.com;
ssl_certificate /home/piyal/ssl/abc.crt;
ssl_certificate_key /home/piyal/ssl/abc.key;
location / {
proxy_pass http://127.0.0.1:3023;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name abc.com www.abc.com;
return 301 https://$host$request_uri;
}
π Install Cloudflare Origin SSL
Cloudflare β SSL/TLS β Origin Server β Create Certificate
nano /home/piyal/ssl/abc.crt
nano /home/piyal/ssl/abc.key
sudo nginx -t
sudo systemctl restart nginx
π³ Install Docker on VPS
sudo dnf install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
docker --version
π§± Dockerize Next.js App (Local Machine)
Create the Dockerfile on your project
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["server.js"]
ποΈ Build & Transfer Image
docker build --platform linux/amd64 -t abc-com-app-amd64 .
docker save abc-com-app-amd64 | gzip > abc-com-app-amd64.tar.gz
scp abc-com-app-amd64.tar.gz root@SERVER_IP:/root/
βΆοΈ Run Container on VPS
docker load < abc-com-app-amd64.tar.gz
docker run -d \
--name navora \
--restart unless-stopped \
-p 3023:3000 \
--read-only \
--tmpfs /tmp:noexec \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--memory 1024m \
--memory-swap 1024m \
--cpus="1.0" \
navora-app-amd64
docker ps
β Final Result
π Your Next.js application is now live:
π https://abc.com