Skip to content

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, timedatectl must show System 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:

uvx iscc-crypto keygen

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:

python -c "import secrets; print(secrets.token_urlsafe(64))"

2. Write the configuration

Put the secrets into a .env file next to your compose file. Do not commit this file.

.env
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:

compose.yaml
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

docker compose up -d
docker compose logs -f iscc-hub

At each start, the container:

  1. Applies the database migrations.
  2. Makes the admin user admin if ISCC_HUB_ADMIN_PWD is set and the user does not exist.
  3. Downloads the hub list of its realm from GitHub. If the download fails, the hub starts anyway and tries again later.
  4. 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:

Caddyfile
hub.example.com {
    reverse_proxy 127.0.0.1:8000
}

nginx sends the upstream address as Host header by default. Set it to $host:

/etc/nginx/conf.d/iscc-hub.conf
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:

location /declaration {
    limit_req zone=hub_writes burst=10 nodelay;
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
}

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 POST and DELETE requests 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 has limit_req. Caddy needs the caddy-ratelimit plugin.
  • 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
  • /health returns "status": "pass" and the version. Without the Accept header, it returns an HTML page.
  • /.well-known/did.json contains the public_key from step 1.
  • /log/checkpoint returns a signed checkpoint. A new hub has tree size 0.
  • docker compose ps shows the container as healthy.

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:

compose.yaml
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-pg volume.
  • ISCC_HUB_SQLITE_SYNC_MODE and the local disk rule for /app/data do not apply. Keep the PostgreSQL defaults fsync=on and synchronous_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:

  1. 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.
  2. Set ISCC_HUB_DB_HOST, ISCC_HUB_DB_PORT, ISCC_HUB_DB_USER, ISCC_HUB_DB_PASSWORD and ISCC_HUB_DB_NAME_PG.
  3. If the server requires TLS, also set PGSSLMODE=verify-full and, if needed, PGSSLROOTCERT with 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

Build the image yourself

git clone https://github.com/iscc/iscc-hub
cd iscc-hub
git checkout v0.1.2
docker build --target production -t iscc-hub .