Installation & Deployment

Installing the Provision Portal UI — an Angular single-page application served by Nginx, which also reverse-proxies /prov-api/ to the Provision Portal backend.

1. Overview

This guide deploys one container: the Provision Portal UI, whose TLS pair ships in the Provision Portal UI folder on FT_DISK. The image is built from docker/Dockerfile on top of nginx:alpine; it copies the pre-built Angular application (dist/provision-portal_angular/browser) to /usr/share/nginx/html/provision-portal and serves it. There is no Node.js in the runtime image. The deployment method is Docker (Compose or docker run) — this repository does not build a distribution archive.

It is a plain Compose service on the ft-network bridge, deployed from /usr/local/ft-system/ under the Compose project name ft-system — the same layout as every deployment guide in the Friendly Tech portal. The Provision Portal UI folder on FT_DISK carries a single-service compose.yml in that layout, for a host that runs this service on its own, plus the TLS pair this container mounts. On a host running the full FT System stack the same service block is already part of the stack compose.yml from the FT System MySQL Stack / FT System Oracle Stack folder — use that one, and take the shared root .env from the same folder.

The Provision Portal backend (the Spring Boot / Flowable service served at the /prov-portal/ context path) is assumed to be already deployed and reachable; it is not deployed by this guide — see Provision Portal API — Installation & Deployment. The two meet at exactly one setting, FT_PROV_API_URL (Pointing at the Backend): the browser never talks to the backend directly, it calls the same-origin path /prov-api/, and Nginx proxies that to the URL in FT_PROV_API_URL.

2. Prerequisites

2.1. Host Requirements

Component Minimum Recommended Notes

Docker Engine

19.03

verify with DevOps

docker/docker-compose.yml declares Compose file format 3.9, which requires Engine 19.03 or newer.

Docker Compose

v2 (docker compose)

v2

All commands in this guide use the Compose v2 CLI plugin syntax.

RAM

verify with DevOps

verify with DevOps

Not pinned in this repository — docker/docker-compose.yml sets no deploy.resources limits.

Disk

verify with DevOps

verify with DevOps

Sized by the nginx:alpine image plus the static Angular build.

Node.js is required only to produce the Angular build from source (see [build-from-source]). The runtime image is nginx:alpine and contains jq, bash and gettext — no Node.js.

2.2. Required External Dependencies

These services must be installed, running, and reachable from this host before the Provision Portal UI starts. None of them are deployed by this guide.

Component Minimum Version Why It Is Needed Port Required

Provision Portal backend

verify with the backend owner

Serves every API call the SPA makes (authentication, provisioning, device status, logs, user management) at the /prov-portal/ context path. Without it the UI loads but every request through /prov-api/ fails, and Nginx will not start at all if the hostname does not resolve.

