VPS + nginx
Your own Linux server, with a domain, HTTPS, a system service and a reverse proxy.
The most control, and the most steps. A few dollars a month.
Assumed: Ubuntu 22.04 or 24.04, a fresh server, a domain pointed at its IP
address, and root or sudo access.
Before you start, point your domain's A record at the server's IP. DNS can take up to an hour to spread, and the HTTPS step at the end will fail without it.
1. Prepare the server
Connect
ssh root@YOUR_SERVER_IPUpdate, and install the basics
apt update && apt upgrade -y
apt install -y curl git unzip nginxInstall Node.js 24
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt install -y nodejs
node -vnode -v must print v24. or higher.
Create a user for the site
Running a web app as root is a bad habit:
adduser --system --group --home /var/www/cubed cubedBun is only needed if you seed from this server. If you already ran
bun run seed on your own computer, skip it — the server never touches the database
schema.
2. Get the code onto the server
mkdir -p /var/www
git clone YOUR_REPO_URL /var/www/cubed/app
chown -R cubed:cubed /var/www/cubed3. Configure and build
Move into the project
cd /var/www/cubed/appCreate the environment file
cp .env.example .env.local
nano .env.localFill it in:
TEBEX_TOKEN=your_headless_store_token
SERVER_SECRET=your_game_server_secret
NEXT_PUBLIC_SITE_URL=https://YOURDOMAIN.COM
PAYLOAD_SECRET=paste_64_random_hex_characters
DATABASE_URI=postgresql://postgres.yourref:yourpassword@aws-0-region.pooler.supabase.com:5432/postgres
SUPABASE_URL=https://yourref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_keyGenerate the secret with openssl rand -hex 32. Save with Ctrl+O, Enter, then
Ctrl+X.
NEXT_PUBLIC_SITE_URL must be https:// and must match the domain visitors will
type. Getting this wrong makes "add to basket" fail with a 403 once you are live.
Install dependencies
npm installBuild
npm run buildSUPABASE_URL has to be in .env.local before this command. The build bakes
your Supabase hostname into the image loader and the Content-Security-Policy — miss
it and your uploaded logo is blocked in the browser.
Takes a few minutes and wants around 2 GB of RAM. If it is killed partway through, see the low-memory box at the bottom of this page.
Seed — only if you have not already
Skip this if you seeded from another machine. It is a one-off against Supabase, not per-server.
curl -fsSL https://bun.sh/install | bash
~/.bun/bin/bun run seedTest it before setting up the service
npm run startYou should see Ready in …. Visit http://YOUR_SERVER_IP:3000 to confirm, then
press Ctrl+C.
You will see "next start" does not work with "output: standalone" configuration.
Ignore it — a suggestion, not an error. Running this way keeps the working
directory at the project root, which is where .env.local lives.
Then fix ownership, since you built as root:
chown -R cubed:cubed /var/www/cubed4. Run it as a service
Keeps the site running after you log out, and restarts it on crash or reboot.
Create the service file
nano /etc/systemd/system/cubed.servicePaste:
[Unit]
Description=Cubed Tebex store
After=network.target
[Service]
Type=simple
User=cubed
Group=cubed
WorkingDirectory=/var/www/cubed/app
Environment=NODE_ENV=production
ExecStart=/usr/bin/node node_modules/next/dist/bin/next start -p 3000 -H 127.0.0.1
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetTwo details worth understanding:
-H 127.0.0.1keeps the app off the public internet directly. Only nginx can reach it, which is what you want.WorkingDirectoryis what makes.env.localresolve. Next.js loads it from there on start, so there is no separateEnvironmentFileto keep in sync.
Start and enable it
systemctl daemon-reload
systemctl enable --now cubed
systemctl status cubedactive (running) means you are in business. If not:
journalctl -u cubed -n 50 --no-pager5. Put nginx in front
Create the site config
nano /etc/nginx/sites-available/cubedPaste, replacing YOURDOMAIN.COM in both places:
server {
listen 80;
listen [::]:80;
server_name YOURDOMAIN.COM www.YOURDOMAIN.COM;
# Admin panel image uploads need more than nginx's 1 MB default.
client_max_body_size 25M;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}Start with plain port 80 — certbot needs it reachable to validate your domain in the next step.
Enable it
ln -s /etc/nginx/sites-available/cubed /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginxnginx -t must say syntax is ok and test is successful before you reload.
Check it over plain HTTP
Visit http://YOURDOMAIN.COM. The store should appear. HTTPS comes next.
6. Add HTTPS
apt install -y certbot python3-certbot-nginx
certbot --nginx -d YOURDOMAIN.COM -d www.YOURDOMAIN.COM --redirect
certbot renew --dry-runCertbot rewrites your nginx config to serve SSL on 443, redirects 80 → 443, and sets
up automatic renewal. The --dry-run confirms renewal works.
7. Lock down the firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
ufw statusPort 3000 stays closed to the outside world — nginx reaches it over localhost.
8. Finish up
Log in to the admin panel
https://YOURDOMAIN.COM/admin, with your seeded credentials.
Change the admin password
Admin → Users → your user → set a new password. Do this now if you used the example password.
Remove the development origins
Open next.config.ts and delete the allowedDevOrigins line — it contains IP
addresses from the theme author's development machine and does nothing for you.
See Security and headers.