# Deployment

## Prequisites

* Docker or Podman (to run the Toolkit Docker image)
* SQL Server (to host the database) (with SQL_Latin1_General_CP1_CI_AS Collation (see Database Section))
* Keycloak (for authentication and authorization)
* Optional: Microsoft Exchange (for email integration)
* Optional: Azure Blob Storage account (if using Azure for blob storage)
* Optional: Nginx or Apache (if using a reverse proxy)
* Optional: Cron (for scheduled tasks, e.g. email polling)
* Optional: PowerBI account (for PowerBI integration)

## Docker/Podman

Toolkit is a Docker container with a set of apps for UDiTH App such as Tickets/Punchlist, Clashes, Lockout/Tagout, and Redlining. You can pull the Toolkit using Docker:

    docker pull quay.io/caxperts/toolkit:latest

or (for the latest stable release)

    docker pull quay.io/caxperts/toolkit:stable

## Database

- The application requires a running SQL Server for its database. 
- The Database should be created with Collation SQL_Latin1_General_CP1_CI_AI.
- Migration scripts are provided that are used to populate the database schema.
- Credentials for a database owner account are required.

![Collation setup](media/admin_guide_database_setup.png)

## Local or Azure blob storage

Admin can specify an environment variables that will change how the application saves blobs. Those variables are:

- STORAGE_TYPE - accepts only 2 values, "local" or "azure".
- STORAGE_PATH - when "local" type used, defines an absolute path to storage on the hardware the server is run, e.g. "/home/user/localStorage".
- AZURE_STORAGE_CONNECTION_STRING - connection string given by Azure Blob Storage.
- AZURE_STORAGE_CONNECTION_DOMAIN - the domain defined by Azure Blob Storage.

## Reverse Proxy Setup

When configuring a reverse proxy like Nginx or Apache, you must increase the size of headers that can be passed to accommodate larger requests. Below is an example configuration snippet for Nginx to achieve this:

    server {
    ...
    proxy_buffers 16 16k; # Sets 16 buffers of 16KB each

    proxy_buffer_size 32k; # Increases the initial buffer size to 32KB

    proxy_busy_buffers_size 64k; # Sets the busy buffer size to 64KB
    ...
    }

## Keycloak Clients and Scopes Setup

To integrate with Keycloak, you need to configure a custom client and associated scopes. Below are the step-by-step instructions for setting up the client and its scopes.

### Client scope configuration
Create a client scope named "groups" with the following settings:

#### Scope Settings

- **Name:** groups
- **Description:** "description"
- **Type:** None
- **Protocol:** OpenID Connect
- **Display on Consent Screen:** On
- **Consent screen text:** if required
- **Include in Token Scope:** On_
- **Display Order:** Leave empty

#### Mappers

1. Add a predefined mapper.
2. Select the "groups" mapper to include group information in the token.

#### Scope Details

- Leave empty by default unless specific mappings are required.

### Client Configuration

Create a custom client in Keycloak with the following settings to suit your application’s needs.

#### General Settings

- **Client Type:**
  - Leave as default or select "OpenID Connect" if prompted.

#### Capability Configuration

- **Client Authentication:**

  - Off (If confidential access required mark it True)

- **Authorization:**

  - Enable or disable based on the client’s specific requirements.

- **Authentication Flow:**
  - **Standard Flow:** Enabled
  - **Direct Access Grants:** Enabled
  - Enable additional flows as needed based on your use case.

#### Login Settings

- **Root URL:**
  - Leave empty.
- **Home URL:**
  - Leave empty.
- **Valid Redirect URIs:**
  - https://your.domain.com/* (Adjust to match your domain and paths).
- **Valid Post-Logout Redirect URIs:**
  - - (Use defaults or specify custom URIs as required).
- **Web Origins:**
  - - (Use defaults or specify allowed origins explicitly).### Client Scopes Configuration

### User groups
As a minimal setup, a user group named "Admin" exactly in that spelling is required.
A typical user groups setup would look like this: 
![Example of user groups configuration in Keycloak](media/deployment_guide_typical_user_groups.png)



## Email Configuration

The application uses two separate email accounts for different purposes:

### Support Email (Interactions)

The **SUPPORT_EMAIL** account is used for the ticket/interaction system. It serves as the single mailbox for:

- **Inbox scanning**: A cron job polls this mailbox every minute for new incoming emails and automatically creates tickets from them.
- **Sending replies**: When a user replies to a ticket from the Toolkit UI, the reply is sent from this email address.
- **Conversation threading**: The Toolkit UI fetches email conversation chains from this mailbox.

The relevant environment variables are: `AZURE_AD_TENANT_ID`, `SUPPORT_EMAIL`, `SUPPORT_CLIENT_ID`, and `SUPPORT_CLIENT_SECRET`.

To enable inbox scanning and automatic ticket creation, set `ENABLE_CRON_TICKET_CREATION=true`. When set to `false` or left unset, the cron job for ticket creation will not run, and incoming emails will not be processed into tickets.

### Workflow Email

The **WORKFLOW_EMAIL** account is used exclusively for outbound workflow notification emails. When a workflow step is accepted and advances to the next step, notification emails are sent to the responsible users from this address. This mailbox is never scanned for incoming emails and is not used for ticket creation.

The relevant environment variables are: `AZURE_AD_TENANT_ID`, `WORKFLOW_EMAIL`, `WORKFLOW_CLIENT_ID`, and `WORKFLOW_CLIENT_SECRET`.

### Azure AD App Registration

Both email accounts require an Azure AD app registration with Microsoft Graph `Mail.Send` and `Mail.Read` permissions (application-level) for their respective mailboxes. The `AZURE_AD_TENANT_ID` is shared between both accounts.

For detailed support in setting up the Azure AD app registrations, please contact us directly. If the toolkit should also be able to send notifications via the Keycloak service client, a Keycloak client is required (i.e. SERVICE_CLIENT_ID and SERVICE_CLIENT_SECRET as described in the following section).


## Setup for Keycloak role sync for with Service Client
To synchronize roles from Keycloak with Toolkit (and also to send notifcations by email), a service client is required. This service client is triggered with the Keycloak group sync cron job. The schedule of this cron job is defined in KC_GROUP_SYNC_CRON.

### Creating the service client in Keycloak

1. Navigate to tab 'Clients'

2. Create a new client:

3. General Settings:

   - Client Type: OpenID Connect
   - Client ID: provide client id name that you want to use
   - Name: Optional
   - Description: Optional

4. Capability config:
   - Client authentication: ON
   - Authorization: OFF
   - Only Service accounts roles is checked

### Adding Groups scope to service client

1. Navigate to tab 'Clients'

2. Choose your newly created client
Here named "serviceclient": 
![Keycloak Panel for service client](media/deployment-keycloak-service-client.png)

3. Navigate to tab 'Client Scopes'

4. Add client scope

5. Find in scopes the following:

   - Name: 'groups'
   - Protocol: 'OpenID Connect'

6. Add groups scope as 'default'

### Assigning account roles to the Keycloak service client and groups to corresponding service client user

1. Navigate to the newly created client id.

2. Navigate to 'Service accounts roles' tab.

3. Assign the service account roles as specified in the picture below: 

![Keycloak service client privileges](media/keycloak_hidden_user.png)

4. Make sure that user account corresponding to the service client (in this example called 'service-account-serviceclient') is member of the Admin group via the link hightlighted in the picture above. 

### Keycloak service client in environment variables
SERVICE_CLIENT_ID is your service client name, SERVICE_CLIENT_SECRET can taken from the "Credential" tab, labelled Client secret. 

## Comprehensive Environment Variables List

(Variables marked as **bold** are the minimum variables required to start Toolkit)

**DATABASE_URL** = <sql_provider>:<port>;database=<database>;user=<user>;password=<password>trustServerCertificate=true

**KEYCLOAK_BASE_URL** = <url> e.g. https://cax.authudith.com

**KEYCLOAK_ID** = <id> e.g. connector

**KEYCLOAK_ISSUER** = <issuer> e.g. https://cax.authudith.com/realms/apps-demo

**KEYCLOAK_REALM** = <realm> e.g. apps-demo

**KEYCLOAK_REDIRECT_URL** = <appcontrol_url> e.g. https://udithappcontrol.com

**SERVICE_CLIENT_ID**="<id>" // Keycloak client id created specifically for cron, required for group synchronization

