Guide on simpliest ever self-hosted next.js on VPS configuration. Designed to run on a really weak hardware.
- Auto HTTPS & certificates renewal with
caddy - Production-ready aplication run process management with
pm2 - Go live in
5 minutes
Try live: https://digitalandy.eu
My example is running on debian, free tier by vultr. Requirements are:
Minimal: 512 MB RAM, 10 GB SSD, 1 CPU, 0.5TB Bandwish / mo
Recommended: 1 GB RAM, 10 GB SSD, 1 CPU, 1TB Bandwish / mo
If you can afford a perfomant VPS, check EasyPanel setup instead
Free Debian | 512 MB | 10 GB SSD | 2 TB Bandwish VPS early access program by vultr.com
- Register via my refferal
- Apply for Vultr Free Tier Program in your account
- Wait for program activation (regullary takes 1-2 days only)
- Deploy new server
Follow new instance configuration example:
Type: Cloud Compute - Shared CPU
Location: Miami, Seattle, or Frankfurt
Image: Debian 12 x64
Plan: Regular Cloud Compute
SSH Key: upload your public key (cat ~/.ssh/id_rsa.pub)
Hostname example: s.example.com
It's recommended to use subdomain as hostname, like
vps.example.comorserver.example.com)
- Enjoy this VPS for free for
1 yearperiod. Recommendations:
- Bandwish usage notifications: turn on to don't be charged if you bypass the free limits qouta
Point your domain to a new vps IP by creating new records:
Wildcard example:
- A @.example.com 11.11.11.11
- A *.example.com 11.11.11.11
- CNAME www.example.com example.com
Hostname / doman example:
- A s.example.com 11.11.11.11
- A @.example.com 11.11.11.11
- CNAME www.example.com example.com
You will be able to use a hostname, e.g s.example.com instead of server's ip after change
DNS changes typically applyed in 30 minutes - 1 hour
Test the hostname record by sending ping from your machine:
ping s.example.comThe configuration goes under the root user.
SSH with ssh [email protected]
SFTP with Transmit app guide
For experienced users. Use the below screenshots to configure required parts by yourself
- configure
git - configure
ufw - install
nvmandnode 18 - install
pm2vianpmas global dependency - install
caddyserver - optionally, install
gh,postgres,pnpm,bun
If you’re on Debian, the steps to resolve the memory issue (which is likely causing the Killed message during pnpm install) are still applicable. Here’s a Debian-specific guide to fix it:
Debian might not have sufficient swap space, and increasing it can help avoid memory-related crashes.
Check if Swap Space is Enabled:
First, check if you have swap space configured:
swapon --showIf you don’t see any output or if your swap is very small, create or increase the swap size:
sudo fallocate -l 2G /swapfile
# If fallocate is not available, use dd:
sudo dd if=/dev/zero of=/swapfile bs=1M count=4096
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make the swap permanent (so it persists across reboots)
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabUser .bashrc and .profile files:
env.sh (exporting the GH_TOKEN var):
I use dedicated example_bot GH user with ssh keys from server added
Optionaly, set up gh cli, loggined via GIT_TOKEN + https helpers in .gitconfig
User gitconfig and gitignore:
Allow access from admin's IP adresses with ufw allow from 193.0.0.0
The node is setted up via nvm manager, see the nvm ls
Packages installed globally via npm:
# Setup the startup script
pm2 startup
# Link to pm2 account
pm2 link xxxx xxxx
# Status / monitor
pm2 status
pm2 monitorpm2 status:
pm2 new npm script
# /root/example.com
APP_ROOT=${1:-$PWD}
cd "$APP_ROOT" || exit
APP_NAME="$(jq -r .name package.json)"
echo "PATH: $APP_ROOT, NAME: $APP_NAME"
pm2 start npm --name "$APP_NAME" -- start
pm2 savepm2 new file
# /root/api.example.com
APP_ROOT=${1:-$PWD}
cd "$APP_ROOT" || exit
APP_FILE="$(jq -r .main package.json)"
APP_NAME="$(jq -r .name package.json)"
echo "PATH: $APP_ROOT, FILE: $APP_FILE, NAME: $APP_NAME"
pm2 start --name "$APP_NAME" "./$APP_FILE"
pm2 saveDon't foget to chmod +x /root/**/*.sh
Caddy running as daemon: https://caddyserver.com/docs/running#unit-files
⚠️ Caddy is running as a daemon, don't caddy stop | start!
Caddy help caddy --help or https://caddyserver.com/docs/caddyfile-tutorial
Configure server by editing file /etc/caddy/Caddyfile
# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace ":80" below with your
# domain name.
(logging) {
log {
output file /var/log/caddy/caddy.log {
roll_size 1mb
}
format json
}
}
(optimization) {
# Compress everything else that would benefit
encode zstd gzip
}
www.example.com {
redir https://example.com{uri}
}
api.example.com {
import logging optimization
reverse_proxy localhost:8080
}
example.com {
import logging optimization
reverse_proxy localhost:3000
}
:80, :443 {
redir https://example.com permanent
# Set this path to your site's directory.
# root * /usr/share/caddy
# Enable the static file server.
# file_servers
# encode gzip
# Another common task is to set up a reverse proxy:
# reverse_proxy localhost:8080
# Or serve a PHP site through php-fpm:
# php_fastcgi localhost:9000
}
#example.com:80, api.example.com:80 {
# redir https://{host}{uri}
#}
# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfileLogs are stored at /var/log/caddy default location
Logs cheatsheet https://caddy.community/t/making-caddy-logs-more-readable/7565/13
CheatSheet:
# LOGS
# view pretty log
tail -f /var/log/caddy.log | jq
# view all logs
bat /var/log/caddy/*.log* --paging=never --plain
# CONFIG
cd /etc/caddy/
caddy validate
caddy fmt --overwrite
# SERVICE
systemctl status caddy
# restart service
sudo systemctl restart caddy
# gracefully reload Caddy after making any changes:
sudo systemctl reload caddyMy current SSH connection with custom welcome message example:










