This guide covers a self-managed Linux server in a home network or lab.
Recommended architecture:
Internet
-> Router / Firewall
-> Reverse proxy (Nginx, Caddy or Apache)
-> Portfolio Node.js application
-> PostgreSQL
Do not expose PostgreSQL directly to the public internet.
Use a current LTS Linux distribution such as Ubuntu Server or Debian.
sudo apt update
sudo apt install -y git curl build-essentialInstall Node.js 22 and verify:
node --version
npm --versionInstall PostgreSQL locally or use a separate trusted database server.
sudo apt install -y postgresql postgresql-contribCreate a dedicated database and application user. Do not run the application as the PostgreSQL superuser.
Prefer a dedicated non-root Linux account:
sudo adduser portfolio
sudo -iu portfolioClone and install:
git clone https://github.com/drnecrotix/Portfolio.git
cd Portfolio
npm ciStore production variables in a protected environment file or process-manager configuration that is not committed to Git.
NODE_ENV=production
NEXT_PUBLIC_SITE_URL=https://portfolio.example.com
DATABASE_URL=postgresql://portfolio:PASSWORD@127.0.0.1:5432/portfolio?schema=public
AUTH_SECRET=your-long-random-secretOn a dedicated server you usually do not need the very small shared-hosting Prisma pool used on N0C. Tune database limits according to actual server capacity.
npx prisma generate
npx prisma validate
npx prisma migrate deploynpm run production:preflight
npm run buildUse the standard build on a normal Linux server. build:n0c is intended for restricted hosts where native SWC cannot run reliably.
Example service:
[Unit]
Description=Portfolio Next.js application
After=network.target postgresql.service
[Service]
Type=simple
User=portfolio
WorkingDirectory=/home/portfolio/Portfolio
Environment=NODE_ENV=production
EnvironmentFile=/home/portfolio/Portfolio/.env.production.local
ExecStart=/usr/bin/npm start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetAdjust paths to the real Node/npm installation.
Then:
sudo systemctl daemon-reload
sudo systemctl enable --now portfolio
sudo systemctl status portfolionpm install -g pm2
pm2 start npm --name portfolio -- start
pm2 save
pm2 startupUse one process-management strategy deliberately. Do not run systemd, PM2 and another supervisor around the same process unless you understand the consequences.
The application can listen on localhost, for example port 3000, while Nginx serves the public HTTPS endpoint.
server {
listen 80;
server_name portfolio.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}Use Certbot, Caddy or another trusted mechanism for TLS.
For public hosting from home:
- forward only HTTP/HTTPS ports to the reverse proxy;
- never forward PostgreSQL 5432 publicly;
- prefer SSH keys;
- restrict SSH exposure where possible;
- enable a host firewall;
- keep the OS and dependencies patched.
Example UFW baseline:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableIf the ISP changes your IP, use dynamic DNS or an API-driven DNS update.
If the connection uses CGNAT, normal inbound port forwarding may not work. Possible approaches include:
- request a public/static IP from the ISP;
- use Cloudflare Tunnel or a comparable reverse tunnel;
- host the reverse proxy on a VPS and tunnel back home.
Back up at least:
- PostgreSQL database;
- uploaded media/local persistent storage;
- environment configuration/secrets in a secure backup;
- repository/custom deployment files not already in Git.
Test restoration, not only backup creation.
git checkout main
git pull --ff-only
npm ci
npx prisma generate
npx prisma migrate deploy
npm run production:preflight
npm run build
sudo systemctl restart portfolioIf PM2 is used, restart the PM2 process instead.
Check router port forwarding, host firewall, ISP CGNAT, DNS, reverse-proxy host configuration, TLS and whether Node is listening.
sudo systemctl status portfolio
curl http://127.0.0.1:3000A 502 usually means the proxy cannot reach the Node process.
sudo systemctl status postgresql
npx prisma migrate statusConfirm that the application uses the correct local/private PostgreSQL host.
Add sufficient RAM/swap or use a controlled build-artifact workflow. Do not hide real OOM problems with repeated restarts.