**SERVICE_CLIENT_SECRET**="<secret>" // Existing keycloak client secret.

**NEXTAUTH_URL** = <appcontrol_url> e.g. https://udithappcontrol.com

**NEXTAUTH_SECRET** = "any_string_that_represents_secret" // required by nextauth to secure authentication via application

REDIRECT_WEBSERVICES_URL="<upv_web_based_redirect_url>", required for usage in browser-based viewing

AZURE_AD_TENANT_ID=""000000-0000-0000-0000-000000000"

SUPPORT_EMAIL="<email>@<domain>" // Email used for interaction inbox scanning, ticket creation, and sending replies, required for inbound emails

SUPPORT_CLIENT_ID="000000-00-0000-0000-000000000" // Azure AD app client ID for the support email, required for inbound emails

SUPPORT_CLIENT_SECRET="your_secret" // Azure AD app client secret for the support email, required for inbound emails

WORKFLOW_EMAIL="<email>@<domain>" // Email used for outbound workflow notification emails only (never scanned), required for outbound emails

WORKFLOW_CLIENT_ID="000000-00-0000-0000-000000000" // Azure AD app client ID for the workflow email, required for outbound emails

WORKFLOW_CLIENT_SECRET="your_secret" // Azure AD app client secret for the workflow email, required for outbound emails

KC_GROUP_SYNC_CRON=0 3 \* \* \* // this variable is used to run the user roles sync with keyccloak, default is set to every day at 3 am  default.

ENABLE_CRON=<boolean> // 'true' if you want cron job to be running (default), otherwise leave it empty or 'false'

ENABLE_CRON_TICKET_CREATION=<boolean> // 'true' to enable automatic ticket creation from incoming emails, otherwise 'false'

DEVICE_ID = <device_id> e.g. AppControls-udithappcontrol.com, Displayed to UDiTH when authenticating

LOGIN_HEADER = <header_displayed_on_login> e.g. CAXperts

SIGNATURE_REPORT_THRESHOLD = <number> e.g. 1536, represents image size threshold in bytes to filter low quality signature like "dot"

STORAGE_PATH= <absolute_path_to_storage> e.g."/app/storage". If using docker volumes make sure that this path matches the definition there, e.g. use "/app/storage" if it says "toolkit_volume:/app/storage" in the docker stack definition.

STORAGE_TYPE= <storage_type> takes value of "local" || "azure"

AZURE_STORAGE_CONNECTION_STRING = <string> // connection string given by Azure Blob Storage.

AZURE_STORAGE_CONNECTION_DOMAIN = <domain> e.g. caxperts.blob.core.windows.net // the domain defined by Azure Blob Storage.

NEXT_PUBLIC_TICKET_PREFIX="PI-"

PORT=3000

HOST=0.0.0.0

POWERBI_LOGO = <URL_to_logo> it will be displayed on powerbi page

POWERBI_TENANT_ID=<uuid>

POWERBI_CLIENT_ID=<uuid>

POWERBI_SECRET=<secret>


## Browser Authentication in UDiTH Mode

By default, when the Toolkit runs inside the UDiTH application, it uses UDiTH's authentication context for single sign-on. However, this can be incompatible with reverse proxies like Azure EasyAuth that handle authentication at the HTTP level.

### Environment Variable

To enable browser-based authentication even when running inside UPV, set:

```bash
USE_BROWSER_AUTH_IN_UPV=true
```

### Behavior

- **Default (USE_BROWSER_AUTH_IN_UPV=false or unset)**: UDiTH App uses in-app authentication context
- **Enabled (USE_BROWSER_AUTH_IN_UPV=true)**: UDiTH App uses browser authentication flow (same as web browser)

### Use Cases

This flag is particularly useful for:

- **Azure EasyAuth Integration**: When placing Azure App Service EasyAuth in front of the application
- **Corporate Proxy Authentication**: When authentication is handled by enterprise reverse proxies
- **Centralized Identity Management**: When all authentication should flow through a single identity provider endpoint

### Configuration Example

For Azure App Service with EasyAuth:

```bash
# Enable browser auth in UDiTH App
USE_BROWSER_AUTH_IN_UPV=true

# Standard Keycloak configuration (same as browser deployment)
KEYCLOAK_ID=your-client-id
KEYCLOAK_SECRET=your-client-secret
KEYCLOAK_ISSUER=https://your-keycloak-server/realms/your-realm
```