8080 on the shared bridge, or 8091 published across hosts (PROV_API_HTTP_PORT). Taken from FT_PROV_API_URL; the entrypoint fallback baked into the image (http://prov-api:8880/prov-portal/) applies only when the variable is unset.

Yes

Registry reachability is not a runtime dependency of this container and is therefore not listed above; it is covered by Registry Access and Registry Authentication.

The backend in turn reaches the Friendly TR-069 ACS and its database. Those are never contacted by the browser or by this container.

2.3. Supported Operating Systems

Deployment Operating system

Docker (Compose or docker run)

Linux (recommended), macOS, or Windows with WSL2

2.4. Registry Access

Network access to the hub.friendly-tech.com Docker registry (or offline image archives — see Offline Servers).

Read-only pull credentials for the registry are issued by the DevOps team; request them before starting. They are sufficient for installation and upgrades.

Access to FT_DISK — Provision Portal UI for compose.yml and the self-signed TLS pair; the shared root .env comes from the FT System MySQL Stack / FT System Oracle Stack folder, or is written from the three values in Environment Configuration. docker/docker-compose.yml in this repository is a local build-and-run file, not the deployment descriptor.

3. Network Requirements

Destination Port Protocol Purpose

Provision Portal backend (host taken from FT_PROV_API_URL)

8080 on the shared bridge, 8091 published across hosts — whatever FT_PROV_API_URL contains. Across hosts it must match PROV_API_HTTP_PORT on the backend host

HTTP

Nginx proxy_pass for the /prov-api/ location — every API call from the browser.

hub.friendly-tech.com

443

HTTPS

Pulling the ui/provision-portal image. Not required on offline hosts.

3.1. Docker Networking

provision-portal joins the ft-network bridge of the ft-system Compose project, together with every other FT service on the host. It reaches the backend by whatever hostname FT_PROV_API_URL contains, resolved from inside the container — the provision-api service name on this host, an IP address across hosts; see Pointing at the Backend.

localhost and 127.0.0.1 never work here — inside the container they point at the container itself.

4. Registry Authentication

All Provision Portal UI images are pulled from hub.friendly-tech.com. Authenticate once per host before the first docker compose up.

docker login hub.friendly-tech.com

Enter the read-only pull credentials when prompted. Request the user name and password from the DevOps team — they are not published in this documentation.

The read-only account provides pull-only access to the published images. It cannot push. For elevated Harbor access (push rights, a dedicated robot token), contact the DevOps team.

Alternatively, log in non-interactively, reading the password from a file that is not committed anywhere:

docker login hub.friendly-tech.com -u <readonly-user> --password-stdin < ~/.ft-harbor-password

Verify authentication:

docker info | grep -A 5 Registry

Then pull the image:

docker pull hub.friendly-tech.com/ui/provision-portal:<tag>

CI publishes the image for linux/amd64 only (platforms: 'linux/amd64' in .github/workflows/build-callable.yml, and the same platform in docker/docker-push.sh).

Building the image from source. Developers who cannot use a published image build it locally. The Dockerfile is runtime-only: it copies an already-produced dist/, so the Angular build runs first.

git clone <repository-url> provision-portal_angular
cd provision-portal_angular

npm ci
npm run build            # emits dist/provision-portal_angular/browser

docker build -f docker/Dockerfile -t provision-portal-ui:local .

CI runs ng build --project=provision-portal_angular -c=production --base-href /provision-portal/ after npm install --legacy-peer-deps. The Node.js version is not pinned in this repository (no engines field, no setup-node step) — verify with the build owner before choosing one.

To build and push the way docker/docker-push.sh does:

docker buildx create --name multi --driver docker-container --use
docker buildx build --platform linux/amd64 --push \
  -f docker/Dockerfile -t hub.friendly-tech.com/ui/provision-portal:<tag> .

4.1. Offline Servers

When the Provision Portal UI host cannot reach hub.friendly-tech.com, pull the image on a machine that does have registry access, export it to an archive, transfer it, and load it on the offline host.

The connected machine can run Linux, macOS, or Windows — commands are given for both shells below. It does not need to be the same platform as the offline host.

An explicit --platform matching the offline host’s architecture is required. Without --platform, docker pull selects the host architecture, which may not match the target. On an Apple Silicon (arm64) Mac or an arm64 Windows machine without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The examples below use linux/amd64; step 1 shows how to read the correct value off the offline host. Only linux/amd64 is published by CI; do not rely on --platform linux/arm64 unless the release owner confirms an arm64 build exists.

  1. On the offline host, find out which architecture it runs — this is the value you will pass as PLATFORM below. Ask Docker itself, since it reports what the daemon will actually accept:

    docker version --format '{{.Server.Arch}}'

    If Docker is not installed there yet, use the operating system instead — uname -m on Linux, or echo $env:PROCESSOR_ARCHITECTURE in PowerShell on Windows. Map the result:

    docker version reports uname -m / Windows reports Use as PLATFORM

    amd64

    x86_64 / AMD64

    linux/amd64

    arm64

    aarch64 / ARM64

    linux/arm64

    On Windows with Docker Desktop, {{.Server.Arch}} reports the architecture of the Linux VM that actually runs the containers — which is the value you want, not the Windows host’s own architecture.

  2. On a machine with registry access, log in. Run this on its own — it prompts for a password, so anything pasted after it on the same go would be swallowed as input:

    docker login hub.friendly-tech.com
  3. Pull and export the image. Paste the whole block as-is; the only lines to change are PLATFORM and TAG on top.

    Linux / macOS (bash):

    PLATFORM=linux/amd64
    TAG=<tag>
    
    docker pull --platform "$PLATFORM" "hub.friendly-tech.com/ui/provision-portal:$TAG"
    docker save "hub.friendly-tech.com/ui/provision-portal:$TAG" | gzip > "provision-portal-ui-$TAG.tar.gz"

    Windows (PowerShell):

    $PLATFORM = "linux/amd64"
    $TAG = "<tag>"
    
    docker pull --platform $PLATFORM "hub.friendly-tech.com/ui/provision-portal:$TAG"
    docker save -o "provision-portal-ui-$TAG.tar" "hub.friendly-tech.com/ui/provision-portal:$TAG"

    On Windows, always write the archive with docker save -o <file>. Piping or redirecting docker save from PowerShell (docker save …​ > file.tar) corrupts the archive, because the PowerShell pipeline re-encodes the stream as text instead of passing raw bytes. docker load then fails with unexpected EOF or invalid tar header. To compress for transfer, use the built-in tar.exe (Windows 10 1803+ / Server 2019+): tar.exe -czf provision-portal-ui.tar.gz provision-portal-ui-$TAG.tar.

  4. Transfer the archive to the offline host, together with compose.yml and the ssl/ pair (friendly.crt, friendly.key) from the Provision Portal UI folder, and the root .env — see Directory Layout.

  5. On the offline host, load the archive and start the service. Set TAG to the same value you used above:

    Linux (bash):

    TAG=<tag>
    
    gzip -dc "provision-portal-ui-$TAG.tar.gz" | docker load
    cd /usr/local/ft-system
    docker compose up -d provision-portal

    Windows (PowerShell):

    $TAG = "<tag>"
    
    docker load -i "provision-portal-ui-$TAG.tar"
    cd /usr/local/ft-system
    docker compose up -d provision-portal

Confirm the image is present before starting, so a missing or mis-architected image fails here rather than mid-startup:

docker images hub.friendly-tech.com/ui/provision-portal

Upgrades use the same flow: pull the new tag on the connected machine, transfer and load the archive, then docker compose up -d provision-portal.

5. Preparation

5.1. Directory Structure

The platform layout gives each service one directory directly under /usr/local/ft-system, beside the host-level compose.yml and the shared .env. This container needs two directories of its own — an empty log directory does not survive a download:

mkdir -p /usr/local/ft-system/provision-portal/{ssl,nginx/logs}
cd /usr/local/ft-system

This follows the platform layout documented in Full System Deployment: one directory per service directly under /usr/local/ft-system.

5.1.1. Directory Layout

Once the files below are in place, the Provision Portal UI part of /usr/local/ft-system/ looks like this:

/usr/local/ft-system/
├── compose.yml                             # from FT_DISK -- Provision Portal UI (or the FT System <vendor> Stack)
├── .env                                    # shared stack environment
└── provision-portal/
    ├── ssl/                                # -> /etc/nginx/ssl (read-only)
    │   ├── friendly.crt
    │   └── friendly.key
    └── nginx/
        └── logs/                           # -> /var/log/nginx (created above)
Path Content Backup

compose.yml

Stack definition, including the provision-portal service block

Yes

.env

Shared stack environment. The three values this container reads live here: FT_PROV_API_URL and the two published ports

Yes

provision-portal/ssl/

friendly.crt and friendly.key, mounted read-only at /etc/nginx/ssl. Both file names are fixed — docker/ops/nginx.conf.template references them literally, and Nginx does not start without them.

Yes

provision-portal/nginx/logs/

Nginx access and error logs, mounted at /var/log/nginx. Written by the container at runtime.

No

The backend is deployed from its own FT_DISK folder into the sibling directory /usr/local/ft-system/provision-api/ — see Provision Portal API — Directory Structure.

5.1.2. FT_DISK Downloads

Download the following from FT_DISK — Provision Portal UI; the root .env comes from the FT System MySQL Stack / FT System Oracle Stack folder or is written from the three values in Environment Configuration:

Component FT_DISK Target Files

Stack definition

FT_DISK — Provision Portal UI

/usr/local/ft-system/compose.yml

compose.yml — single-service, in the platform layout (Docker Compose). Skip it on a host that runs the full FT System stack: its compose.yml already contains this service block

Shared environment

FT System MySQL Stack / FT System Oracle Stack

/usr/local/ft-system/.env

.env — shared by every FT service on the host

Provision Portal UI certificate

FT_DISK — Provision Portal UI

provision-portal/ssl/

Only the TLS pair: friendly.crt, friendly.key

There is no per-service .env in the folder, and that is deliberate: the container reads exactly one setting of its own, FT_PROV_API_URL, and that setting lives in the root .env next to the two published ports. A per-service copy would be a second place to edit the same values — and the published ports could not live there at all ([env-file-is-not-interpolated]).

5.2. Environment Configuration

Every value this container needs comes from the shared root .env, which compose.yml uses twice: it hands the file to the container through env_file:, and it reads the same file to interpolate $\{…​} inside compose.yml.

This container needs no database, no Flowable schema, no Northbound API and no JWT secret: tokens are issued and validated by the backend.

Table 1. .env (root) — the values Provision Portal UI reads
Variable Description Default Required

FT_PROV_API_URL

Upstream URL used by Nginx proxy_pass for the /prov-api/ location — the backend /prov-portal/ context path. Read by docker/ops/entrypoint.sh and substituted into nginx.conf with envsubst.

http://provision-api:8080/prov-portal/ in the shipped root .env (image fallback when unset: http://prov-api:8880/prov-portal/)

Yes

PROV_PORTAL_HTTP_PORT

Published HTTP port (→ 80)

8890

Yes

PROV_PORTAL_HTTPS_PORT

Published HTTPS port (→ 443)

8893

Yes

TZ

Container time zone

Europe/Kyiv

No

Why the published ports live in the root .env and nowhere else. Compose resolves $\{…​} in the compose file from two places only: the shell environment, and the .env file in the project directory — here /usr/local/ft-system/.env. Files named under env_file: are handed to the container at start-up; Compose never reads them while parsing the compose file.

Moved into a per-service env file, PROV_PORTAL_HTTP_PORT fails silently: the mapping falls back to its $\{PROV_PORTAL_HTTP_PORT:-8890} default and the container still starts, on the wrong port.

5.2.1. Pointing at the Backend

FT_PROV_API_URL is the only link between the two services. Its value depends on where the backend runs:

Backend runs FT_PROV_API_URL

On this host

http://provision-api:8080/prov-portal/ — both containers are on the ft-network bridge of the ft-system project, so the service name resolves and the container port is used

On another host

http://<api-host-ip>:8091/prov-portal/ — the published port, which must match PROV_API_HTTP_PORT on the backend host

Set FT_PROV_API_URL in the root .env, and do not add it under environment: in compose.yml — a compose environment: entry always wins over env_file:, so it would pin one upstream for every deployment, and an edit to .env would then silently do nothing: the login page still opens, and every call behind it returns 502.

Since Nginx resolves the proxy_pass host while loading its configuration (Startup Dependencies), a name that does not resolve stops the container from starting at all. Across hosts, use a literal IP or a name that already resolves.

The browser API base path is not configurable. docker/ops/entrypoint.sh rewrites assets/config/app.config.json with jq so that .apiUrl is the fixed relative value /prov-api/, matching the Nginx location. The browser calls /prov-api/…​ same-origin, Nginx forwards to FT_PROV_API_URL.

The Angular app loads assets/config/app.config.json before it boots (ConfigService.loadConfig() in an app initializer), so apiUrl is available to the first HTTP call. See Configuration.

5.3. TLS Certificate (required)

There is no HTTPS_ENABLED switch and no keystore environment variable. nginx.conf.template contains listen 443 ssl unconditionally and reads /etc/nginx/ssl/friendly.crt and /etc/nginx/ssl/friendly.key, so in practice the certificate mount is mandatory: without it Nginx fails to start. TLSv1.2 and TLSv1.3 with ECDHE ciphers are configured in the template.

FT_DISK ships a self-signed pair, enough to start the container. To generate a fresh one on the deployment host (run from /usr/local/ft-system/provision-portal) — a deployment host installing from the registry image has no source checkout, so docker/generate-cert.sh is not available there, and the command below is what that script does:

openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
  -keyout ssl/friendly.key \
  -out    ssl/friendly.crt \
  -subj "/C=IL/ST=Israel/L=Tel-Aviv/O=Friendly Tech./CN=friendly-tech.com" \
  -addext "subjectAltName=DNS:friendly-tech.com,DNS:*.friendly-tech.com,DNS:localhost,IP:127.0.0.1"
chmod 600 ssl/friendly.key
chmod 644 ssl/friendly.crt

In a source checkout the equivalent is cd docker && ./generate-cert.sh, which writes docker/ssl/friendly.crt and docker/ssl/friendly.key for the local build compose file.

For production, install a CA-issued pair under the same file names:

cp your-cert.crt  ssl/friendly.crt
cp your-cert.key  ssl/friendly.key
chmod 600 ssl/friendly.key

The certificate directory is mounted read-only at /etc/nginx/ssl and the HTTPS port is published by the stack compose.yml (${PROV_PORTAL_HTTPS_PORT:-8893}:443) — see Docker Compose.

Rotate certificates before expiry — the self-signed command above issues a 10-year certificate and is for internal use only. The container also serves plain HTTP on container port 80 (published as 8890); redirect or firewall it if your environment requires HTTPS only.

If Nginx fails to start because of the certificate, see Container Fails to Start.

6. Deployment

6.1. Startup Dependencies

provision-portal has no depends_on in the shipped compose.yml — the backend is external to that stack. Three conditions are on you:

  1. TLS certificate and key present in provision-portal/ssl/ — Nginx will not start without them.

  2. The host in FT_PROV_API_URL must resolve when the container starts: proxy_pass uses a literal host, which Nginx resolves while loading the configuration. The backend does not have to be serving requests yet, but its name must resolve — see Pointing at the Backend.

  3. The image must be available locally (pulled or loaded).

In the full FT System stack the same service block carries depends_on: provision-api: condition: service_healthy, so the backend is up — and its service name resolves on the shared bridge — before Nginx loads its configuration.

6.2. Docker Compose

Click to expand compose.yml (Provision Portal UI)
services:

  provision-portal:
    image: hub.friendly-tech.com/ui/provision-portal:latest
    container_name: provision-portal
    # The backend is external to this stack, so there is no depends_on. Its
    # host in FT_PROV_API_URL must resolve when the container starts: Nginx
    # resolves the upstream while loading its configuration.
    env_file:
      - .env
    ports:
      - "${PROV_PORTAL_HTTP_PORT:-8890}:80"      # HTTP
      - "${PROV_PORTAL_HTTPS_PORT:-8893}:443"    # HTTPS
    volumes:
      - ./provision-portal/ssl:/etc/nginx/ssl:ro   # friendly.crt + friendly.key, required by the shipped config
      - ./provision-portal/nginx/logs:/var/log/nginx
    extra_hosts:
      # Lets FT_PROV_API_URL use host.docker.internal when the backend runs on
      # this host outside this stack. Harmless otherwise.
      - "host.docker.internal:host-gateway"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://127.0.0.1/provision-portal/index.html"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    stop_grace_period: 45s
    networks:
      - ft-network

networks:
  ft-network:
    driver: bridge

There is no per-service env_file: layer for this container: everything it reads is in the root .env (Environment Configuration). Set FT_PROV_API_URL there before the first start (Pointing at the Backend); to change a published port, set PROV_PORTAL_HTTP_PORT / PROV_PORTAL_HTTPS_PORT in the same file.

In the full FT System stack the same block additionally carries profiles: [api] and depends_on: provision-api — COMPOSE_PROFILES in the root .env must then list api for a bare docker compose up -d to start it, and it has no extra_hosts entry — that line exists only in the single-service file, for a backend running on this host outside Compose. Everything else, including the mount paths and the single env_file: entry, is identical.

6.2.1. Start the Stack

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d provision-portal
docker compose ps
docker compose logs -f provision-portal | grep -i "nginx.conf generated"
curl -sk https://localhost:8893/provision-portal/index.html | head -1

Without Compose. The same deployment as a single docker run, executed from /usr/local/ft-system (where the Directory Structure step left you):

docker run -d \
  --name provision-portal \
  -p 8890:80 \
  -p 8893:443 \
  --env-file .env \
  -v "$PWD/provision-portal/ssl:/etc/nginx/ssl:ro" \
  -v "$PWD/provision-portal/nginx/logs:/var/log/nginx" \
  --add-host=host.docker.internal:host-gateway \
  --restart unless-stopped \
  hub.friendly-tech.com/ui/provision-portal:<tag>
Outside Compose there is no shared bridge, so FT_PROV_API_URL must use a published port here — http://host.docker.internal:8091/prov-portal/ on this host — either in the root .env or as -e FT_PROV_API_URL=…​ on the command above.

docker/docker-compose.yml in this repository is the local build variant: it builds from docker/Dockerfile, tags the result ui/provision-portal:tmp and publishes 8880:80 for local development only. The platform assigns this image the published ports 8890 → 80 and 8893 → 443 (Full System Deployment — Port Reference); 8880 / 8843 on a shared host belong to the Portals container and would collide. This guide follows the platform — see docs/decisions/LOG.md.

7. Verification

7.1. Startup Log

docker compose logs -f provision-portal

docker/ops/entrypoint.sh prints, in order:

Generating nginx.conf from template...
nginx.conf generated with FT_PROV_API_URL=<your value>
Patched app.config.json -> .apiUrl = /prov-api/

A WARN: nginx.conf.template not found or WARN: …​ app.config.json not found (skipping) line means the image is not the expected one.

7.2. Endpoint Checks

# 1. Container is up with both mappings
docker compose ps

# 2. The SPA is served over HTTP (host 8890 -> container 80)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8890/provision-portal/
# Expected: 200

# 3. The no-slash form redirects
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8890/provision-portal
# Expected: 301

# 4. TLS listener answers (host 8893 -> container 443)
curl -sk -o /dev/null -w "%{http_code}\n" https://localhost:8893/provision-portal/

# 5. The runtime API base was patched to the relative proxy path
docker compose exec provision-portal \
  cat /usr/share/nginx/html/provision-portal/assets/config/app.config.json
# Expected: { "apiUrl": "/prov-api/" }

# 6. The backend answers through the proxy
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8890/prov-api/

Then open http://<HOST>:8890/provision-portal/ in a browser.

8. Port Reference

Ports are written as published → container. Across hosts you connect to the published port; inside the Docker bridge you connect to the container port.

8.1. HTTP / HTTPS

Port Protocol Purpose Exposure

8890 → 80

HTTP

Serves the SPA under /provision-portal/ and the /prov-api/ reverse proxy. The container always listens on 80. Host port from PROV_PORTAL_HTTP_PORT.

Public

8893 → 443

HTTPS

TLS listener; requires friendly.crt + friendly.key under /etc/nginx/ssl/. The container always listens on 443. Host port from PROV_PORTAL_HTTPS_PORT.

Public

The published ports are the ones the platform assigns to this image; docker/docker-compose.yml in this repository publishes 8880:80 for local development only, and 8880 / 8843 belong to the Portals container on a shared host.

8.2. Outbound Connections

Destination Port Protocol Purpose

Provision Portal backend (host from FT_PROV_API_URL)

8080 on the shared bridge, 8091 published across hosts — whatever FT_PROV_API_URL contains. Across hosts it must match PROV_API_HTTP_PORT on the backend host

HTTP

proxy_pass target for /prov-api/, including WebSocket upgrade headers.

hub.friendly-tech.com

443

HTTPS

Image pull and update.

8.3. HTTP Endpoints

Method Path Purpose Auth

GET

/provision-portal

Redirects (301) to /provision-portal/.

None

GET

/provision-portal/

The SPA and its assets, with try_files fallback to index.html for deep links.

None

any

/prov-api/

Reverse-proxied to FT_PROV_API_URL.

Enforced by the backend

9. Stack Management

9.1. Logs

docker compose logs -f provision-portal
docker compose logs --tail 100 provision-portal

9.2. Start, Stop, Restart

docker compose up -d provision-portal      # start
docker compose stop provision-portal       # stop, keep the container
docker compose restart provision-portal    # restart (re-runs the entrypoint)
docker compose down                        # stop and remove EVERY container of the ft-system project

9.3. Shell Access

docker compose exec provision-portal bash          # bash is installed in the image
docker compose exec provision-portal cat /etc/nginx/nginx.conf
docker compose exec provision-portal env | grep FT_PROV_API_URL

9.4. Updating Provision Portal UI

Online hosts:

docker compose pull provision-portal
docker compose up -d provision-portal
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8890/provision-portal/

Offline hosts: pull, save and transfer the new image as described in Offline Servers, then

gzip -dc provision-portal-ui-<new-tag>.tar.gz | docker load
docker compose up -d provision-portal     # after updating the tag in compose.yml

To roll back, set the previous tag in compose.yml and run docker compose up -d provision-portal again.

10. Production Checklist

  • FT_PROV_API_URL points at the correct backend /prov-portal/ context path and its hostname resolves from inside the container.

  • A CA-issued friendly.crt + friendly.key are mounted at /etc/nginx/ssl/, and ssl/friendly.key is chmod 600.

  • Both mappings are published: 8890:80 and 8893:443.

  • The image tag is pinned to a release tag, never latest — the stack compose.yml uses latest.

  • restart: unless-stopped is set.

  • Resource limits are set (deploy.resources.limits or --memory) — nothing is set by default.

  • The backend is reachable at the address in FT_PROV_API_URL (Pointing at the Backend); across two hosts, verify the firewall allows port 8091.

  • Container stdout/stderr is collected by the Docker logging driver or a log aggregator.

11. Troubleshooting

Start with:

docker compose logs --tail 100 provision-portal
docker compose exec provision-portal env | grep FT_PROV_API_URL

11.1. Container Fails to Start

Symptom: the container starts and immediately exits, or the log shows an SSL certificate error or an Nginx host not found in upstream error.

Cause and fix:

  • The certificate mount is missing. nginx.conf.template always listens on 443 and reads /etc/nginx/ssl/friendly.crt + friendly.key. Confirm the ./provision-portal/ssl volume is mounted and both files exist and are readable. For testing, generate a self-signed pair as shown in TLS Certificate (required).

  • The hostname in FT_PROV_API_URL does not resolve. Nginx resolves the literal proxy_pass host when it loads the configuration. Check the rendered file: docker compose exec provision-portal cat /etc/nginx/nginx.conf.

11.2. UI Loads but Requests Fail

Symptom: the login page appears, but login fails or no data loads.

Check:

docker compose exec provision-portal \
  cat /usr/share/nginx/html/provision-portal/assets/config/app.config.json
# Expected: { "apiUrl": "/prov-api/" }

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8890/prov-api/

Fix:

  • Verify FT_PROV_API_URL in the root .env points at a running backend, ends with the /prov-portal/ context path, and matches the topology — service name and container port on one host, IP and published port across two (Pointing at the Backend). Never localhost: inside the container it points at the container itself.

  • Across two hosts, check the firewall between them: curl -s http://<api-host-ip>:8091/prov-portal/actuator/health.

11.3. Port Already in Use

Symptom: address already in use when starting the container.

Check: lsof -i :8890 or docker ps.

Fix: stop the conflicting process, or change PROV_PORTAL_HTTP_PORT in the root .env — see [env-file-is-not-interpolated] for why the published ports live there.

11.4. Blank Page or JavaScript Errors

Symptom: the page loads blank, or the browser console shows errors.

Check: open DevTools → Console, and confirm assets/config/app.config.json is served and contains "apiUrl": "/prov-api/".

Fix: an empty or missing config file means the entrypoint’s jq patch did not run — check docker compose logs provision-portal for the Patched app.config.json line.

[[404-on-deep-links-or-refresh]] === 404 on Deep Links or Refresh

Symptom: opening a route directly, or refreshing, returns 404.

Fix: the location /provision-portal/ block uses try_files $uri $uri/ /provision-portal/index.html. Confirm the request path is under /provision-portal/ and that the build output was copied to /usr/share/nginx/html/provision-portal (docker compose exec provision-portal ls /usr/share/nginx/html/provision-portal). The CI build sets --base-href /provision-portal/; a build without it will not resolve its assets.

11.5. Permission Denied on Mounted Volumes

Symptom: the container starts but provision-portal/nginx/logs/ stays empty, or the log shows Nginx failing to open its log files.

Fix:

sudo chown -R $(whoami):$(whoami) /usr/local/ft-system/provision-portal
chmod -R 755 /usr/local/ft-system/provision-portal/nginx/logs
chmod 600 /usr/local/ft-system/provision-portal/ssl/friendly.key
chmod 644 /usr/local/ft-system/provision-portal/ssl/friendly.crt

The ssl/ mount is read-only, so it needs read access only — but the key must stay unreadable to other users on the host.

11.6. Getting Support

Collect the following before contacting the Friendly Technologies support team:

docker compose logs --tail 500 provision-portal > provision-portal-ui.log
docker compose exec provision-portal cat /etc/nginx/nginx.conf > nginx.rendered.conf
docker compose exec provision-portal \
  cat /usr/share/nginx/html/provision-portal/assets/config/app.config.json
docker inspect $(docker compose ps -q provision-portal) --format '{{json .Config.Env}}'

Include the image tag in use and whether the host is online or offline.

The repository README covers the local development workspace and the local production build.