Cubed Docs
Deployment

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_IP

Update, and install the basics

apt update && apt upgrade -y
apt install -y curl git unzip nginx

Install Node.js 24

curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt install -y nodejs
node -v

node -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 cubed

Bun 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/cubed

3. Configure and build

Move into the project

cd /var/www/cubed/app

Create the environment file

cp .env.example .env.local
nano .env.local

Fill it in:

.env.local
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_key

Generate 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.

Build

npm run build

SUPABASE_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 seed

Test it before setting up the service

npm run start

You 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/cubed

4. 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.service

Paste:

/etc/systemd/system/cubed.service
[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.target

Two details worth understanding:

  • -H 127.0.0.1 keeps the app off the public internet directly. Only nginx can reach it, which is what you want.
  • WorkingDirectory is what makes .env.local resolve. Next.js loads it from there on start, so there is no separate EnvironmentFile to keep in sync.

Start and enable it

systemctl daemon-reload
systemctl enable --now cubed
systemctl status cubed

active (running) means you are in business. If not:

journalctl -u cubed -n 50 --no-pager

5. Put nginx in front

Create the site config

nano /etc/nginx/sites-available/cubed

Paste, replacing YOURDOMAIN.COM in both places:

/etc/nginx/sites-available/cubed
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 nginx

nginx -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-run

Certbot 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 status

Port 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.

Everyday operations

On this page