### Testing

1. Deploy with `USE_BROWSER_AUTH_IN_UPV=true`
2. Open the application in UDiTH App
3. Verify that clicking "Sign In" redirects to browser authentication instead of opening UDiTH App authentication dialog
4. Confirm that authentication works through your proxy/EasyAuth setup

## Breaking Change

1. When using STORAGE_TYPE=local with mapped container volume:
   - Please make sure that the mapped volume is owned by user 1000. Otherwise, on blob saving 'permission denied' will be raised.


## Multi-tenancy (Path-Based Multi-tenancy) [Experimental]

### Overview

This feature enables deployment of multiple Toolkit instances on a single domain or subdomain, each accessible via a unique subpath (e.g., `/instance1`, `/instance2`).

**Example URLs:**
- https://mydomain.com/instance1
- https://mydomain.com/instance2
- https://sub.mydomain.com/instance1
- https://sub.mydomain.com/instance2

Each instance can have its own Microsoft SQL Server database and Keycloak Identity Provider. It is strongly recommended to use a separate database for each instance to fully benefit from multi-tenancy.

### Use Cases

**1. Segregated Access Control**

Toolkit does not natively support multi-tenancy. Deploying multiple instances allows you to restrict access to data (e.g., punchlist items) by user group. For example, users in group A only see items relevant to category A, while group B sees category B.

**2. Simplified Deployment**

With multi-tenancy, IT admins only need to acquire a single domain and SSH key. Previously, each new instance required a separate domain setup, which was time-consuming and error-prone. Now, one domain can host multiple instances, streamlining the process.

### Requirements

- Recent Toolkit Docker image (e.g., `toolkit:2.90`).
- Custom Nginx reverse proxy Docker image (e.g., `toolkit:nginx-2.9`).
- Separate SQL Server databases for each instance (recommended).
- At least one Keycloak Identity Provider (shared sessions possible).
- Modified docker-compose file for multi-instance deployment.
- Adjusted environment variables for each instance in the docker-compose file.

### Example docker-compose.yml

Below is an example docker-compose file deploying one Nginx reverse proxy and two Toolkit instances. The Nginx container auto-generates routing based on the `INSTANCES` environment variable. SQL Server and Keycloak are assumed to be running externally.

