1What a deploy puts on the host
games.divi.domains is two Rust programs and a reverse proxy. divi-gamechain is the sidechain node from Run the gamechain; divi-games-site is the website, forum and courses, and it is also the certificate issuer. nginx sits in front and sends /chain/ to the node and everything else to the site. deploy/deploy.sh in the divi-games repository does the whole install, and this page runs it on a plain Ubuntu 22.04 box.
The repo's nginx file literally names games.divi.domains, and the script restarts whatever services it finds. Run it on a machine you own and can break. Do not run it on the production host, and do not point the production name at your box: there is one public gamechain and one sequencer, and your deploy is a new chain, not a copy of it (step 4 shows the genesis being made).
What the script does, in order, is readable at the top of this page's source: pull the branch, build both binaries as the user who ran sudo, copy them to /usr/local/bin, copy the four web trees to /usr/local/share/divi-games, install the two config files only if they are missing, install the two units and the nginx site, enable and restart the services, and refuse to say ok until both are active. It never reads a secret; it only checks that the two env files exist with mode 0600.
2Get the source onto the host
The script builds from a git checkout on the host itself, and it runs git fetch origin there, so the host needs an origin it can reach. DiviDomains/divi-games on GitHub is still private on 2026-10-21, so we used the same route the production host uses (ARCHITECTURE.md §12): a bare mirror on the host that the workstation pushes to. Nothing on the host needs a GitHub key. We kept everything under ~/tut-i on a host called hp3; use your own names.
# on the workstation, which already has the repo
ssh hp3 'git init -q --bare ~/tut-i/divi_games.git && echo bare-ok'
git -C ~/code/divi_games push -q hp3:tut-i/divi_games.git main:main
ssh hp3 'git clone -q ~/tut-i/divi_games.git ~/tut-i/divi_games && git -C ~/tut-i/divi_games log --oneline -1'
bare-ok 957e13c deploy: re-exec after pulling so a changed deploy.sh runs whole
A later release is just another git push to the mirror followed by another deploy. The checkout's origin is the mirror, which is what the script fetches:
$ git -C ~/tut-i/divi_games remote -v origin /home/bshuler/tut-i/divi_games.git (fetch) origin /home/bshuler/tut-i/divi_games.git (push)
The build happens on the host, so the host needs Rust. Ours had cargo 1.99.0 and rustc 1.99.0 installed for the user, not root; that matters in step 4.
3nginx and the two secret files
Two things the script will not do for you. It installs an nginx site only if /etc/nginx/sites-available exists, and it stops with an error if either env file is missing. The env files hold the secrets: TELEGRAM_BOT_TOKEN for the site (login, invites and notifications), and optional DG_CONFIG, DG_DATA_DIR or RUST_LOG for the node. They are created by you, as root, mode 0600, and the script only checks that mode.
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q nginx sudo install -d -m 0755 /etc/divi-games sudo install -m 0600 -o root -g root /dev/null /etc/divi-games/gamechain.env sudo install -m 0600 -o root -g root /dev/null /etc/divi-games/site.env ls -l /etc/divi-games
-rw------- 1 root root 0 Oct 21 19:57 gamechain.env -rw------- 1 root root 0 Oct 21 19:57 site.env
We left both files empty on purpose: a gamechain needs nothing, and a site without a bot token still serves every page and game, it just logs a warning and turns Telegram login off (you will see that line in the journal). When you do have a token, put TELEGRAM_BOT_TOKEN=… in site.env with an editor as root and restart divi-games-site. Never put it in the unit file, the config file or the repo.
/etc/divi-games/*.env (0600, root) hold secrets. /etc/divi-games/*.toml (0644) hold settings and no secrets; they are installed once and then belong to you. /var/lib/divi-games/{chain,site} (0700, divigames) hold the databases and the keys the programs make for themselves. That split is why the script can be run by anyone with sudo without ever seeing a secret.
4Run deploy.sh
From the checkout, as root via sudo. Under sudo the script fetches and builds as the invoking user ($SUDO_USER), with that user's ~/.cargo/bin on the path, so root never writes into your checkout or your toolchain. We sent the output to a log because the first build compiles the whole dependency tree:
cd ~/tut-i/divi_games && sudo ./deploy/deploy.sh > ~/tut-i/deploy.log 2>&1; echo rc=$? tail -8 ~/tut-i/deploy.log
rc=0
Compiling divi-games-site v0.1.0 (/home/bshuler/tut-i/divi_games/bins/divi-games-site)
Compiling divi-gamechain v0.1.0 (/home/bshuler/tut-i/divi_games/bins/divi-gamechain)
Finished `release` profile [optimized] target(s) in 1m 39s
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Created symlink /etc/systemd/system/multi-user.target.wants/divi-gamechain.service → /etc/systemd/system/divi-gamechain.service.
Created symlink /etc/systemd/system/multi-user.target.wants/divi-games-site.service → /etc/systemd/system/divi-games-site.service.
deploy: ok (957e13c)
The last line names the commit that was built. Both services are up, both on loopback only, with nginx on 80:
$ systemctl is-active divi-gamechain divi-games-site nginx active active active $ sudo ss -ltn | grep -E ':(7410|7411|80) ' LISTEN 0 128 127.0.0.1:7410 0.0.0.0:* LISTEN 0 128 127.0.0.1:7411 0.0.0.0:* LISTEN 0 511 0.0.0.0:80 0.0.0.0:* LISTEN 0 511 [::]:80 [::]:*
The journal shows the node making its genesis. This is a brand-new chain: the house commits its deck, the faucet and the fund actions land in blocks 1 to 3, and the node confirms it can reach Divi testnet for its checkpoints. The site logs the missing-token warning we expected:
$ sudo journalctl -u divi-gamechain -u divi-games-site --since "-2min" --no-pager -o short-iso … divi-gamechain[3270609]: INFO divi_gamechain: serving listen=127.0.0.1:7410 … divi-gamechain[3270609]: INFO divi_gamechain::house: house action submitted kind="house_commit" pending=0 … divi-gamechain[3270609]: INFO divi_gamechain: block height=1 txs=1 … divi-gamechain[3270609]: INFO divi_gamechain::house: house action submitted kind="faucet" pending=0 … divi-gamechain[3270609]: INFO divi_gamechain: Divi checkpoint reachable height=359938 … divi-gamechain[3270609]: INFO divi_gamechain: block height=2 txs=1 … divi-gamechain[3270609]: INFO divi_gamechain::house: house action submitted kind="fund" pending=0 … divi-gamechain[3270609]: INFO divi_gamechain: block height=3 txs=1 … systemd[1]: Started Divi Games website, forum and courses (divi-games-site). … divi-games-site[3270686]: WARN divi_games_site: TELEGRAM_BOT_TOKEN is not set: login, invites and notifications are off … divi-games-site[3270686]: INFO divi_games_site: listening on 127.0.0.1:7411
Certificates on the gamechain are only valid from addresses listed in certificate_issuers, and that list is fixed in the genesis block. So on a first deploy, when /etc/divi-games/gamechain.toml does not exist yet, the script runs divi-games-site issuer-address (which creates the site's issuer.key) and writes the address into a fresh gamechain.toml before starting the node. If a chain.db already exists at that point the script warns that the address will not be an issuer until the chain is recreated.
5What it wrote where
Config, generated on the first run and never overwritten after. The issuer address is the one the site printed; the site file is the repo's deploy/site.toml verbatim:
$ cat /etc/divi-games/gamechain.toml # /etc/divi-games/gamechain.toml: written by deploy.sh on first deploy. certificate_issuers = ["y1R82BrRevwz2V2eCdWA5MKBHUSXFHxr3L"] $ cat /etc/divi-games/site.toml # /etc/divi-games/site.toml: installed by deploy.sh only when missing, so local edits survive. # No secrets here; the bot token is TELEGRAM_BOT_TOKEN in /etc/divi-games/site.env. listen = "127.0.0.1:7411" data_dir = "/var/lib/divi-games/site" site_dir = "/usr/local/share/divi-games/site" web_dir = "/usr/local/share/divi-games/web" assets_dir = "/usr/local/share/divi-games/assets" content_dir = "/usr/local/share/divi-games/content" chain_url = "http://127.0.0.1:7410" base_url = "https://games.divi.domains" # The Telegram bot's username (without @), for the Login Widget. bot_username = "" network = "testnet" # Telegram user ids that may hide forum posts. admins = [] cookie_secure = true post_limit_per_minute = 3
For your own host you will at least change base_url and, when you have a bot, bot_username and admins. cookie_secure = true means the login cookie is only sent over HTTPS, so behind plain HTTP the Telegram login would not stick. State, owned by the service user and closed to everyone else. Note the two keys the programs made for themselves, 0600, and that the script never touched them:
$ sudo ls -la /var/lib/divi-games/chain /var/lib/divi-games/site /var/lib/divi-games/chain: drwx------ 2 divigames divigames 4096 Oct 21 19:59 . -rw------- 1 divigames divigames 4096 Oct 21 19:59 chain.db -rw------- 1 divigames divigames 32768 Oct 22 00:41 chain.db-shm -rw------- 1 divigames divigames 3098272 Oct 22 00:41 chain.db-wal -rw------- 1 divigames divigames 65 Oct 21 19:59 house.secret -rw------- 1 divigames divigames 53 Oct 21 19:59 sequencer.key /var/lib/divi-games/site: drwx------ 2 divigames divigames 4096 Oct 21 19:59 . -rw------- 1 divigames divigames 52 Oct 21 19:59 issuer.key -rw------- 1 divigames divigames 4096 Oct 21 19:59 site.db -rw------- 1 divigames divigames 32768 Oct 21 19:59 site.db-shm -rw------- 1 divigames divigames 173072 Oct 21 19:59 site.db-wal
Binaries and web trees:
$ ls -l /usr/local/bin/divi-gamechain /usr/local/bin/divi-games-site; ls /usr/local/share/divi-games -rwxr-xr-x 1 root root 12164072 Oct 21 19:59 /usr/local/bin/divi-gamechain -rwxr-xr-x 1 root root 16008600 Oct 21 19:59 /usr/local/bin/divi-games-site assets content site web
The unit files are worth reading once. Both run as divigames with ProtectSystem=strict, ProtectHome=true, an empty CapabilityBoundingSet, MemoryDenyWriteExecute, a @system-service syscall filter and UMask=0077; the only writable path is /var/lib/divi-games. The node unit sets DG_CONFIG and DG_DATA_DIR and runs divi-gamechain run; the site unit sets DG_SITE_CONFIG and DG_SITE_DATA_DIR. Both read their env file with EnvironmentFile=, which is the only place a secret enters.
6Prove it through nginx
The nginx site answers to server_name games.divi.domains, and no DNS name points at our box, so every test below sends that name in the Host header. On your host either do the same or change server_name in deploy/nginx-games.divi.domains.conf to your own name before deploying (the script copies the file on every run, so edit it in the repo, not in /etc).
The node, through the /chain/ prefix that nginx strips. The chain id is the constant CHAIN_ID in dg-core, not a setting, so it says divi-games-testnet-1 like the public chain; the sequencer, genesis and history are our own:
$ curl -s -H "Host: games.divi.domains" http://127.0.0.1/chain/v1/info
{"chain_id":"divi-games-testnet-1","divi":{"hash":"47fe9963…0ee9bb2d","height":360221},
"divi_seen":{"hash":"47fe9963…0ee9bb2d","height":360221},
"head_hash":"da2263a69e54c7a8259b8ee47f974c85fc3641570f00f69805d96d98db2ac381","height":281,"mempool":0,
"modules":["backend","blackjack","bridge","certificate","htlc","market","system","tictactoe","towerdefense"],
"network":"testnet","sequencer":"y5LF7Lb2AF6bPipM8uC5Z8peGEbfswB3Ge","time_ms":1792629671348,"tokens":["GOLD","TDIVI"]}
The site, a game page and the SDK the games load, all through the / location. The site sends a strict Content-Security-Policy (the /play/ pages get a looser one, because the games load their own scripts and wasm):
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" -H "Host: games.divi.domains" http://127.0.0.1/
200 text/html; charset=utf-8
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" -H "Host: games.divi.domains" http://127.0.0.1/play/tictactoe/
200 text/html
$ curl -s -D - -o /dev/null -H "Host: games.divi.domains" http://127.0.0.1/ | grep -i "content-security\|server:"
Server: nginx/1.18.0 (Ubuntu)
content-security-policy: default-src 'self'; script-src 'self' https://telegram.org; frame-src https://oauth.telegram.org; img-src 'self' data:; …
$ curl -s -H "Host: games.divi.domains" http://127.0.0.1/sdk/divigames.js | head -c 60
// divigames.js: the Divi Games browser SDK.
The block feed is a WebSocket, and it has its own location = /chain/v1/ws with the Upgrade headers and an hour-long read timeout, because the sequencer heartbeat is 60 s. A hand-made upgrade request gets the switch:
$ curl -s -i --max-time 5 -H "Host: games.divi.domains" -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" http://127.0.0.1/chain/v1/ws | head -4
HTTP/1.1 101 Switching Protocols
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 22 Oct 2026 01:02:17 GMT
Connection: upgrade
From here the SDK works against your host exactly as it does against the public chain: new Chain('http://your-host/chain') in divigames.js, and the games from Build tic-tac-toe onwards play on it. Your chain starts with its own GOLD faucet and fund, made in blocks 1 to 3 above.
The repo's nginx file is HTTP-only. The script runs certbot install --nginx --cert-name games.divi.domains --redirect only if /etc/letsencrypt/live/games.divi.domains already exists, which is how the production host keeps its certificate and its HTTP→HTTPS redirect across deploys. We had no certificate and no public name, so that branch did not run. To add TLS on your own host, point a name at it, run certbot --nginx -d your.name once, and change --cert-name in the script to match.
7Run it again
The header of the script says it is safe to run again. We checked by running it a second time an hour later and watching the chain height across the restart:
$ curl -s -H "Host: games.divi.domains" http://127.0.0.1/chain/v1/info | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["height"], d["sequencer"])'
301 y5LF7Lb2AF6bPipM8uC5Z8peGEbfswB3Ge
$ cd ~/tut-i/divi_games && sudo ./deploy/deploy.sh > ~/tut-i/deploy2.log 2>&1; echo rc=$?; tail -4 ~/tut-i/deploy2.log; grep -c Compiling ~/tut-i/deploy2.log
rc=0
Finished `release` profile [optimized] target(s) in 0.49s
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
deploy: ok (957e13c)
0
$ curl -s -H "Host: games.divi.domains" http://127.0.0.1/chain/v1/info | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["height"], d["sequencer"])'
301 y5LF7Lb2AF6bPipM8uC5Z8peGEbfswB3Ge
$ sudo journalctl -u divi-gamechain --since "-2min" --no-pager -o short-iso
… systemd[1]: Stopping Divi Games side-chain node (divi-gamechain)...
… divi-gamechain[3270609]: INFO divi_gamechain: stopped
… systemd[1]: Started Divi Games side-chain node (divi-gamechain).
… divi-gamechain[3462012]: INFO divi_gamechain: chain opened chain=divi-games-testnet-1 sequencer=y5LF7Lb2AF6bPipM8uC5Z8peGEbfswB3Ge height=301
… divi-gamechain[3462012]: INFO divi_gamechain: serving listen=127.0.0.1:7410
Nothing recompiled (the mirror had no new commits, cargo found its target up to date in half a second), the config files were left alone, the units and nginx file were re-copied, both services restarted, and the node reopened the same chain at the same height with the same sequencer key. That is the whole upgrade procedure: push to the mirror, run the script.
8 · The rules in one table
| Rule | Where it is enforced | What you see if you break it |
|---|---|---|
| Run as root (via sudo) | deploy.sh, id -u check | deploy: run as root |
The invoking user must have cargo | deploy.sh, command -v cargo as $SUDO_USER | deploy: cargo not found for <user> (the build happens on this host) |
| Both env files exist, mode 0600 | deploy.sh, before anything is built | deploy: /etc/divi-games/site.env is missing; create it (mode 0600, owner root) before deploying or … must have mode 0600 |
| Fast-forward only | git merge --ff-only origin/main | git refuses, the script stops under set -e; reset the checkout to the mirror |
| A changed script runs whole | DG_DEPLOY_REEXEC re-exec after the merge | nothing; the new copy of the script takes over |
| Config installed once, never overwritten | [ -f … ] || install for both .toml files | your edits survive every deploy |
| Issuer fixed at genesis | first-deploy branch writes certificate_issuers before the node starts | deploy: warning: the chain already has a genesis; <addr> is not an issuer until it is recreated |
| Units and nginx file are re-copied every run | install into /etc/systemd/system and sites-available | local edits to those two files are lost; change them in the repo |
| Both services must come up | systemctl is-active after restart | deploy: divi-gamechain did not come up; see: journalctl -u divi-gamechain, exit 1 |
Only /var/lib/divi-games is writable | the unit files | a program that tries to write elsewhere gets a read-only filesystem error in the journal |
9 · If something goes wrong
| Symptom | Cause | Fix |
|---|---|---|
deploy: cargo not found for root | you ran it as root directly, and root has no Rust | run it with sudo from the user whose ~/.cargo/bin has cargo |
| Build fails with a missing crate or network error | the first build downloads the dependency tree | the host needs outbound HTTPS to crates.io once; --locked means the versions are those in Cargo.lock |
deploy: ok but curl http://127.0.0.1/ gives the nginx welcome page | no Host header, so the default site answered | send -H "Host: games.divi.domains", or set server_name to your own name |
/chain/v1/info is 502 | the node is down or still starting | journalctl -u divi-gamechain -e; the script would have said did not come up if it was down at the end |
Site logs TELEGRAM_BOT_TOKEN is not set | site.env is empty | fine for playing; add the token to site.env as root and systemctl restart divi-games-site to get login |
| Telegram login never sticks over HTTP | cookie_secure = true | serve over TLS; that is the setting the production site needs |
| You want a clean chain again | the chain is in /var/lib/divi-games/chain | stop the node, move that folder away (keep sequencer.key if you want the same address), start it; the issuer warning in the table above tells you when to also delete gamechain.toml |
| You need the node on the public chain, not your own | a deploy makes a new chain | that is not what this script is for; talk to the public one with the SDK, or run a node as in Run the gamechain |
Next: Anchors and the tDIVI bridge turns on the two Divi-side mechanisms your new chain is still missing, and all tutorials.