<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: The complete install reference: upload and extract the archive, run server/install.sh, every option, its output, the files it writes and what to do next. -->
# Installing COLLAB on a server

`server/install.sh` sets up a complete COLLAB site on a Debian or Ubuntu server with nginx. This is
the complete reference for it: what to prepare, the commands, every option, what the installer
prints, and the files, users and services it leaves behind. It sets up:
- **HTTPS.** A Let's Encrypt certificate for every name, renewed by itself.
- **The names.** The site at **https://collab.genesisl1.com**. **dao.genesisl1.com**,
  **collab.descifi.org** and **dao.descifi.org** answer `301` to the same path and query there.
- **The admin page** at `/admin`: deploy the contracts from your wallet, publish them on this
  site, change the site's settings, watch its status.
- **The prerender job.** It gives every DAO and token a page of its own for search engines and
  link previews, and keeps the logos, descriptions, token list and L1 price cached.

The site itself stays serverless. Visitors' browsers read the chain directly, and every page keeps
working when the admin service or the prerender job is stopped. COLLAB 2.2 is an alpha: it has had
no external audit ([Security](SECURITY.md)). [Running a COLLAB site](operators.md) explains what the
server does and does not do.

## What you need

- A server with Debian 11 or later, or Ubuntu 22.04 or later, root access, and ports 80 and 443
  reachable from the internet.
- The release archive, `collab-2.2.0.tar.gz`, and its `.sha256` file.
- For deploying the contracts: a browser wallet with some L1 for gas (section 4).

[Before you install](before-you-install.md) is the checklist: server, ports, DNS, firewall, an
nginx that serves other sites, the main host and the RPC.

The installer adds what is missing: nginx, certbot, python3 with Pillow and pycryptodome, openssl
and curl.
- nginx may already serve other sites. The installer adds one file of its own and never touches
  theirs, nor the default site.
- It refuses to go on when another site already answers for one of COLLAB's names (section 9).

## 1. DNS

Point each name at the server before you install: an `A` record with its IPv4 address and, when the
server has IPv6, an `AAAA` record with that address.

| Name | Record | Role |
|---|---|---|
| `collab.genesisl1.com` | A (and AAAA) | the site |
| `dao.genesisl1.com` | A (and AAAA) | 301 to the site |
| `collab.descifi.org` | A (and AAAA) | 301 to the site |
| `dao.descifi.org` | A (and AAAA) | 301 to the site |

Check from your computer with `dig +short collab.genesisl1.com`, or from the server with
`getent ahosts collab.genesisl1.com`, which is what the installer reads. If the names are on Cloudflare, set them to **DNS only** (grey
cloud), at least until the certificate is issued.

The installer checks each name before asking for the certificate:
- a name passes when its DNS points at an address of this server;
- behind NAT or a proxy, it passes when a request through the name reaches this server.