```yaml
version: '3.8'

services:
  toolkit_entrypoint:
    image: quay.io/caxperts/toolkit:nginx-2.90
    #ports: <- only set if no reverse proxy e.g. nginx proxy manager is used
    #  - 80:80 
    environment:
      INSTANCES: "instance1,instance2"
    depends_on:
      - instance1
      - instance2
    restart: unless-stopped
    # volumes:
    #   - ./index.html:/usr/share/nginx/html/index.html:ro
    networks:
      - toolkit_network
      - external

  instance1:
    image: quay.io/caxperts/toolkit:2.90
    container_name: toolkit-instance1
    environment:
      BASE_PATH: "/instance1"
      INSTANCE_NAME: "instance1"
      NEXT_PUBLIC_BASE_PATH: "/instance1"
      DATABASE_URL: sqlserver://sqlServerEngine:1433;database=toolkit_db1;user=toolkit_user_db1;password=changeme;trustServerCertificate=true;MultipleActiveResultSets=true
      KEYCLOAK_BASE_URL: https://keycloakdomain.com
      KEYCLOAK_ID: connector
      KEYCLOAK_ISSUER: https://keycloakdomain.com/realms/toolkit_realm
      KEYCLOAK_REALM: toolkit_realm
      NEXTAUTH_URL: https://toolkitBaseUrl.com/instance1/api/auth
      NEXTAUTH_SECRET: demoSecret1
      STORAGE_PATH: /app/storage # must be ABSOLUTE and match the volume mount below
      STORAGE_TYPE: local
      NODE_ENV: production
      BODY_SIZE_LIMIT: 200mb
      SERVICE_CLIENT_ID: serviceclient
      SERVICE_CLIENT_SECRET: demoSecret2
      SERVICE_ACCOUNT_SECRET: demoSecret2
      DEVICE_ID: Toolkit_instance1
      KEYCLOAK_REDIRECT_URL: https://toolkitBaseUrl.com/instance1
      SUPPORT_CLIENT_ID: 00000000-0000-0000-0000-000000000000
      SUPPORT_CLIENT_SECRET: demoSecret3
      SUPPORT_EMAIL: support@company-name.com
      AZURE_AD_TENANT_ID: 00000000-0000-0000-0000-000000000001
      KC_GROUP_SYNC_CRON: "*/10 * * * *"
      # Email notification
      WORKFLOW_EMAIL: noreply@company-name.com
      WORKFLOW_CLIENT_ID: 00000000-0000-0000-0000-00000000002
      WORKFLOW_CLIENT_SECRET: demoSecret4
    volumes:
      - instance1-storage:/app/storage
    restart: unless-stopped
    networks:
      - toolkit_network
      - external

  instance2:
    image: quay.io/caxperts/toolkit:2.90
    container_name: toolkit-instance2
    environment:
      BASE_PATH: "/instance2"
      INSTANCE_NAME: "instance2"
      NEXT_PUBLIC_BASE_PATH: "/instance2"
      DATABASE_URL: sqlserver://sqlServerEngine:1433;database=toolkit_db2;user=toolkit_user_db2;password=changeme;trustServerCertificate=true;MultipleActiveResultSets=true
      KEYCLOAK_BASE_URL: https://keycloakdomain.com
      KEYCLOAK_ID: connector
      KEYCLOAK_ISSUER: https://keycloakdomain.com/realms/toolkit_realm
      KEYCLOAK_REALM: toolkit_realm
      NEXTAUTH_URL: https://toolkitBaseUrl.com/instance2/api/auth
      NEXTAUTH_SECRET: demoSecret1
      STORAGE_PATH: /app/storage # must be ABSOLUTE and match the volume mount below
      STORAGE_TYPE: local
      NODE_ENV: production
      BODY_SIZE_LIMIT: 200mb
      SERVICE_CLIENT_ID: serviceclient
      SERVICE_CLIENT_SECRET: demoSecret2
      SERVICE_ACCOUNT_SECRET: demoSecret2
      DEVICE_ID: Toolkit_instance2
      KEYCLOAK_REDIRECT_URL: https://toolkitBaseUrl.com/instance2
      SUPPORT_CLIENT_ID: 00000000-0000-0000-0000-000000000000
      SUPPORT_CLIENT_SECRET: demoSecret3
      SUPPORT_EMAIL: support@company-name.com
      AZURE_AD_TENANT_ID: 00000000-0000-0000-0000-000000000001
      KC_GROUP_SYNC_CRON: "*/10 * * * *"
      # Email notification
      WORKFLOW_EMAIL: noreply@company-name.com
      WORKFLOW_CLIENT_ID: 00000000-0000-0000-0000-00000000002
      WORKFLOW_CLIENT_SECRET: demoSecret4
    volumes:
      - instance2-storage:/app/storage
    restart: unless-stopped
    networks:
      - toolkit_network

networks: # should be created seperately 
  toolkit_network:
    external: true # set to false if the network should be auto-created with docker-compose up (not recommended)
    name: "toolkit_network"
  external:
    external: true # set to false if the network should be auto-created with docker-compose up (not recommended)
    name: "external"

volumes:
  instance1-storage:
  instance2-storage:
```

### Important Notes

1. Toolkit instance service names (for example instance2 as shown in above docker-compose file) must be unique for the specified docker-network (here; toolkit_network and external) and **MUST** correlate to the values specified in the **INSTANCES** variable in the nginx service. So if you set INSTANCES=app1,app2,app3,... then the service names of the toolkit instances in the docker-compose file must also be app1:,app2:,app3:,... Apart from that rules you can set any name like foo or bar for the individual instances.
2. The Nginx container, Toolkit instances, and SQL Server container must be on the same Docker network (e.g., `toolkit_network`) for proper domain resolution.
3. If using a reverse proxy (e.g., Nginx Proxy Manager), ensure the Nginx container is on the same network for accessibility.
4. All `KEYCLOAK_REDIRECT_URL` values must be set as "Valid redirect URIs" and "Valid post logout redirect URIs" in your Keycloak realm for the client specified in `KEYCLOAK_ID`.
5. Each Toolkit instance requires a valid license and consumes one session per user. A user logged into two instances uses two sessions.

