# Setup Portal Linux

## Requirements

See the **Portal** section on the [Requirements](/UDiTH%20Portal/Requirements) page, including **Software (Linux)**.

## Installation

We recommend using a guided installation with CAXperts for UDiTH Portal. Please contact `support@caxperts.com` for information.

The Docker images are available at `quay.io/caxperts/portal` and `quay.io/caxperts/caxturn`.

### Reverse Proxy

Please use a reverse proxy in front of Portal. See the [Secure Setup Guidelines](/UDiTH%20Portal/Secure%20Setup%20Guidelines) page.

### Installation Steps

- Ensure that all requirements are met.
- Ensure that MSSQL Server and Keycloak are accessible from the new Portal container.
- Create a container for Portal. A template `docker-compose.yml` file with the most important settings is included below.
  - In comparison with the Windows version, the Docker variant mainly uses environment variables to configure the instance.

### Sample `docker-compose.yml` File

```yml
services:
  portal_demo:
    image: quay.io/caxperts/portal:${PORTAL_VERSION:-latest}
    restart: unless-stopped
    environment:
      - KeycloakAdmin__Name=${KEYCLOAK_ADMIN_USER}
      - KeycloakAdmin__Password_file=${KEYCLOAK_ADMIN_PASSWORD}
      - KeycloakUrl=https://${KEYCLOAK_DOMAIN}${KEYCLOAK_PATH:-/}
      - KeycloakRealm=${KEYCLOAK_REALM}
      - ConnectionStrings__AdminConnection=${PORTAL_CONNECTION_STRING}
      - ServerUrl=https://${PORTAL_DOMAIN}
      - LicenseKey_file=${PORTAL_LICENSEKEY}
    networks:
      - sqlserver
      - external
    volumes:
      - ${PORTAL_IMPORT_FOLDER}:/import
      - ${PORTAL_STORAGE_FOLDER}:/storage
      - ${PORTAL_UPLOAD_FOLDER}:/upload
networks:
  external:
    external: true
    name: external
  sqlserver:
    external: true
    name: sqlserver
```

### Sample `docker-compose.yml` File with CAX Turn for BBV

```yml
services:
  portal_demo:
    image: quay.io/caxperts/portal:${PORTAL_VERSION:-latest}
    restart: unless-stopped
    environment:
      - KeycloakAdmin__Name=${KEYCLOAK_ADMIN_USER}
      - KeycloakAdmin__Password_file=${KEYCLOAK_ADMIN_PASSWORD}
      - KeycloakUrl=https://${KEYCLOAK_DOMAIN}${KEYCLOAK_PATH:-/}
      - KeycloakRealm=${KEYCLOAK_REALM}
      - ConnectionStrings__AdminConnection=${PORTAL_CONNECTION_STRING}
      - ServerUrl=https://${PORTAL_DOMAIN}
      - StreamingConfig__IceConfig__IceServers__0__Urls__0=turn:${TURNSERVER_ADDRESS}:${TURNSERVER_UDP_PORT}
      - StreamingConfig__IceConfig__IceServers__0__CredentialType=password
      - StreamingConfig__IceConfig__IceServers__0__Username=${TURNSERVER_USERNAME}
      - StreamingConfig__IceConfig__IceServers__0__Credential=${TURNSERVER_PASSWORD}
      - LicenseKey_file=${PORTAL_LICENSEKEY}
    networks:
      - sqlserver
      - external
    volumes:
      - ${PORTAL_IMPORT_FOLDER}:/import
      - ${PORTAL_STORAGE_FOLDER}:/storage
      - ${PORTAL_UPLOAD_FOLDER}:/upload
  turn:
    image: quay.io/caxperts/caxturn:${CAXTURN_VERSION:-latest}
    # Port range mapping is very slow, so network_mode: host is used.
    network_mode: host
    environment:
      - CAXTURN_ENABLE_UDP=true
      - CAXTURN_ENABLE_TCP=true
      - CAXTURN_UDP_PORT=${CAXTURN_UDP_PORT}
      - CAXTURN_TCP_PORT=${CAXTURN_TCP_PORT}

      - CAXTURN_MIN_PORT=${CAXTURN_MIN_PORT}
      - CAXTURN_MAX_PORT=${CAXTURN_MAX_PORT}

      - CAXTURN_PUBLIC_IP=${CAXTURN_PUBLIC_IP}
      - CAXTURN_USERNAME=${CAXTURN_USERNAME}
      - CAXTURN_PASSWORD=${CAXTURN_PASSWORD}
    restart: unless-stopped
networks:
  external:
    external: true
    name: external
  sqlserver:
    external: true
    name: sqlserver
```

### Sample `docker-compose.yml` for Keycloak

```yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION:-26.7.2}
    restart: unless-stopped
    environment:
      # >= 26
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN_USER}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      # <26
      KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER}
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_DB: mssql
      KC_DB_URL: "jdbc:sqlserver://${KEYCLOAK_SQLSERVER_ADDRESS:-mssql:1433};databaseName=${KEYCLOAK_SQLSERVER_DATABASENAME};encrypt=true;trustServerCertificate=true;"
      KC_DB_USERNAME: ${KEYCLOAK_SQLSERVER_USER}
      KC_DB_PASSWORD: ${KEYCLOAK_SQLSERVER_PASSWORD}
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      KC_HTTP_ENABLED: true
      KC_PROXY_HEADERS: xforwarded
      KC_HOSTNAME: https://${KEYCLOAK_DOMAIN}${KEYCLOAK_PATH:-/}
      KC_HTTP_RELATIVE_PATH: ${KEYCLOAK_RELATIVE_PATH:-/}
    command: start
    networks:
      - "sqlserver"
      - "external"
    healthcheck:
      test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000${KEYCLOAK_RELATIVE_PATH:-/}/health/live']
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 1m
    volumes: 
      - /root/storage/keycloak/providers:/opt/keycloak/providers
networks:
  external:
    external: true
    name: "external"
  sqlserver:
    external: true
    name: "sqlserver"
```