A name that fails is left out of the certificate, with a warning. Run the installer again once its
DNS is right. [Before you install](before-you-install.md#dns-point-every-name-first) has the
details.

## 2. Upload and install

From your computer, then on the server:

```bash
ssh root@SERVER mkdir -p /var/www/html    # a new server has no such folder before a web server
scp collab-2.2.0.tar.gz collab-2.2.0.tar.gz.sha256 root@SERVER:/var/www/html/
ssh root@SERVER
cd /var/www/html
sha256sum -c collab-2.2.0.tar.gz.sha256
tar xzf collab-2.2.0.tar.gz
sudo ./collab-2.2.0/server/install.sh --email you@example.org
```

`--email` is the contact address of the Let's Encrypt account; you may leave it out.

The extracted folder is only the installer's source:
- The installer copies what the site needs to `/var/www/collab/releases/2.2.0`, and nothing
  depends on the folder afterwards.
- `/var/www/html` is the root of nginx's default site, so its files can be fetched under
  `http://SERVER/collab-2.2.0/` while it is there. It holds nothing secret.
- Delete the folder and the archive once the install is done, or keep them until the next upgrade:

  ```bash
  sudo rm -rf /var/www/html/collab-2.2.0 /var/www/html/collab-2.2.0.tar.gz /var/www/html/collab-2.2.0.tar.gz.sha256
  ```

The installer prints what it does, step by step:
1. **Packages:** installs only what is missing.
2. **Checking nginx:** checks before changing anything, so a refused run leaves the server as it was.
   - nginx's configuration must pass its own test (`nginx -T`).
   - No other site may answer for COLLAB's names (wildcard names such as `*.genesisl1.com` are
     reported but not refused: nginx prefers COLLAB's exact names).
   - No program other than nginx may hold ports 80 or 443.
3. **Users and folders:** creates the system users `collab` (the admin service) and
   `collab-prerender` (the prerender job) and the folders of section 7. The folders under `shared/`
   are created so that a symbolic link or a folder of the wrong owner there is refused, never
   followed.
4. **Release:** copies the release to `/var/www/collab/releases/<version>`, owned by root. Nothing
   is served from it yet.
5. **Settings:** `/etc/collab/collab.env`, written on the first run with a new admin token and two
   secrets, and kept on every later run. Options you pass change only the values they name.
6. **Site configuration:** `config.js` from the site settings and the published deployment, written
   as user `collab`. The content policy step writes the policy `config.js` needs into the new
   release's pages, and the helper `/usr/local/sbin/collab` is written. Until a deployment is
   published, every page says COLLAB is not deployed here yet.
7. **nginx:** COLLAB's own file, `/etc/nginx/sites-available/collab.conf`, linked as
   `sites-enabled/zz-collab.conf`. Every change is checked with `nginx -t`; when nginx refuses it,
   the previous file comes back and nginx keeps running as before. Only then does
   `/var/www/collab/current` switch to the new release. When the release or its assets version
   changed, the old prerendered pages are removed first; releases older than the two before are
   deleted.
   Until there is a certificate, port 80 answers Let's Encrypt's challenges and a short "being set
   up" notice. Which site answers names nobody configured is explained in
   [Domains, redirects and certificates](domains.md#unknown-names-on-ports-80-and-443).
8. **DNS and certificate:** the check of section 1, then `certbot certonly --webroot` for one
   certificate named `collab` with every name that passed.
9. **nginx: HTTPS:**
   - the site on 443 with HTTP/2 (before nginx 1.25.1, only when the other sites on port 443 use
     it already), HSTS, the security headers and caching;
   - one server that answers the other names with `301`;
   - port 80 answers `301` to HTTPS on the main name for every name, and keeps the challenge path
     open for renewals.
10. **Services:** the systemd units of section 7, enabled and started, and the admin service
    restarted. certbot's own timer renews the certificate, and the deploy hook the installer gives
    certbot, `collab renewed`, reloads nginx.
    Once HTTPS is on, the content policy step runs again (section 6). A prerender run is requested,
    and when ufw is active, ports 80 and 443 are allowed.
11. **Checking:** the site and the admin service are asked through nginx, on the server itself.

It ends with a summary. This one is from the test sandbox (`server/tests/sandbox_install.sh`),
which installs under other paths, on port 8443 and with a self-signed certificate; the admin token
is masked here:

```text
COLLAB 2.2.0 is installed. It is an alpha: the site says so at the top of every page.
Site:          https://collab.genesisl1.com:8443/
Redirected:    dao.genesisl1.com, collab.descifi.org, dao.descifi.org (301 to the site, path and query kept)
Admin page:    https://collab.genesisl1.com:8443/admin
Admin token:   ********************************    (any time: sudo /var/tmp/collab-sandbox.RNfwGK/root/bin/collab admin-token)
Certificate:   collab.genesisl1.com dao.genesisl1.com collab.descifi.org dao.descifi.org (self-signed, for testing)
Settings:      /var/tmp/collab-sandbox.RNfwGK/etc/collab/collab.env (the token and secrets; root:collab 0640)
Files:         /var/tmp/collab-sandbox.RNfwGK/root/current -> releases/2.2.0; /var/tmp/collab-sandbox.RNfwGK/root/shared (config.js, site.json, deployments, prerender, data)
Status, logs:  sudo /var/tmp/collab-sandbox.RNfwGK/root/bin/collab status | logs

Next: open the admin page, sign in with the token, deploy the contracts from your wallet (Deploy),
then Publish to this site. Until then every page says COLLAB is not deployed here yet.
The folder this ran from (/var/tmp/collab-sandbox.RNfwGK/src/collab-2.2.0) and its archive are no longer needed: delete them, or keep
them until the next upgrade (run the new release's server/install.sh the same way).
```

On a real server the helper is `sudo collab admin-token`, and the paths are `/etc/collab` and
`/var/www/collab`. With Let's Encrypt the certificate line ends "(Let's Encrypt, renewed by
certbot's timer)". Other lines that may appear:
- `Not covered:   dao.descifi.org: point their DNS at this server, then run this again (the same command).`
- `COLLAB 2.2.0 is installed, but not on HTTPS yet: see the warnings above.` when the main name has
  no certificate yet.
- `This site is deployed: config.js names its contracts and was kept.` in place of "Next: …" when a
  deployment is already published (an upgrade, or a second run).
- `This ran from an installed release (/var/www/collab/releases/2.2.0): keep that folder, it is part
  of the site.` in place of the last two lines, when you ran the installer from an installed
  release (section 8).

### Options

| Option | What it does |
|---|---|
| `--main HOST` | the site's address (default `collab.genesisl1.com`) |
| `--alias HOST` | another name that answers 301 to it; repeat it for each name. The list replaces the default one, which is the other three of the four names above |
| `--no-aliases` | no other names |
| `--email ADDRESS` | contact address of the Let's Encrypt account (optional) |
| `--rpc URL` | the RPC endpoint the prerender job reads (default: the site's own RPCs, in order); `--rpc ""` goes back to them |
| `--admin-port N` | the admin service's port on 127.0.0.1 (default 8411) |
| `-h`, `--help` | the installer's help text |

These are for testing only (`server/tests/sandbox_install.sh` uses them):

| Option | What it does |
|---|---|
| `--self-signed` | a self-signed certificate instead of Let's Encrypt; browsers warn every visitor |
| `--prefix DIR` | install under DIR instead of `/var/www/collab`, the helper as `DIR/bin/collab` |
| `--etc DIR` | what goes to `/etc` goes to DIR instead: `DIR/collab`, `DIR/nginx` (its `nginx.conf` is the one tested and reloaded), `DIR/systemd/system` |
| `--no-systemd` | start no services: the units are written, and the commands to run are printed |
| `--http-port N` | HTTP on N instead of 80 |
| `--https-port N` | HTTPS on N instead of 443 |

To make `dao.genesisl1.com` the main name on a first install, run the same command with
`--main dao.genesisl1.com`. The aliases become the other three, and every redirect, canonical link
and certificate follows.

### Changing options later

The installer remembers `--main`, the aliases, `--email`, `--rpc` and the ports in
`/etc/collab/collab.env`. A later run without them keeps them; an option you pass changes only
what it names. `--self-signed`, `--prefix`, `--etc` and `--no-systemd` are not remembered: a run
without `--self-signed` asks Let's Encrypt for a certificate.

Run the same installer again with the option, from the extracted folder or from the installed copy:

```bash
sudo /var/www/collab/current/server/install.sh --rpc https://rpc.example.org
```

Run from an installed release, the summary ends "This ran from an installed release (…): keep that
folder, it is part of the site."

Two options need care:
- `--alias` replaces the whole list of aliases. Name every alias you want to keep.
- Switching the main host after the install keeps the remembered aliases, so the old main name is
  dropped unless you name it with `--alias`. See
  [Domains, redirects and certificates](domains.md#changing-the-main-host).

## 3. The admin page

Open `https://collab.genesisl1.com/admin` and sign in with the admin token.
- The installer prints the token; `sudo collab admin-token` shows it again.
- `sudo collab admin-token --new` replaces it and ends every session signed in with the old one.

Sessions last 12 hours. Signing out ends the session at once, and stays effective when the admin
service restarts. After 10 failed sign-ins in 10 minutes from one address, sign-in waits until the
first of them is 10 minutes old.

The tabs:
- **Status:** the release, the published deployment, the prerender job (last run, errors, warnings,
  its log), the certificate, the content policy, the server and `config.js`.
- **Deploy:** deploys the protocol's contracts from your wallet. Any EIP-1193 browser wallet works
  (MetaMask, Rabby, Keplr in EVM mode): the page never asks for a key.
- **Publish:** puts a deployment on this site, or takes it off.
- **Check:** compares any deployment with this release, contract by contract.
- **Site settings:** the alpha strip, an announcement, featured and hidden DAOs, verified and hidden
  tokens, search-engine texts, links, Google Analytics, RPC endpoints and the explorer.

Deploy and Check work without signing in. `/deploy` answers `301` to `/admin/deploy`.
[The admin page](admin.md) describes every tab.

## 4. Order of the first day

1. Install (section 2) and open the admin page.
2. Deploy the contracts on GenesisL1 from the **Deploy** tab: one transaction per contract, from
   your wallet. Let each transaction confirm before you reload the page. See
   [Deploy the protocol from /admin](deploy-from-admin.md).
3. **Publish to this site.** The record is checked strictly:
   - chain 29, a COLLAB DAO v2.2 version;
   - every contract address present, non-zero and distinct, with a right EIP-55 checksum;
   - the integration lists checked as `tools/write_web_config.py` checks them.
4. Wait for the first prerender (Status; a few seconds to minutes), then open the site, create a
   small test DAO and run one proposal through.
5. Optionally: [site settings](site-settings.md), Google Analytics, and
   `python3 /var/www/collab/current/deploy/check_site.py https://collab.genesisl1.com`
   ([Status, logs and the collab command](monitoring.md#checking-the-live-site)).

To deploy with Foundry instead ([Deploying COLLAB: reference](DEPLOY.md), section 1), publish the
record it writes from the **Publish** tab.

## 5. The deployment record and config.js

The server keeps one deployment for its chain (29), in `/var/www/collab/shared/deployments/29.json`.
- Publishing another record replaces it.
- **Unpublish** on the Publish tab (the admin API's `{"record": null}`) takes the site back to its
  undeployed mode.
- The replaced file is kept in `shared/history/`, as every replaced `config.js` and `site.json` is
  (the newest 50 of each).

`config.js` is written only by `tools/write_web_config.py`: the admin service calls it, and so does
the installer. It holds:
- the record's addresses and integration lists;
- `"deployed"`;
- the site settings as `"site"`;
- the RPC endpoints, explorer and analytics ID from the site settings;
- the site's address.

The page caches never keep it: a change takes effect on the next page load.
[Publish, unpublish and check](publish.md) covers the operator's side, and
[Data formats](data-formats.md#site-configuration) the file itself.

## 6. Site settings and the content policy

Site settings are kept in `shared/site.json` and checked strictly. Unknown keys, control or
invisible characters, URLs other than https, malformed slugs or addresses, and lists that are too
long are all refused, naming the field. [Site settings](site-settings.md) describes every field.
Hiding changes nothing on chain or on other copies of COLLAB.

Some settings change the site's Content-Security-Policy: an analytics ID, or other RPC endpoints.
- The policy is the `<meta>` tag of every page, and the same policy, with `frame-ancestors`, is
  nginx's header.
- The admin service cannot change either, by design.
- It drops a request file instead. `collab-apply.path` then starts `collab-apply.service` as root.

That service:
- checks every value of `config.js` again, and trusts nothing the admin service wrote;
- builds the policy from origins only, and refuses one with any character it never needs;
- writes the policy into the release's pages and into COLLAB's own nginx file, and touches nothing
  else;
- checks nginx with `nginx -t` and reloads it. When nginx refuses, it puts every file back and
  says why in Status (and in `sudo collab logs`);
- does nothing when the policy is unchanged;
- asks for a prerender run afterwards, so the prerendered pages carry the new policy too.

The installer runs the same step after every upgrade, so a new release gets the policy too.
`sudo collab apply` runs it by hand.

## 7. Files, users and services

```text
/var/www/collab/
  releases/<version>/   web/, tools/, server/, deploy/, docs/ of a release (root; only the policy step changes its pages)
  current -> releases/<version>
  shared/               collab, 0755
    config.js           the site's configuration (served at /config.js)
    site.json           the site settings
    deployments/29.json the published deployment record
    history/            the replaced versions (0750)
    status/             the content policy step, the certificate's status and the sign-in sessions (0750)
    jobs/               the prerender job's requests, status, log, lock and caches (collab-prerender, group collab, 2770)
    prerender/          the prerender job's pages, preview images and sitemap (collab-prerender)
    data/               its cache, served at /data/: logos, descriptions, tokens.json, price.json (collab-prerender)
  acme/                 Let's Encrypt's webroot
/etc/collab/            0751
  collab.env            settings, the admin token and two secrets (root:collab 0640)
  prerender.env         the same settings without the secrets, for the prerender job (0644)
  nginx-proxy.conf      the secret header nginx adds for the admin service (root 0600)
  tls/                  only with --self-signed: the test certificate
/etc/nginx/sites-available/collab.conf, sites-enabled/zz-collab.conf
/etc/letsencrypt/live/collab/   the certificate (certbot's)
/usr/local/sbin/collab  the helper
```

| Unit | What it does |
|---|---|
| `collab-admin.service` | the admin API, user `collab`, on 127.0.0.1:8411 |
| `collab-prerender.service` | one prerender run, user `collab-prerender` |
| `collab-prerender.timer` | the prerender every 10 minutes |
| `collab-prerender.path` | a prerender as soon as the admin page asks for one |
| `collab-apply.path` + `collab-apply.service` | the content policy step, as root, on request |
| `certbot.timer` | certificate renewal (certbot's own) |

How the services are confined:
- **The admin service** runs sandboxed by systemd: no privileges, a read-only system, no network
  beyond 127.0.0.1, and write access to `shared/` only.
  - It accepts only requests that carry the secret header nginx adds.
  - Every state-changing request must be JSON, carry `X-Collab-Admin: 1` and come from the site's
    own origin.
- **The prerender job** has its own user and may write only `prerender/`, `data/` and `jobs/`.
  - It cannot read the admin token or the secrets.
  - It needs outbound HTTPS to the RPC endpoints and to api.coingecko.com.
  - nginx never serves scripts, `config.js` or the admin page from its folders. A job that reads
    untrusted chain data, images included, can change neither the contracts the site uses nor its
    code.

The helper, `sudo collab …`:

| Command | |
|---|---|
| `admin-token [--new]` | the admin token (a new one) |
| `status` | release, site, deployment, prerender, content policy, certificate, services |
| `prerender [--request]` | run the prerender job now, in the foreground (or ask the service for a run) |
| `logs [-f] [LINES]` | the log of the admin service, the prerender job and the policy step |
| `apply` | the content policy step |
| `renewed` | certbot's deploy hook: reload nginx and note the new certificate |

[Status, logs and the collab command](monitoring.md) shows what each prints.

## 8. Upgrades, backups and moving

**Upgrade.** Upload and extract the new archive as in section 2, and run its installer:

```bash
cd /var/www/html && tar xzf collab-2.2.1.tar.gz && sudo ./collab-2.2.1/server/install.sh
```

Extract each archive where no folder of the same name is left. tar over an older `collab-2.2.1/`
keeps the files the new archive no longer has, and the installer copies them into the release
with the rest, without checking them against `SHA256SUMS`. Delete such a folder first.

The settings, the token, the published deployment, the site settings and the certificate are kept,
and the two previous releases stay in `releases/`.

**Going back.** Run the older release's own copy of the installer, never `ln -sfn`:

```bash
sudo /var/www/collab/releases/2.2.0/server/install.sh
```

**Backups.** Back up `/etc/collab` and `shared/site.json`, `config.js`, `deployments/` and
`history/`.

[Upgrades, rollbacks and backups](upgrades.md) has the commands, what happens at each step,
restoring and moving to another server.

## 9. Troubleshooting

| What you see | What to do |
|---|---|
| "Another site on this nginx answers for a name COLLAB would take" | That site's file is named. Remove the name from its `server_name` (and `sudo nginx -t && sudo systemctl reload nginx`), or install without it (`--main`, `--alias`, `--no-aliases`). Nothing was changed. |
| "Port 80 is used by another program" (or 443) | Another web server (Apache, Caddy) holds the port. Stop or reconfigure it, then run the installer again. |
| A name "points at …, not at this server", or the certificate leaves names out | Fix the DNS record (section 1) and run the same command again; the certificate is extended. Port 80 must be reachable from the internet for Let's Encrypt. |
| certbot could not get the certificate | The installer shows certbot's last lines; `/var/log/letsencrypt/letsencrypt.log` has the rest. Fix the cause before trying again: Let's Encrypt allows only a few failed validations an hour. |
| The site answers "COLLAB is being set up" (503) | There is no certificate for the main name yet (above). |
| The admin page says the admin service is not running | `sudo systemctl status collab-admin`, `sudo collab logs`. |

The installer can always be run again: it repairs what is missing and keeps what is there.
[Troubleshooting](troubleshooting.md) covers these and the other cases, from the prerender's
warnings to a refused content policy.

## 10. Testing the installer

`sudo server/tests/sandbox_install.sh` runs the installer in a sandbox without touching the
system's nginx. It sets up:
- a private nginx on ports 8088 and 8443, with another site on it;
- a self-signed certificate;
- the admin service as user `collab`.

Then it checks:
- the refusal while another site claims a name;
- `nginx -t`, the redirects, headers, caching, `/data/` and the prerender overlay;
- the admin API with its refusals and limits, and the content policy step;
- `deploy/check_site.py`;
- a second run that changes nothing, the same version again with other files, an upgrade, and a run
  from an installed release;
- the Let's Encrypt path, with certbot and DNS stubbed: one name without DNS, the deploy hook, and a
  next run that keeps the certificate.

`python3 -m unittest discover -s server/tests` runs the unit tests of the admin service. See
[Testing](testing.md#server-tests).

## Related

- [Before you install: server, DNS and names](before-you-install.md)
- [The admin page](admin.md)
- [Deploy the protocol from /admin](deploy-from-admin.md)
- [Upgrades, rollbacks and backups](upgrades.md)
- [Troubleshooting](troubleshooting.md)