### Multi-tenancy on the .NET stack

Toolkit images from the .NET release line (one container serving the API, the web UI and the
background worker) support the same path-based multi-tenancy, configured the same way. The
differences an administrator needs to know:

**1. Use the matching Nginx image.** Multi-tenancy on the .NET stack needs the Nginx image
built for it, tagged `nginx-<version>` in the same repository as the Toolkit image (for
example `quay.io/caxperts/toolkit_internal:nginx-2.90_net`). Deploy the two with matching
tags.

> **Do not point the older Nginx image at a .NET Toolkit image.** The older image rewrote the
> web UI's responses to insert the subpath; the .NET UI already handles the subpath itself, so
> both would apply it and every request would fail with a doubled prefix such as
> `/instance1/instance1/api/...`.

**2. The instance's subpath.** `BASE_PATH: "/instance1"` continues to work unchanged, so an
existing multi-instance docker-compose file needs no edit for the subpath. The variable the
new image reads natively is `App__BasePath`; `NEXT_PUBLIC_BASE_PATH` is no longer needed
(nothing is baked into the UI at build time any more) but is accepted as a fallback.

**3. The container port is unchanged.** The .NET container listens on **3000**, the same port
the Next.js image used, so a hand-written proxy configuration or a published port mapping keeps
working as it is.

**Running the new stack alongside the old one.** Docker publishes every service name as a
hostname on each network the container joins, so two stacks that both contain services named
`instance1`, `instance2` and the same Nginx service leave those names ambiguous — and the new
proxy will happily serve the *old* application under the new URL. Give the new stack unique
service names, and keep the public URLs by writing each `INSTANCES` entry as
`subpath:servicename`:

```yaml
  toolkit_net_entrypoint:            # unique Nginx service name
    environment:
      INSTANCES: "instance1:net_instance1,instance2:net_instance2"
  net_instance1:                     # unique instance service name, still served at /instance1
    environment:
      App__BasePath: /instance1
```

Point Nginx Proxy Manager's **Forward Hostname / IP** at the new Nginx service name. Once the
old stack is retired the plain `INSTANCES: "instance1,instance2"` form can be restored.

**4. One container per instance.** The background worker is bundled, so the separate cron
service disappears. Leave `RUN_WORKER: "true"` on **every** instance, including when they share
one Keycloak realm: the worker reads the shared realm but writes the users and roles into *that
instance's own* database, so an instance without a worker never learns who its users are and
group-based access there stays empty. Several instances against one realm means duplicated
reads, not conflicting writes.

The Keycloak sync schedule is now an interval in minutes rather than a cron expression. A legacy
`KC_GROUP_SYNC_CRON: "*/10 * * * *"` is understood and carried over as 10 minutes; a fixed time
such as `0 3 * * *` cannot be expressed as an interval, so set
`Worker__KeycloakSyncIntervalMinutes` yourself in that case (the container logs a warning and
otherwise syncs every 30 minutes).

**5. Database connection string.** `DATABASE_URL` is not read by the .NET image. Convert it by
hand, per instance:

```
DATABASE_URL: sqlserver://mssql:1433;database=toolkit_instance1;user=toolkit_user;password=***;trustServerCertificate=true
```

becomes

```
ConnectionStrings__DefaultConnection: Server=mssql,1433;Database=toolkit_instance1;User Id=toolkit_user;Password=***;TrustServerCertificate=true;MultipleActiveResultSets=true
```

Each instance keeps its own database, its own storage volume and its own
`KEYCLOAK_REDIRECT_URL` — unchanged from above, and notes 1-5 above all still apply. The
Toolkit repository contains a ready-to-run two-instance example,
`docker-compose.multi-instance-net.yml`.

**Links inside emails.** `${serverAddress}` in mail templates and custom code — the base of
every ticket link the app sends out — comes from `NEXTAUTH_URL` as before (its `/api/auth`
suffix is dropped), or from `App__BaseUrl` if you prefer the new name. Keep it per instance, the
same way the old stack file spelled it: `https://toolkitBaseUrl.com/instance1`. If it is left at
the bare domain the instance path is added automatically, so an emailed link still opens the
right instance.

