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. The image is built from
docker/Dockerfile on top of nginx:alpine; it copies the pre-built Angular bundle
(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.
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. 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 Compose |
v2 ( |
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 — |
Disk |
verify with DevOps |
verify with DevOps |
Sized by the |
|
Node.js is required only to produce the Angular build from source (see [build-from-source]). The
runtime image is |
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 |
Taken from |
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 |
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.
This repository does not ship a pre-configured deployment package. The compose.yml and the TLS
certificate directory are created on the target host from the examples in this guide;
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 |
Whatever |
HTTP |
Nginx |
|
443 |
HTTPS |
Pulling the |
3.1. Docker Networking
The container reaches the backend by whatever hostname FT_PROV_API_URL contains, resolved from
inside the container:
-
Backend in Docker: put both containers on the same Docker network and use the backend’s service or container name as the hostname.
docker network create prov-netAdd
--network prov-nettodocker run, or anetworks:section tocompose.yml:services: provision-portal: image: hub.friendly-tech.com/ui/provision-portal:<tag> networks: - prov-net networks: prov-net: external: true -
Backend on the host machine: use
host.docker.internal(works on Docker Desktop; on Linux addextra_hosts: ["host.docker.internal:host-gateway"]or--add-host=host.docker.internal:host-gateway), or the host’s real IP address.
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 |
-
On the offline host, find out which architecture it runs — this is the value you will pass as
PLATFORMbelow. 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 -mon Linux, orecho $env:PROCESSOR_ARCHITECTUREin PowerShell on Windows. Map the result:docker versionreportsuname -m/ Windows reportsUse as PLATFORMamd64x86_64/AMD64linux/amd64arm64aarch64/ARM64linux/arm64On 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. -
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 -
Pull and export the image. Paste the whole block as-is; the only lines to change are
PLATFORMandTAGon 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 redirectingdocker savefrom PowerShell (docker save … > file.tar) corrupts the archive, because the PowerShell pipeline re-encodes the stream as text instead of passing raw bytes.docker loadthen fails withunexpected EOForinvalid tar header. To compress for transfer, use the bundledtar.exe(Windows 10 1803+ / Server 2019+):tar.exe -czf provision-portal-ui.tar.gz provision-portal-ui-$TAG.tar. -
Transfer the archive to the offline host, together with
compose.yml, the shared.env, and thessl/directory holdingfriendly.crtandfriendly.key— see Directory Layout. -
On the offline host, load the archive and start the service. Set
TAGto the same value you used above:Linux (bash):
TAG=<tag> gzip -dc "provision-portal-ui-$TAG.tar.gz" | docker load docker compose up -d provision-portalWindows (PowerShell):
$TAG = "<tag>" docker load -i "provision-portal-ui-$TAG.tar" 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.
|
This covers the Provision Portal UI image only.
The Provision Portal backend is a separate image on its own host — for the offline procedure covering a whole stack, see All in one server deployment — Offline Servers, which derives the image list from |
5. Preparation
5.1. Directory Structure
Create the deployment directory on the host:
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
The Provision Portal UI slice of the platform layout — compose.yml and the shared .env at the
root, everything owned by this service under provision-portal/:
/usr/local/ft-system/
├── compose.yml # from FT_DISK (or the example in <<docker-compose>>)
├── .env # shared stack environment
└── provision-portal/
├── ssl/ # -> /etc/nginx/ssl (read-only)
│ ├── friendly.crt
│ └── friendly.key
└── nginx/
└── logs/ # -> /var/log/nginx (created automatically)
| Path | Content | Backup |
|---|---|---|
|
Stack definition — image tag, published ports, volumes and the |
Yes |
|
Shared stack environment: |
Yes |
|
|
Yes |
|
Nginx access and error logs, mounted at |
No |
5.2. Environment Configuration
The platform uses a two-layer environment file architecture, and this service reads only the first layer:
-
Root
.env— shared by every service: database connection (DB_HOST,DB_PORT,DB_USER,DB_PASSWORD), Flowable DB, ClickHouse, PostgreSQL, Hazelcast, JWT,COMPOSE_PROFILESand the inter-service URLs.FT_PROV_API_URL, the only variable this container reads, belongs here. -
<service>/.env— per-service host ports, pool tuning, JVM settings and feature flags. The platformcompose.ymldefines noprovision-portal/.env: the UI’s published ports (PROV_PORTAL_HTTP_PORT,PROV_PORTAL_HTTPS_PORT) are documented under provision-api/.env. The Provision Portal backend guide lists a secondenv_fileentry./provision-portal/.envfor this container; if you deploy from that guide’scompose.yml, create that file (it may be empty) or Compose aborts withenv file ./provision-portal/.env not found. Seedocs/decisions/LOG.md.
There is no per-database-vendor env file. Selecting Oracle instead of the default MySQL means
changing DB_PROFILE, SPRING_PROFILES_ACTIVE, DB_HOST and the other variables listed in
Switching to Oracle inside
the single root .env; compose.yml maps the generic DB_* names to the vendor-specific ones each
application expects through the x-db-env / x-flowable-env YAML anchors. None of this reaches the
Provision Portal UI container, which never talks to a database.
The variable read by this container:
| Variable | Description | Default | Required |
|---|---|---|---|
|
Upstream URL used by Nginx |
No |
The browser’s 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.
For local or internal use, generate a self-signed pair directly on the deployment host (run from
/usr/local/ft-system, where the Directory Structure step left you). A deployment host installing
from the registry image has no source checkout, so docker/generate-cert.sh is not available there;
the command below is what that script does:
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
-keyout provision-portal/ssl/friendly.key \
-out provision-portal/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 provision-portal/ssl/friendly.key
chmod 644 provision-portal/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 provision-portal/ssl/friendly.crt
cp your-cert.key provision-portal/ssl/friendly.key
chmod 600 provision-portal/ssl/friendly.key
The certificate directory is mounted read-only at /etc/nginx/ssl and the HTTPS port is published by
the compose.yml in Docker Compose (${PROV_PORTAL_HTTPS_PORT:-8893}:443).
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
-
TLS certificate and key present in the mounted
ssldirectory — Nginx will not start without them. -
The hostname in
FT_PROV_API_URLmust resolve when the container starts:proxy_passuses 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. -
The image must be available locally (pulled or loaded).
6.2. Docker Compose
Create compose.yml in the deployment directory:
services:
provision-portal:
image: hub.friendly-tech.com/ui/provision-portal:<tag>
env_file:
- .env # shared stack environment; holds FT_PROV_API_URL
ports:
- "${PROV_PORTAL_HTTP_PORT:-8890}:80" # HTTP (host 8890 -> container 80)
- "${PROV_PORTAL_HTTPS_PORT:-8893}:443" # HTTPS (host 8893 -> container 443)
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:
- "host.docker.internal:host-gateway"
restart: unless-stopped
FT_PROV_API_URL is taken from the shared root .env. Do not add it under environment: in
compose.yml — a compose environment: entry always wins over env_file, so it would pin one
host for every deployment and, since Nginx resolves the proxy_pass host at startup
(Startup Dependencies), the container would fail to start wherever that host does not resolve.
Set the value once in .env:
FT_PROV_API_URL=http://provision-api:8080/prov-portal/
To change a published port, set PROV_PORTAL_HTTP_PORT / PROV_PORTAL_HTTPS_PORT in .env, or edit
only the left side of a mapping (<host>:<container>).
docker compose up -d
docker compose ps
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,
PROV_PORTAL_HTTP_PORT / PROV_PORTAL_HTTPS_PORT in
the environment 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.
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-ui \
-p 8890:80 \
-p 8893:443 \
-e FT_PROV_API_URL="http://provision-api:8080/prov-portal/" \
-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>
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 |
Public |
8893 → 443 |
HTTPS |
TLS listener; requires |
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 |
Whatever |
HTTP |
|
|
443 |
HTTPS |
Image pull and update. |
9. Stack Management
9.2. Start, Stop, Restart
docker compose up -d # 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
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_URLpoints at the correct backend/prov-portal/context path and its hostname resolves from inside the container. -
A CA-issued
friendly.crt+friendly.keyare mounted at/etc/nginx/ssl/, andprovision-portal/ssl/friendly.keyischmod 600. -
Both mappings are published:
8890:80and8893:443. -
The image tag is pinned to a release tag, never
latest. -
restart: unless-stoppedis set. -
Resource limits are set (
deploy.resources.limitsor--memory) — nothing is set by default. -
The UI and the backend share a dedicated Docker network, or the backend host is reachable by name.
-
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.templatealways listens on 443 and reads/etc/nginx/ssl/friendly.crt+friendly.key. Confirm the./provision-portal/sslvolume 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_URLdoes not resolve. Nginx resolves the literalproxy_passhost 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_URLpoints at a running backend and ends with the/prov-portal/context path. -
If the backend runs on the host, use
host.docker.internal(withextra_hostson Linux) or the host IP — neverlocalhost. -
If both run in Docker, put them on the same network and use the backend’s container name.
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 only the published side of the mapping
(8080:80 instead of 8890:80).
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 bundle 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 bundle
built without it will not resolve its assets.
11.5. 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.