### Sample `docker-compose.yml` for MSSQL

```yml
services:
 mssql:
    image: mcr.microsoft.com/mssql/server:${MSSQL_VERSION:-2025-latest}
    environment:
      SA_PASSWORD: ${SQLSERVER_SA_PASSWORD}
      ACCEPT_EULA: "Y"
    volumes:
      - ${SQLSERVER_STORAGE_FOLDER}:/var/opt/mssql
    ports:
      - 1433:1433 # Not required, but useful for remote connections. Secure this appropriately.
    restart: unless-stopped
    healthcheck:
      # -C disable checks for encryption
      test: /opt/mssql-tools18/bin/sqlcmd -S localhost -C -U sa -P "$SQLSERVER_SA_PASSWORD" -Q "SELECT 1" -b -o /dev/null
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 10m
networks:
  default:
    external: true
    name: sqlserver
```

### Image Variants and Volume Permissions

The `quay.io/caxperts/portal` image is published with several base-OS variants.
These variants do **not** all run as the same user, so the host directories that
are mounted as volumes (`/import`, `/storage` and `/upload`) must be readable and
writable by the user of the variant you use.

| Tag               | Base OS                          | Runs as        | Required ownership of the host folders |
| ----------------- | -------------------------------- | -------------- | -------------------------------------- |
| `ubuntu`          | Ubuntu 26.04 LTS                 | `root` (uid 0) | No change required                     |
| `ubuntu-chiseled` | Ubuntu 26.04 LTS, chiseled       | uid `1654`     | `chown -R 1654 <folder>`               |
| `alpine`          | Alpine Linux                     | `root` (uid 0) | No change required                     |
| `ubi`             | RHEL 9 (UBI)                     | uid `1001`     | `chown -R 1001 <folder>`               |
| `noble`           | Ubuntu 24.04 LTS *(legacy)*      | `root` (uid 0) | No change required                     |
| `noble-chiseled`  | Ubuntu 24.04 LTS, chiseled *(legacy)* | uid `1654` | `chown -R 1654 <folder>`             |

The `noble` variants are kept for compatibility and will be removed with release
2027.0. New installations should use `ubuntu` or `ubuntu-chiseled`.

The uids of the non-root variants do not need to exist on the host; the numeric
owner is what matters.

All three volumes require **read, write and delete** access:

| Mount      | Purpose                                             |
| ---------- | --------------------------------------------------- |
| `/storage` | Converted models, caches and temporary data          |
| `/import`  | Files picked up for import                           |
| `/upload`  | Files uploaded through the web interface             |

Because deleting a file on Linux is a permission of the *containing directory*,
the container user needs **write and execute** permissions on the directories
themselves, not only on the files inside them.

The image already contains the three directories, owned by the application user
with group `root` and group write permission, and declares them as volumes. A
named or anonymous Docker volume mounted over one of these paths inherits that
ownership, so it needs no preparation. A **bind mount** keeps the ownership of
the host directory and must therefore be prepared as described above.

Example for the `ubuntu-chiseled` variant:

```bash
mkdir -p /srv/udith/storage /srv/udith/import /srv/udith/upload
chown -R 1654:1654 /srv/udith/storage /srv/udith/import /srv/udith/upload
chmod -R u+rwX /srv/udith/storage /srv/udith/import /srv/udith/upload
```

Notes:

- The root-based variants (`ubuntu`, `noble`, `alpine`) write files owned by
  `root` on the host. If you later switch to `ubuntu-chiseled`, `noble-chiseled`
  or `ubi`, the existing content of all three volumes must be re-owned to the new
  uid.
- Network shares (NFS/CIFS) usually ignore `chown`. Mount them with the
  matching `uid=`/`gid=` options instead (for example
  `uid=1654,gid=1654,file_mode=0660,dir_mode=0770` for CIFS). The share must also
  permit deleting files.

### CAXturn Image Variants

The `quay.io/caxperts/caxturn` image is published in three variants. All of them
run as uid `65532` and need no volumes, so the choice only affects what else is
present in the image.

| Tag       | Base OS      | Notes                                             |
| --------- | ------------ | ------------------------------------------------- |
| `scratch` | none         | Smallest image, contains only the server binary   |
| `debian`  | Debian       | Includes a shell for troubleshooting              |
| `alpine`  | Alpine Linux | Includes a shell, smaller than the Debian variant |

## Accessing Portal

Go to the Keycloak installation and update the `defaultadmin` password in the realm you specified.

Then go to `https://<alias/(sub)domain>/Portal` and sign in by using `defaultadmin` and the password you defined.

You can add additional users through the web interface or connect the portal to an SSO provider.

![Keycloak Login](media/Portal_Login.png)

## Configuration

In comparison with the Windows version, the Docker variant is configured mainly through environment variables, optionally backed by Docker secret files.

For the complete list of available settings, and details on how to provide them on both Linux and Windows, see the [Portal Configuration](/UDiTH%20Portal/Portal%20Configuration) page.

## Security

See the [Secure Setup Guidelines](/UDiTH%20Portal/Secure%20Setup%20Guidelines) page.
