Guide

Add a Subdomain

Step-by-step checklist for deploying a new Node.js service as a subdomain.rokojori.com on Server A. Fill in the variables below — all commands update automatically.

Variables

Fill these in once and every command in the guide updates.

0 — Ensure compatibility

When do you need CORS?

If any other rokojori site loads resources from this service (fonts, CSS, JSON, images) via a browser request — @import, fetch, <img src> etc. — the browser enforces the same-origin policy and blocks the response unless the server sends an Access-Control-Allow-Origin header. Services that only serve their own pages and API calls to their own frontend do not need CORS.

Do not use *

Access-Control-Allow-Origin: * allows any website on the internet to load your resources. For public CDN assets that is acceptable, but for rokojori services restrict it to known origins.

Because the header cannot carry a wildcard pattern like *.rokojori.com, the server must check the incoming Origin header against an allowlist and echo back the matched origin. Unrecognised origins receive no header and the browser blocks them.

Pattern — allowlist middleware

Add this to source/server/index.ts before the routes that need it:

const CORS_ALLOWED: Array<string | RegExp> = [
  /^https?:\/\/([\w-]+\.)?rokojori\.com$/,  // all *.rokojori.com subdomains
  // 'https://someother.site',               // add extra origins here
];

function allowCors(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
): void
{
  const origin = req.headers.origin;
  if ( origin && CORS_ALLOWED.some( rule =>
    typeof rule === 'string' ? rule === origin : rule.test( origin )
  ) )
  {
    res.set( 'Access-Control-Allow-Origin', origin );
  }
  next();
}

// Apply before the routes that serve cross-origin resources:
app.use( '/your-public-route', allowCors );

What the regex matches

/^https?:\/\/([\w-]+\.)?rokojori\.com$/ accepts: https://rokojori.com, https://roject.rokojori.com, https://styles.rokojori.com, and so on. HTTP is included so local dev with a mapped host still works. Extend CORS_ALLOWED with additional strings or regexes for any external sites that need access.

1 — Create the repository

New repo on Gitea

Log in to community.rokojori.com, click New Repository, name it (e.g. subdomain), set it to private, and copy the clone URL. Do not initialise with a README — the local project is already set up.

2 — .gitignore, commit, push

.gitignore

Create this at the project root before the first commit:

node_modules/
dist/
build/
.env
storage/

Initial commit and push

git init
git add .
git commit -m "init"
git remote add origin https://community.rokojori.com/Rokojori/subdomain.git
git push -u origin main

3 — DNS record on IONOS

Add A record

Log in to IONOS → Domains & SSL → rokojori.com → DNS. Add a new record:

Type:  A
Name:  subdomain
Value: <Server A IP>
TTL:   300

Wait a few minutes for propagation before running certbot. Verify with ping subdomain.rokojori.com from any machine.

4 — Clone on the server

SSH and clone

ssh root@<Server A IP>

cd /opt
git clone https://community.rokojori.com/Rokojori/subdomain.git subdomain
cd subdomain

5 — Install, build, configure

Install dependencies

npm install

Copy pages (if the project has source/pages/)

node scripts/copy-pages.js

Create .env

Copy the contents from your local .env and paste them on the server. Update any values that differ in production (port, secrets, URLs).

nano .env

Create storage directories (if needed)

# example for styles.rokojori.com
mkdir -p storage/fonts

Quick smoke test

Run the server once manually to confirm it starts. Kill it with Ctrl+C before moving to the systemd step — leave it running and the port will conflict.

npm run dev
# → service running on http://localhost:PORT
# Ctrl+C to stop

6 — nginx config (HTTP only)

Create the site config

nano /etc/nginx/sites-available/subdomain.rokojori.com
server {
    listen 80;
    server_name subdomain.rokojori.com;

    location / {
        proxy_pass         http://localhost:PORT;
        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;
    }
}

Enable and reload

ln -s /etc/nginx/sites-available/subdomain.rokojori.com \
         /etc/nginx/sites-enabled/subdomain.rokojori.com

nginx -t
systemctl reload nginx

7 — Let's Encrypt certificate

Run certbot

The --nginx plugin handles the ACME HTTP-01 challenge through nginx directly — the app does not need to be running. When prompted whether to redirect HTTP to HTTPS, choose Redirect (option 2).

certbot --nginx -d subdomain.rokojori.com

Certbot modifies the nginx config in-place, adding TLS directives and an HTTP→HTTPS redirect block. It also installs a systemd timer for automatic renewal — no extra setup needed.

8 — Verify nginx HTTPS config

Check and reload

Certbot updates the config automatically, but always verify before reloading:

nginx -t
systemctl reload nginx

Confirm TLS is live

curl -I https://subdomain.rokojori.com

Expect HTTP/2 200 (or 502 if the app is not yet running — that is fine at this stage).

9 — systemd service

Create the unit file

nano /etc/systemd/system/service-name.service
[Unit]
Description=service-name
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/subdomain
ExecStart=/usr/bin/npx ts-node --project tsconfig.ts-node.json source/server/index.ts
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

The .env file is loaded by import 'dotenv/config' inside the app, so no EnvironmentFile= is needed.

Enable and start

systemctl daemon-reload
systemctl enable service-name
systemctl start service-name
systemctl status service-name

Check logs

journalctl -u service-name -f

Quick reference

Deploy an update

cd /opt/subdomain
git pull
npm install
node scripts/copy-pages.js   # if applicable
systemctl restart service-name

Common commands

systemctl restart service-name        # restart app
systemctl status  service-name        # current state
journalctl -u service-name -n 100    # last 100 log lines
nginx -t                              # validate nginx config
systemctl reload nginx                # apply nginx changes

Certificate renewal

Certbot installs a systemd timer (certbot.timer) that renews certificates automatically before expiry. Check its status with:

systemctl status certbot.timer