2 Installation

This chapter installs one instance with Docker Compose. Installation takes about fifteen minutes, most of it the first image build.

2.1 Before you begin

You need:

A release is the product's source tree: docker-compose.yml, the Dockerfile, the deploy/ scripts and the application code. Compose builds the application image from it on your server.

2.2 Unpack the release

  1. Create the installation directory and unpack the release into it. This guide uses /srv/grc throughout.

    sudo mkdir -p /srv/grc
    sudo chown "$USER" /srv/grc
    tar -xzf grc-release-<version>.tar.gz -C /srv/grc
    cd /srv/grc
    
  2. Check that docker-compose.yml, Dockerfile and the deploy/ directory are at the top of /srv/grc. Every command in this guide runs from that directory.

Keep the installation directory for this release only. Upgrades replace its contents (see Upgrades), and a file you add there yourself stops the next upgrade unless you list it in .deploy-keep.

2.3 Create the .env file

All instance configuration that is not made in the application lives in .env next to docker-compose.yml.

  1. Copy the example and restrict it to your account:

    cp deploy/env.example .env
    chmod 600 .env
    
  2. Generate the three required secrets and paste each into .env:

    openssl rand -base64 32   # AUTH_SECRET
    openssl rand -hex 32      # POSTGRES_PASSWORD
    openssl rand -hex 32      # APP_DB_PASSWORD
    

    Use hexadecimal for the two database passwords: they go into a database connection URL, where characters such as /, @ and + would break it.

  3. Fill in the remaining values:

    SettingValue
    APP_URLThe public address users reach, with https:// and no trailing slash, for example https://grc.example.com.
    APP_NAMEThe product name shown until an administrator sets one under Settings → Branding.
    INITIAL_ADMIN_EMAILThe email address of the first administrator.
    INITIAL_ADMIN_PASSWORDIts password, 12 characters or more.
    DOMAINWith the bundled Caddy: the host name only, for example grc.example.com.
    ACME_EMAILWith the bundled Caddy: the address Let's Encrypt sends certificate notices to.
  4. Decide now whether to set BACKUP_SIGNING_KEY and SETTINGS_ENCRYPTION_KEY (32 characters or more each, for example from openssl rand -base64 32). Both are optional. Setting them at installation lets you rotate AUTH_SECRET later without losing in-app restore of older snapshots or the stored integration passwords. Adding them later has the same effect as changing AUTH_SECRET. See Configuration Reference.

  5. Store a copy of .env in your organization's password vault.

The full list of settings, with defaults, is in Configuration Reference. Compose reads .env only to fill in docker-compose.yml: a variable reaches the containers only if docker-compose.yml passes it on, so adding other variables to .env has no effect on the application.

2.4 Choose how HTTPS is provided

Pick one of the three options below before the first start.

2.4.1 Option 1: the bundled Caddy

The tls profile starts Caddy in front of web. Caddy obtains and renews a certificate from Let's Encrypt for DOMAIN, redirects HTTP to HTTPS, sends the HSTS header, compresses responses, and refuses request bodies larger than the application accepts (32 MiB, and 2049 MiB on /api/backups/restore) before they reach it. Caddy also replaces any X-Forwarded-For header a client sends with the address it saw, so the default TRUSTED_PROXY_HOPS=1 is correct.

Requirements: DOMAIN and ACME_EMAIL set in .env, ports 80 and 443 free on the host and reachable for the ACME challenge. Caddy refuses to start without DOMAIN.

Start every command that brings the stack up with --profile tls.

2.4.2 Option 2: the bundled Caddy with your own certificate

If the server cannot be reached by Let's Encrypt (an internal network), keep the bundled Caddy and give it a certificate you supply. Do not edit deploy/Caddyfile: an upgrade replaces it. Instead:

  1. Copy the shipped file to a top-level Caddyfile.* name, which upgrades never touch:

    cp deploy/Caddyfile Caddyfile.local
    
  2. In Caddyfile.local, add a tls line inside the site block that names your certificate and key as they will appear inside the container:

    {$DOMAIN} {
    	tls /certs/fullchain.pem /certs/privkey.pem
    	...
    }
    

    For a server reached only on an internal network, tls internal makes Caddy issue its own certificate from a local authority instead; browsers then need Caddy's root certificate installed to trust it.

  3. Create docker-compose.override.yml (also never touched by upgrades) to mount the file and the certificates:

    services:
      caddy:
        volumes:
          - ./Caddyfile.local:/etc/caddy/Caddyfile:ro
          - /etc/ssl/grc:/certs:ro
    
  4. Keep the request body limits and the reverse_proxy web:3000 line from the shipped file. Compare your copy with deploy/Caddyfile after each upgrade and carry over any change.

2.4.3 Option 3: your own reverse proxy

Leave out --profile tls and point your proxy at http://127.0.0.1:3000. The proxy must:

An nginx example:

server {
    listen 443 ssl;
    server_name grc.example.com;
    ssl_certificate     /etc/ssl/grc/fullchain.pem;
    ssl_certificate_key /etc/ssl/grc/privkey.pem;

    client_max_body_size 32m;

    location = /api/backups/restore {
        client_max_body_size 2049m;
        proxy_read_timeout 300s;
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        proxy_read_timeout 300s;
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

If the proxy runs on another machine, set WEB_BIND in .env to the server's private address (never 0.0.0.0 on a host with a public interface) and firewall port 3000 so that only the proxy can reach it.

2.5 First start

  1. Build the image and start the stack. With the bundled Caddy:

    docker compose --profile tls up -d --build
    

    Behind your own proxy, leave out --profile tls:

    docker compose up -d --build
    
  2. Follow the migrate service until it exits:

    docker compose logs -f migrate
    

    On a new installation the log ends with lines like these:

    app-role: grc_app created; grants applied; pgboss owned by grc_app.
    [bootstrap] reference data added: <n> roles, <n> classifications, <n> templates, <n> data mapping questionnaires
    [bootstrap] shared dashboard: default layout created
    [bootstrap] created administrator [email protected]; remove INITIAL_ADMIN_PASSWORD now
    

    <n> stands for the counts, which depend on the release. Before them, Prisma lists the database migrations it applied.

  3. Check that every service is up and that web reports healthy:

    docker compose ps
    

    migrate shows as exited with code 0; db and web show (healthy); worker (and caddy) show as running.

  4. Check the health endpoint:

    curl -s http://127.0.0.1:3000/api/health
    # {"status":"ok"}
    curl -s https://grc.example.com/api/health
    # {"status":"ok"}
    

If migrate stops with an error, web and worker stay down; see Monitoring and Troubleshooting.

2.6 Bootstrapping the first administrator

The first administrator is created by the bootstrap step that migrate runs on every start (prisma/bootstrap.ts, available in development as npm run db:bootstrap). The bootstrap:

  1. Adds any missing reference data (default roles, classifications, email templates and the default data mapping questionnaire). It never changes or deletes existing data, so it is safe on every start, with one exception: the shared dashboard. When a release ships a new default layout, the bootstrap replaces the shared dashboard with it if nobody has changed it since the last default was applied (personal dashboards are never touched; their owners get a "New widgets available" note instead), writes an audit entry (module dashboard, entity dashboardLayout), and logs [bootstrap] shared dashboard: default layout updated. A dashboard someone has edited (a widget added, removed, moved or retitled) is left as it is, logged as edited by an administrator, default layout not applied. The layout last applied is recorded in the dashboard.appliedDefaultLayout setting, which travels with backups.
  2. Encrypts any secret setting still stored in plain text.
  3. If the database has no users at all, creates an administrator from INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD. The address is stored in lower case, the name is set to Administrator, and the account is marked break-glass, so it keeps password sign-in if you later turn on SSO only.

When users already exist, the bootstrap logs [bootstrap] users exist; initial administrator skipped and ignores both variables. When no users exist and either variable is missing, or the password is shorter than 12 characters, it fails with:

[bootstrap] No users exist. Set INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD (at least 12 characters) to create the first administrator.

migrate then exits with an error and the application does not start. Set both values and run docker compose up -d again.

The application never reads the password from anywhere except INITIAL_ADMIN_PASSWORD. Some operators record the initial administrator's password in a file named .initial-admin (mode 600) in the installation directory; upgrades leave a file of that name alone, but the safer place is your password vault.

2.7 First sign-in

  1. Open APP_URL in a browser. You are taken to the sign-in page.
  2. Enter the initial administrator's Email and Password and click Sign in.
The sign-in page with the Email and Password fields and the Sign in button.
Figure 2.1. The sign-in page with the Email and Password fields and the Sign in button.
  1. Open the user menu, choose My profile, and set your Time zone.
  2. Change the initial password: in Settings → Users, open the Administrator account and set a new password (see Users, Groups and Roles). Store it in the vault.

Then remove the initial password from the server:

  1. Delete the value of INITIAL_ADMIN_PASSWORD in .env (leave the line as INITIAL_ADMIN_PASSWORD=).

  2. Recreate the exited migrate container, which otherwise keeps the old value in its configuration:

    docker compose up -d --no-deps --force-recreate migrate
    
  3. Confirm the value is gone:

    docker inspect "$(docker compose ps -aq migrate)" | grep INITIAL_ADMIN_PASSWORD
    

    The command should print "INITIAL_ADMIN_PASSWORD=" with no value.

2.8 Post-installation checklist

Work through this list before you invite users. The chapters named in each row describe the screens.

StepWhereChapter
Initial password changed, removed from .env and from the migrate container.Server, Settings → UsersThis chapter
A copy of .env stored in the password vault.VaultInstallation
Product name, logo and color set.Settings → BrandingPlatform Settings
SMTP configured and Send test email received.Settings → IntegrationsNotifications and Email
Session length and idle timeout reviewed.Settings → SessionsAuthentication and Single Sign-On
LDAP or SAML configured and tested, if used.Settings → IntegrationsAuthentication and Single Sign-On
A second administrator created and marked break-glass, password in the vault.Settings → UsersUsers, Groups and Roles
SSO only turned on, if every user signs in through the identity provider.Settings → IntegrationsAuthentication and Single Sign-On
Groups and roles set up for least privilege.Settings → Groups, Settings → RolesUsers, Groups and Roles
Nightly deploy/backup.sh scheduled, copied off the server, and one restore drill passed.ServerBackup and Restore
Daily audit chain verification scheduled.ServerMonitoring and Troubleshooting
External monitoring of /api/health.Monitoring systemMonitoring and Troubleshooting
Existing registers loaded, if any.APILoading and Importing Data

2.9 Running several instances on one server

Each instance needs its own directory, .env, project name and volumes. In each instance's .env, set:

If you set APP_IMAGE yourself, give each instance a different value; two instances sharing one image name would build over and roll back each other's image.

Only one process can listen on ports 80 and 443, so at most one instance can use the bundled Caddy's default configuration. Put one reverse proxy in front of all instances, or give each instance's Caddy its own port in a Caddyfile.* and docker-compose.override.yml.