Guide
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.
Fill these in once and every command in the guide updates.
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.
*
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.
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 );
/^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.
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.
Create this at the project root before the first commit:
node_modules/
dist/
build/
.env
storage/
git init
git add .
git commit -m "init"
git remote add origin https://community.rokojori.com/Rokojori/subdomain.git
git push -u origin main
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.
ssh root@<Server A IP>
cd /opt
git clone https://community.rokojori.com/Rokojori/subdomain.git subdomain
cd subdomain
npm install
node scripts/copy-pages.js
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
# example for styles.rokojori.com
mkdir -p storage/fonts
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
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;
}
}
ln -s /etc/nginx/sites-available/subdomain.rokojori.com \
/etc/nginx/sites-enabled/subdomain.rokojori.com
nginx -t
systemctl reload nginx
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.
Certbot updates the config automatically, but always verify before reloading:
nginx -t
systemctl reload nginx
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).
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.
systemctl daemon-reload
systemctl enable service-name
systemctl start service-name
systemctl status service-name
journalctl -u service-name -f
cd /opt/subdomain
git pull
npm install
node scripts/copy-pages.js # if applicable
systemctl restart service-name
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
Certbot installs a systemd timer (certbot.timer) that
renews certificates automatically before expiry. Check its status with:
systemctl status certbot.timer