**Compression note.** The older setup had to disable gzip compression to rewrite responses.
The .NET setup does not, so compression is enabled again and the UI loads noticeably less data
over the network.

#### Migration checklist

To move an existing multi-instance deployment onto the .NET image, this is the whole list:

| | Change | How many |
|---|---|---|
| 1 | The image tag on the Toolkit services **and** on the Nginx service (`nginx-<same version>`). Using one stack variable for both — `…:${TOOLKIT_IMAGE_TAG}` and `…:nginx-${TOOLKIT_IMAGE_TAG}` — makes this a single edit. | 1 |
| 2 | `DATABASE_URL` → `ConnectionStrings__DefaultConnection` (note 5 above). | one per instance |
| 3 | **Check** that `STORAGE_PATH` is an absolute path matching the volume mount, e.g. `/app/storage`. A relative value still works — the container resolves it the same way the old image did and logs what it resolved to — but spell it out. | check only |
| 4 | **Check** the Keycloak client's "Valid redirect URIs". The web UI now returns to `/instanceN/auth/callback` instead of `/instanceN/api/auth/...`, so a wildcard such as `https://your.domain.com/*` already covers it and needs no change; a narrower entry does. | usually none |

Everything else in an existing stack file keeps working as it is, including `BASE_PATH`,
all `KEYCLOAK_*`, `SERVICE_CLIENT_*`, `WORKFLOW_*`, `SUPPORT_*`, `HASH_SECRET`, `ENABLE_CRON`,
`NEXT_PUBLIC_TICKET_PREFIX` and `NEXTAUTH_URL`. Variables that no longer do anything
(`NEXTAUTH_SECRET`, `BODY_SIZE_LIMIT`, `DEVICE_ID`, `INSTANCE_NAME`, `NODE_ENV`,
`STORAGE_TYPE`, `SERVICE_ACCOUNT_SECRET`, …) can be left in place; the container lists them on
startup so you can clean them up later.

To see exactly what a stack file resolves to **before** deploying it, without starting anything:

```bash
docker run --rm --env-file instance1.env quay.io/caxperts/toolkit:<tag> --translate-only
```

It prints every setting the container resolved, with secrets masked, and warns about anything it
could not translate — including a redirect URL or email link base that is missing its instance
subpath.

**Licensing.** Each instance still consumes one seat per signed-in user, so a user working in two
instances holds two seats, exactly as before. Copy `HASH_SECRET` across unchanged: it is part of
how a seat is identified, and a different value makes the instance look like a new server, so
seats held under the old value stay reserved until they expire.


### Configuring Nginx Proxy Manager

If you use Nginx Proxy Manager (recommended) instead of directly exposing the nginx service e.g. via setting the PORT section to Port: -80:80 then make sure the following settings are set and match the values specified in the docker-compose file:

*Note:* This section assumes you are already using Nginx Proxy Manager to host a single-instance deployment of the toolkit and therefore only covers the necessary changes to migrate to a multi-tenant deployment.

The following needs to changed in the Proxy Host:

**Forward Hostname / IP**: toolkit_entrypoint <- must be the same value as the service name of the nginx service in the docker-compose file

**Forward Port**: 80 <- this is the port the nginx service listens on

The rest is the same as for the single instance deployment e.g. the 
**Custom Nginx Configuration** is also set to:
```
proxy_buffers 16 16k;
proxy_buffer_size 32k;
proxy_busy_buffers_size 64k;
```

### Custom Nginx landing page

Per default a simple landing page containing links to all available instances is created that looks like this:

<img src="media/deployment_guide_nginxLandingPage_default.png" width="500">

In order to set your own index.html file that for example contains your corporate identity or an automatic redirect to a specific instance do the following.

Put your custom index.html file in the same folder as the docker-compose.yml file. Than uncomment the volume mapping in the docker-compose.yml file: 

before -> leads to generation of default landing page
```
# volumes:
    #   - ./index.html:/usr/share/nginx/html/index.html:ro
```
after -> your specified index.html file is used instead of the auto-generated one:

```
volumes:
      - ./index.html:/usr/share/nginx/html/index.html:ro
```

Here is an example how this can look like:

<img src="media/deployment_guide_nginxLandingPage_custom.png" width="500">





