Run a hub¶
The hub is one container image. You need a server with Docker, a domain name and a reverse proxy for TLS.
Before you start¶
You need:
- A Linux server with Docker, 1 CPU and 2 GB of memory. The hub uses about 500 MB with the default 4 workers. On a
server with 1 GB, set
WORKERS=2. - A local disk with 10 GB of free space. The database needs about 2 KB for each declaration. Do not put the data on NFS, SMB or a cloud file share: SQLite needs a local file system.
- A domain name for the hub, for example
hub.example.com. The domain is part of the identity of the hub. Do not change it later. - A reverse proxy that terminates TLS, for example Caddy, nginx or Traefik.
- A hub ID. Select the next free hub ID in the hub list of your network. See Join the network.
Make sure that:
- The server clock is synchronized. Each ISCC-ID contains the time of the hub, and wrong times stay in the IDs forever.
On most Linux systems,
timedatectlmust showSystem clock synchronized: yes. - The server can open outbound HTTPS connections to any public host. The hub downloads the hub list from GitHub, fetches the DID documents of declarers (DID verification policy) and sends searches to the search server. If the hub cannot fetch a DID document, it rejects the declaration.
- The domain points to the server before the reverse proxy requests a TLS certificate. Otherwise the certificate can be delayed by DNS caches and the limits of the certificate authority.
Decide on the network first:
| Network | ISCC_HUB_REALM |
Use |
|---|---|---|
| testnet | 0 |
Tests and development. ISCC-IDs are not permanent |
| mainnet | 1 |
Production. ISCC-IDs are permanent |
Fixed for the life of the hub
The hub ID, the realm and the domain are fixed. Each ISCC-ID contains the hub ID and the realm. The domain is the
did:web identity of the hub. If you change one of them, you must start a new hub with a new database.
The signing key
The hub publishes its public key in its DID document. The protocol rotates keys in the DID document. This version of
the hub lists only the current key. If you change ISCC_HUB_SECKEY, nobody can verify old receipts and checkpoints
against the DID document anymore. Change the key only if it is compromised.
1. Make the secrets¶
The hub needs two secrets. Make them one time.
Make the Ed25519 signing key of the hub:
The command prints a JSON object. secret_key is the value of ISCC_HUB_SECKEY. The public_key goes into the hub
list. The hub also publishes it at https://<your domain>/.well-known/did.json.
Keep a copy of the secret key offline, in two safe places. A restored hub needs the same key. See Back up.
Make the Django secret key:
2. Write the configuration¶
Put the secrets into a .env file next to your compose file. Do not commit this file.
DJANGO_SECRET_KEY=<the Django secret key>
ISCC_HUB_SECKEY=<the secret_key from iscc-crypto>
ISCC_HUB_ADMIN_PWD=<a password for the admin interface>
Write each comment in a .env file on its own line. docker run --env-file reads a comment after a value as part of
the value. The .env.example file in the repository lists
all settings.
Write compose.yaml. Replace the domain and the hub ID:
services:
iscc-hub:
image: ghcr.io/iscc/iscc-hub:0.1.2
restart: unless-stopped
ports:
- 127.0.0.1:8000:8000
volumes:
- iscc-hub-data:/app/data
# Optional logo, see Configuration > Home page
# - ./logo.svg:/app/iscc_hub/static/media/logo.svg:ro
environment:
DJANGO_DEBUG: "false"
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
ISCC_HUB_DOMAIN: hub.example.com
ISCC_HUB_SECKEY: ${ISCC_HUB_SECKEY}
ISCC_HUB_ID: "42"
ISCC_HUB_REALM: "0"
ISCC_HUB_ADMIN_PWD: ${ISCC_HUB_ADMIN_PWD}
ISCC_HUB_SQLITE_SYNC_MODE: FULL
# Gunicorn needs up to 30 seconds to finish open requests at shutdown
stop_grace_period: 45s
# Docker does not rotate container logs by default
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
volumes:
iscc-hub-data:
Always use an exact version tag. Do not use latest, so that each upgrade is your decision. The
Configuration page lists all settings.
Keep the data volume
All state is in the volume on /app/data. Without a volume, the hub loses its log each time you recreate the
container. Never change the volume, ISCC_HUB_DB_ENGINE or ISCC_HUB_DB_NAME after the first start. The hub would
then start an empty log under the same identity.
3. Start the hub¶
At each start, the container:
- Applies the database migrations.
- Makes the admin user
adminifISCC_HUB_ADMIN_PWDis set and the user does not exist. - Downloads the hub list of its realm from GitHub. If the download fails, the hub starts anyway and tries again later.
- Starts the web server on port 8000.
4. Set up the reverse proxy¶
The container serves plain HTTP on port 8000. The reverse proxy must terminate TLS and send the original Host header.
If it sends a different host name, the hub answers with status 400.
The examples below are for a proxy on the host. If your proxy runs in a container, for example Traefik, put both
containers on one Docker network, remove ports: from compose.yaml and point the proxy at iscc-hub:8000.
Caddy sends the original Host header by default:
nginx sends the upstream address as Host header by default. Set it to $host:
server {
listen 443 ssl;
server_name hub.example.com;
# ssl_certificate and ssl_certificate_key of your certificate
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
}
}
To limit write requests (see below), add this to the http block:
map $request_method $hub_write { default ""; POST $binary_remote_addr; DELETE $binary_remote_addr; }
limit_req_zone $hub_write zone=hub_writes:10m rate=30r/m;
limit_req_status 429;
And this location to the server block:
We also recommend these proxy rules:
- Limit write requests. The hub has no rate limit of its own. Declarations are permanent, and each new controller
costs a DID fetch. Limit
POSTandDELETErequests to paths that start with/declaration, for each IP address, for example to 30 per minute. Answer with status 429. Do not limit reads or/health. nginx haslimit_req. Caddy needs thecaddy-ratelimitplugin. - Restrict the admin interface. If you can, allow
/admin/only from your own IP addresses or your VPN.
If the proxy must send a different host name, set DJANGO_ALLOWED_HOSTS and DJANGO_CSRF_TRUSTED_ORIGINS. See
Configuration.
5. Check the hub¶
Do these checks from a different computer:
curl -H "Accept: application/json" https://hub.example.com/health
curl https://hub.example.com/.well-known/did.json
curl https://hub.example.com/log/checkpoint
/healthreturns"status": "pass"and the version. Without theAcceptheader, it returns an HTML page./.well-known/did.jsoncontains thepublic_keyfrom step 1./log/checkpointreturns a signed checkpoint. A new hub has tree size 0.docker compose psshows the container ashealthy.
Then open https://hub.example.com/admin/ and log in as admin.
6. Join the network¶
Other hubs and clients find your hub through the hub list. Add your hub to the list: see Join the network.
Then set up backups and monitoring: see Operations.
With PostgreSQL¶
SQLite is the default and needs no other service. Use PostgreSQL if you already run it, or if your platform has no persistent local disk. The hub is tested with PostgreSQL 16.
Add POSTGRES_PASSWORD=<a long random password> to your .env file. Then use this compose.yaml instead of the one in
step 2:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: iscc_hub
POSTGRES_USER: iscc_hub
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- iscc-hub-pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U iscc_hub -d iscc_hub"]
interval: 10s
timeout: 5s
retries: 5
stop_grace_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
iscc-hub:
image: ghcr.io/iscc/iscc-hub:0.1.2
restart: unless-stopped
# Start the hub only when the database accepts connections
depends_on:
db:
condition: service_healthy
ports:
- 127.0.0.1:8000:8000
environment:
DJANGO_DEBUG: "false"
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
ISCC_HUB_DOMAIN: hub.example.com
ISCC_HUB_SECKEY: ${ISCC_HUB_SECKEY}
ISCC_HUB_ID: "42"
ISCC_HUB_REALM: "0"
ISCC_HUB_ADMIN_PWD: ${ISCC_HUB_ADMIN_PWD}
ISCC_HUB_DB_ENGINE: postgres
ISCC_HUB_DB_HOST: db
ISCC_HUB_DB_USER: iscc_hub
ISCC_HUB_DB_PASSWORD: ${POSTGRES_PASSWORD}
ISCC_HUB_DB_NAME_PG: iscc_hub
stop_grace_period: 45s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
volumes:
iscc-hub-pg:
With PostgreSQL:
- All state is in the database. The hub container needs no volume. Keep the
iscc-hub-pgvolume. ISCC_HUB_SQLITE_SYNC_MODEand the local disk rule for/app/datado not apply. Keep the PostgreSQL defaultsfsync=onandsynchronous_commit=on, so that the database writes each declaration to disk before the hub confirms it.- Use one database for each hub. Never let two hubs share one database.
To use a PostgreSQL server that you already have, or a managed database service:
- Make a database and a user for the hub. Make the user the owner of the database, so that the hub can create its tables.
- Set
ISCC_HUB_DB_HOST,ISCC_HUB_DB_PORT,ISCC_HUB_DB_USER,ISCC_HUB_DB_PASSWORDandISCC_HUB_DB_NAME_PG. - If the server requires TLS, also set
PGSSLMODE=verify-fulland, if needed,PGSSLROOTCERTwith the path of the CA certificate in the container. The hub passes these standard PostgreSQL variables to the database driver.
The hub applies its migrations at the first start. Operations describes backups with
pg_dump.
Without Compose¶
docker run -d --name iscc-hub --restart unless-stopped \
--env-file .env \
-e DJANGO_DEBUG=false \
-e ISCC_HUB_DOMAIN=hub.example.com \
-e ISCC_HUB_ID=42 \
-e ISCC_HUB_REALM=0 \
-e ISCC_HUB_SQLITE_SYNC_MODE=FULL \
-v iscc-hub-data:/app/data \
-p 127.0.0.1:8000:8000 \
--stop-timeout 45 \
--log-driver json-file --log-opt max-size=10m --log-opt max-file=5 \
ghcr.io/iscc/iscc-hub:0.1.2