Installation & Deployment

Installing FT Configs UI — a Vite + React single-page application served by nginx, which also reverse-proxies /configs-service/ to FT Configs Service.

Overview

This guide deploys one container, ft-configs-ui, from the image hub.friendly-tech.com/configs/ft-configs-ui:latest. The image is built on nginx:alpine (docker/Dockerfile): it copies the Vite bundle (dist/) to /usr/share/nginx/html and serves it. There is no Node.js in the runtime image. Docker Compose is the only deployment method — this repository does not build a distribution archive.

FT Configs Service (the Spring Boot backend served at the /configs-service context path) is assumed to be already deployed and reachable; it is not deployed by this guide. To deploy it, see FT Configs Stack deployment or All in one server deployment.

The browser never talks to the backend directly. It calls the same-origin relative path /configs-service/…​, and nginx proxies that to the URL in BACKEND_URL.

FT Configs UI is one service of the platform stack: Compose project root /usr/local/ft-system/, bridge network ft-network, one directory per service below the root. Keeping the layout identical means the container can be added to an existing stack without moving any directory.

Prerequisites

Host Requirements

Component Minimum Recommended Notes

Docker Engine

20.10

20.10 or newer

Stated by FT Configs Stack deployment — Prerequisites

Docker Compose

2.0

v2 (docker compose)

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

RAM

verify with FT DevOps

verify with FT DevOps

Not pinned in this repository — neither docker/compose.yml nor the platform compose.yml sets deploy.resources limits

Disk

verify with FT DevOps

verify with FT DevOps

Sized by the nginx:alpine image plus the static Vite bundle

Node.js is required only to produce the Vite bundle from source. The runtime image is nginx:alpine and contains no Node.js; docker/Dockerfile uses node:20-bookworm-slim in a discarded builder stage only.

Required External Dependencies

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

Component Minimum Version Why It Is Needed Port Required

FT Configs Service

verify with the FT Configs Service owner

Serves every API call the SPA makes — authentication, configuration CRUD, import/export, audit, user management — at the /configs-service context path. Without it the UI loads but every request through /configs-service/ fails, and /ready returns 502.

8087 → 8080 (HTTP). On the ft-network bridge use the container port 8080; across hosts use the published port 8087

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.

FT Configs Service in turn reaches its MySQL or Oracle database and the Hazelcast cluster. Neither is ever contacted by the browser or by this container.

Supported Operating Systems

Deployment Operating system

Docker Compose

Linux (recommended), macOS, or Windows with WSL2

Registry Access

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

Access to Friendly Tech SharePoint (FT_DISK — ft-configs) for the deployment files (compose.yml, .env) and to FT_DISK — ft-configs/frontend for generate-cert.sh and the shipped tls.crt / tls.key pair.

Network Requirements

Each row is a connection that must be open through firewalls when the peer is on another host; traffic to peers on the same Docker bridge needs no rule.

Destination Port Protocol Purpose

FT Configs Service (host from BACKEND_URL)

8080 on the bridge, 8087 across hosts

HTTP

Outbound: nginx proxy_pass for the /configs-service/ location and the /ready probe

hub.friendly-tech.com

443

HTTPS

Outbound: image pull and update. Not required on offline hosts

FT Configs UI

3001 → 80

HTTP

Inbound from browsers: the SPA and the /configs-service/ reverse proxy

FT Configs UI

3443 → 443

HTTPS

Inbound from browsers: the TLS listener, active only when tls.crt and tls.key are mounted

Docker Networking

The platform compose.yml puts the container on the ft-network bridge with container_name: ft-configs-ui, so peers on that bridge reach it by that name. The backend is addressed the same way: the default BACKEND_URL=http://ft-configs-service:8080 is a container name on the same bridge.

BACKEND_URL must be scheme://host[:port] with no path component. docker/ops/entrypoint.sh documents why: with a variable proxy_pass, nginx replaces the whole client URI with the path in the directive, which would drop /csrf, /auth/login and every other request path.

Both nginx templates set resolver 127.0.0.11 ipv6=off valid=30s and assign the upstream to a variable before proxy_pass, so the hostname is resolved at request time. The UI container therefore starts even when the backend name does not resolve yet.

If the backend runs on the same host but outside Docker, use host.docker.internal (Docker Desktop on macOS and Windows; on Linux add extra_hosts: ["host.docker.internal:host-gateway"] or --add-host=host.docker.internal:host-gateway) or the host’s real LAN IP. localhost and 127.0.0.1 never work here — inside the container they point at the container itself.

Registry Authentication

All FT Configs 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:

Field Value

Username

readonly

Password

fokxuw-fymte1-taSxyc

The readonly 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:

echo "fokxuw-fymte1-taSxyc" | docker login hub.friendly-tech.com -u readonly --password-stdin

Verify authentication, then pull the image:

docker info | grep -A 5 Registry
docker pull hub.friendly-tech.com/configs/ft-configs-ui:latest
For production, pin a release tag instead of latest. The QA release workflow (.github/workflows/build-test-qa-deploy.yml) publishes both :latest and :v<version>-b<build>.

Offline Servers

When the FT Configs 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. The release workflow .github/workflows/build-test-qa-deploy.yml builds linux/amd64,linux/arm64, so the tag in Harbor is multi-arch; 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.

  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=latest
    
    docker pull --platform "$PLATFORM" "hub.friendly-tech.com/configs/ft-configs-ui:$TAG"
    docker save "hub.friendly-tech.com/configs/ft-configs-ui:$TAG" | gzip > "ft-configs-ui-$TAG.tar.gz"

    Windows (PowerShell):

    $PLATFORM = "linux/amd64"
    $TAG = "latest"
    
    docker pull --platform $PLATFORM "hub.friendly-tech.com/configs/ft-configs-ui:$TAG"
    docker save -o "ft-configs-ui-$TAG.tar" "hub.friendly-tech.com/configs/ft-configs-ui:$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 bundled tar.exe (Windows 10 1803+ / Server 2019+): tar.exe -czf ft-configs-ui.tar.gz ft-configs-ui-$TAG.tar.

  4. Transfer the archive to the offline host, together with compose.yml, the shared .env, ft-configs-ui/.env, and the ft-configs-ui/certs/ directory holding tls.crt and tls.key — 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=latest
    
    gzip -dc "ft-configs-ui-$TAG.tar.gz" | docker load
    docker compose up -d ft-configs-ui

    Windows (PowerShell):

    $TAG = "latest"
    
    docker load -i "ft-configs-ui-$TAG.tar"
    docker compose up -d ft-configs-ui

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/configs/ft-configs-ui

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

This covers the FT Configs UI image only. FT Configs Service is a separate image — for the offline procedure covering the whole stack, see All in one server deployment — Offline Servers, which derives the image list from compose.yml itself.

Preparation

Directory Structure

Create the deployment directory on the host:

mkdir -p /usr/local/ft-system/ft-configs-ui/certs
cd /usr/local/ft-system

This follows the platform layout documented in Prepare Working Directory: compose.yml and the shared .env at the root, one directory per service below it.

Directory Layout

The FT Configs UI slice of the platform layout:

/usr/local/ft-system/
├── compose.yml                             # from FT_DISK (the service block is shown in <<docker-compose>>)
├── .env                                    # shared stack environment
└── ft-configs-ui/
    ├── .env                                # per-service environment
    └── certs/                              # -> /etc/nginx/ssl (read-only)
        ├── tls.crt
        └── tls.key
Path Content Backup

compose.yml

Stack definition — image tag, published ports, the env_file list and the certificate mount for this service

Yes

.env

Shared stack environment: TZ, database, Hazelcast and the inter-service values used by the whole stack

Yes

ft-configs-ui/.env

FT Configs UI only: FT_CONFIGS_UI_HTTP_PORT, FT_CONFIGS_UI_HTTPS_PORT, BACKEND_URL, METRICS_ALLOW_IP

Yes

ft-configs-ui/certs/

tls.crt and tls.key, mounted read-only at /etc/nginx/ssl. Both file names are fixed — docker/ops/entrypoint.sh looks for exactly these. An empty directory means HTTP-only

Yes

Environment Configuration

The stack uses two environment layers, and this container reads both:

File Holds

.env

Shared by the whole stack: TZ, database connection, Hazelcast, and the values Docker Compose interpolates into compose.yml

ft-configs-ui/.env

FT Configs UI only: the published host ports and the two variables the container itself reads

# /usr/local/ft-system/ft-configs-ui/.env

# Published host ports (mapped to container 80 / 443)
FT_CONFIGS_UI_HTTP_PORT=3001
FT_CONFIGS_UI_HTTPS_PORT=3443

# Upstream for the nginx /configs-service/ proxy.
# scheme://host[:port] with NO path -- use the container name on ft-network.
BACKEND_URL=http://ft-configs-service:8080

# Extra CIDR allowed to scrape /nginx_status
METRICS_ALLOW_IP=0.0.0.0/0

The variables the container itself reads, both consumed by docker/ops/entrypoint.sh and substituted into the nginx configuration with envsubst:

Variable Description Default Required

BACKEND_URL

Upstream URL used by nginx proxy_pass for the /configs-service/ location and the /ready probe. Must be scheme://host[:port] with no path. Resolved at request time through the Docker embedded resolver

http://ft-configs-service:8080

No

METRICS_ALLOW_IP

Extra CIDR added to the /nginx_status allow-list. 127.0.0.1, 10.0.0.0/8 and 172.16.0.0/12 are always allowed on top of it

0.0.0.0/0

No

FT_CONFIGS_UI_HTTP_PORT

Published host port mapped to container port 80. Read by Docker Compose, not by the container

3001

No

FT_CONFIGS_UI_HTTPS_PORT

Published host port mapped to container port 443. Read by Docker Compose, not by the container

3443

No

The full platform-wide reference is ft-configs-ui/.env.

Runtime Configuration Injection

The browser’s API base path is not configurable. On every container start docker/ops/entrypoint.sh writes /usr/share/nginx/html/runtime-config.js with the fixed value

window.__RUNTIME_CONFIG__ = {"API_URL":"/configs-service"};

That path must match both the nginx /configs-service/ location and the backend context path. The browser calls the relative path /configs-service/…​ same-origin, nginx intercepts it and forwards it to BACKEND_URL.

index.html loads /runtime-config.js with a plain synchronous <script> before the app bundle, so window.RUNTIME_CONFIG.API_URL is available to the first module that imports src/lib/runtime-config.ts. That module validates the object against a Zod schema and throws a descriptive error if it is missing or malformed.

The same image therefore targets any backend and runs with or without TLS without a rebuild: only runtime-config.js and the selected nginx template change at startup.

The version and build number shown in the UI are not build-time environment variables. They come from version.json, which the Dockerfile copies to /usr/share/nginx/html/version.json from the build context — the QA release workflow rewrites that file immediately before docker build, so the served value always matches the published tag.

TLS Certificate (optional)

There is no HTTPS_ENABLED switch. TLS is enabled automatically when the container starts with both certificate files present at /etc/nginx/ssl/:

  • /etc/nginx/ssl/tls.crt

  • /etc/nginx/ssl/tls.key

If both exist, docker/ops/entrypoint.sh renders nginx.conf.template — nginx listens on 80 and 443 with TLSv1.2 and TLSv1.3 and ECDHE ciphers. If either is missing it renders nginx-http-only.conf.template, which listens on 80 only. That is the expected mode behind a Kubernetes Ingress or any proxy terminating TLS upstream.

The file names are fixed. They match the keys emitted by a Kubernetes Secret of type kubernetes.io/tls, so such a secret can be mounted at /etc/nginx/ssl/ without renaming anything.

The ft-configs-ui/certs/ directory on FT_DISK already ships a shared *.friendly-tech.com wildcard development certificate, so HTTPS works out of the box. To use your own pair, drop it in under the same two names; to fall back to HTTP-only, remove both files.

For local or internal use, generate a self-signed pair on the deployment host (run from /usr/local/ft-system, where the Directory Structure step left you):

openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
  -keyout ft-configs-ui/certs/tls.key \
  -out    ft-configs-ui/certs/tls.crt \
  -subj "/C=IL/ST=Israel/L=Tel-Aviv/O=Friendly Tech./CN=friendly-tech.com"
chmod 600 ft-configs-ui/certs/tls.key
chmod 644 ft-configs-ui/certs/tls.crt

In a source checkout the equivalent helper is docker/generate-cert.sh; the same script is available on FT_DISK for deployment hosts that have no checkout. A self-signed pair makes browsers warn until the certificate is imported into the trust store.

The chmod above is deliberate: the nginx:alpine master process starts as root and reads both files before dropping privileges to the nginx user, so world-readable permissions are never required and the private key stays owner-only.

TLS Parameters

The TLS parameters are baked into docker/ops/nginx.conf.template; there are no runtime flags for them:

listen 443 ssl;
ssl_certificate     /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;

ssl_protocols       TLSv1.2 TLSv1.3;
ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;

To change the cipher suites or the protocol versions, edit that template and rebuild the image.

The two templates are identical apart from the listen directives and this TLS block: every location (/configs-service/, /health, /ready, /nginx_status, the SPA fallback, the cache rules and the source-map block) is shared. nginx terminates TLS itself — the image runs no other server. The TLS template listens on 80 and 443 at the same time and does not redirect HTTP to HTTPS.

Neither template sets Strict-Transport-Security. If you need HSTS, terminate TLS in a proxy that adds the header, or add it to docker/ops/nginx.conf.template and rebuild — do not add it to the HTTP-only template, which runs behind an upstream terminator.

Kubernetes Secret

Outside Docker Compose the same two files come from a kubernetes.io/tls Secret mounted at the same path:

apiVersion: v1
kind: Secret
metadata:
  name: ft-configs-ui-tls
type: kubernetes.io/tls
data:
  tls.crt: <base64>
  tls.key: <base64>
---
spec:
  containers:
    - name: ft-configs-ui
      image: hub.friendly-tech.com/configs/ft-configs-ui:latest
      volumeMounts:
        - name: tls
          mountPath: /etc/nginx/ssl
          readOnly: true
  volumes:
    - name: tls
      secret:
        secretName: ft-configs-ui-tls

When the cluster terminates TLS at the Ingress, omit the volume entirely and let the image fall through to HTTP-only.

Certificate Problems

Symptom Cause Fix

The container runs and port 3001 answers, but https://<HOST>:3443/ is refused

entrypoint.sh did not find tls.crt and tls.key under /etc/nginx/ssl, so it rendered the HTTP-only template

Check the startup log for [nginx] No SSL certificates — HTTP-only mode (see Startup Log), then verify the mount and the file names — they must be exactly tls.crt and tls.key

nginx: [emerg] cannot load certificate "/etc/nginx/ssl/tls.crt"

The file is missing, malformed, or not PEM

Validate it with openssl x509 -in ft-configs-ui/certs/tls.crt -noout -text. It must be full PEM with -----BEGIN CERTIFICATE----- markers, not DER

nginx: [emerg] SSL_CTX_use_PrivateKey_file(…​) failed

The key does not match the certificate, or it is encrypted with a passphrase

Compare the moduli: openssl x509 -in tls.crt -noout -modulus | openssl md5 against openssl rsa -in tls.key -noout -modulus | openssl md5. Strip a passphrase with openssl rsa -in tls.key -out tls.key.nopass

The browser shows NET::ERR_CERT_AUTHORITY_INVALID

A self-signed certificate, including the output of docker/generate-cert.sh

Import the certificate into the OS or browser trust store, or replace the pair with one issued by a trusted CA

Deployment

Startup Dependencies

  1. The image must be available locally (pulled or loaded — see Registry Authentication).

  2. FT Configs Service must be healthy. The platform compose.yml gates the UI on it with depends_on: ft-configs-service: condition: service_healthy, so Compose starts the UI only after the backend’s health check passes.

  3. The certificate pair is not a startup dependency: with no tls.crt / tls.key the container starts in HTTP-only mode.

  4. The backend hostname does not have to resolve at start: nginx resolves the proxy_pass upstream at request time (see Docker Networking).

Docker Compose

The FT Configs UI service block of the platform compose.yml:

services:
  ft-configs-ui:
    image: hub.friendly-tech.com/configs/ft-configs-ui:latest
    container_name: ft-configs-ui
    depends_on:
      ft-configs-service:
        condition: service_healthy
    env_file:
      - .env                                  # shared stack environment
      - ./ft-configs-ui/.env                  # per-service environment
    ports:
      - "${FT_CONFIGS_UI_HTTP_PORT:-3001}:80"     # HTTP  (host 3001 -> container 80)
      - "${FT_CONFIGS_UI_HTTPS_PORT:-3443}:443"   # HTTPS (host 3443 -> container 443)
    volumes:
      # HTTPS is auto-enabled if tls.crt + tls.key exist here; HTTP-only otherwise.
      - ./ft-configs-ui/certs:/etc/nginx/ssl:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/health"]
      interval: 10s
      timeout: 5s
      retries: 30
    restart: unless-stopped
    networks:
      - ft-network

Start the service:

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d ft-configs-ui
docker compose ps

To change a published port, set FT_CONFIGS_UI_HTTP_PORT / FT_CONFIGS_UI_HTTPS_PORT in ft-configs-ui/.env, or edit only the left side of a mapping (<host>:<container>). If you change the UI port, update CORS_ALLOWED_ORIGINS on the backend accordingly — it must list the host and port typed in the browser, never the internal Docker name.

docker/compose.yml in this repository is a developer file, not the deployment descriptor: it builds a local ft-configs-ui:latest image, publishes 80:80 and 443:443, and mounts ./ssl. This guide follows the platform deployment; the divergences are recorded in docs/decisions/LOG.md.

Verification

Startup Log

docker compose logs -f ft-configs-ui

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

[runtime-config] API_URL=/configs-service
[nginx] SSL certificates found — enabling HTTPS
[nginx] Config generated, backend=http://ft-configs-service:8080

The second line is [nginx] No SSL certificates — HTTP-only mode when the certificate pair is absent; that is the normal HTTP-only startup, not an error. The third line echoes the effective BACKEND_URL — check it first whenever API calls fail.

Endpoint Checks

# 1. Container is up with both mappings
docker compose ps ft-configs-ui

# 2. Liveness endpoint served by nginx itself (host 3001 -> container 80)
curl -s http://localhost:3001/health
# Expected: {"status":"ok"}

# 3. The runtime config was injected
curl -s http://localhost:3001/runtime-config.js
# Expected: window.__RUNTIME_CONFIG__ = {"API_URL":"/configs-service"};

# 4. Readiness -- proxied to the backend actuator
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001/ready
# Expected: 200 once FT Configs Service is up

# 5. TLS listener answers (host 3443 -> container 443), only with certificates mounted
curl -sk -o /dev/null -w "%{http_code}\n" https://localhost:3443/health

# 6. The SPA itself
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001/

Then open http://<HOST>:3001/ in a browser. The first administrator account is created on the first startup of FT Configs Service from its FT_CONFIGS_BOOTSTRAP_ADMIN_* variables.

If the UI loads but shows connection errors, check the backend from inside the container:

docker exec ft-configs-ui wget -qO- "$BACKEND_URL/configs-service/actuator/health"

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.

HTTP / HTTPS

Port Protocol Purpose Exposure

3001 → 80

HTTP

Serves the SPA and the /configs-service/ reverse proxy. The container always listens on 80. Host port from FT_CONFIGS_UI_HTTP_PORT

Public

3443 → 443

HTTPS

TLS listener; present only when tls.crt and tls.key are mounted at /etc/nginx/ssl/. Host port from FT_CONFIGS_UI_HTTPS_PORT

Public

With certificates mounted the container listens on 80 and 443 at the same time; publishing both mappings is safe.

The Vite dev server (npm run dev, port 9002) is a local-development tool only and is never part of a deployment; it is described in the repository README.

Outbound Connections

Destination Port Protocol Purpose

FT Configs Service (host from BACKEND_URL)

8080 on the bridge, 8087 across hosts

HTTP

proxy_pass target for /configs-service/ and for the /ready probe

hub.friendly-tech.com

443

HTTPS

Image pull and update

HTTP Endpoints

Method Path Purpose Auth

GET

/

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

None

GET

/runtime-config.js

The injected window.RUNTIME_CONFIG; served with no-store

None

GET

/version.json

Version and build number shown in the UI

None

GET

/health

Liveness; answered by nginx itself with {"status":"ok"}, access_log off

None

GET

/ready

Readiness; proxied to ${BACKEND_URL}/configs-service/actuator/health with 5s timeouts

None

GET

/nginx_status

nginx stub_status metrics; allow-list is 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12 and METRICS_ALLOW_IP, deny all

IP allow-list

any

/configs-service/

Reverse-proxied to BACKEND_URL, forwarding the original request URI verbatim

Enforced by the backend

Stack Management

Logs

docker compose logs -f ft-configs-ui
docker compose logs --tail 100 ft-configs-ui

nginx access and error logs go to the container’s stdout and stderr; nothing is written to a mounted volume. Collect them with the Docker logging driver or a log aggregator.

Start, Stop, Restart

docker compose up -d ft-configs-ui        # start
docker compose stop ft-configs-ui         # stop, keep the container
docker compose restart ft-configs-ui      # restart (re-runs the entrypoint)
docker compose down                       # stop and remove the stack

A restart re-runs entrypoint.sh, so it re-detects the certificate pair and re-renders /etc/nginx/nginx.conf. Adding or removing certificates takes effect on restart, without a rebuild.

Shell Access

docker compose exec ft-configs-ui sh                              # nginx:alpine ships ash, not bash
docker compose exec ft-configs-ui cat /etc/nginx/nginx.conf       # the rendered configuration
docker compose exec ft-configs-ui cat /usr/share/nginx/html/runtime-config.js
docker inspect ft-configs-ui --format '{{json .Config.Env}}'      # effective environment
docker stats ft-configs-ui --no-stream                            # CPU and memory

Updating FT Configs UI

Online hosts:

docker compose pull ft-configs-ui
docker compose up -d ft-configs-ui
curl -s http://localhost:3001/health

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

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

To roll back, set the previous tag in compose.yml and run docker compose up -d ft-configs-ui again. The container holds no state, so a rollback needs no data step.

Production Checklist

  • BACKEND_URL points at the correct FT Configs Service instance and is scheme://host[:port] with no path.

  • A CA-issued tls.crt + tls.key are in ft-configs-ui/certs/, and tls.key is chmod 600 — or the pair is deliberately absent because TLS is terminated upstream.

  • Both mappings are published: 3001:80 and, when TLS is served here, 3443:443.

  • The backend’s CORS_ALLOWED_ORIGINS lists the browser-facing UI host and port, and COOKIE_SECURE=true when the UI is served over HTTPS.

  • The image tag is pinned to a release tag, never latest.

  • METRICS_ALLOW_IP is narrowed from the default 0.0.0.0/0 to the monitoring scraper’s CIDR.

  • restart: unless-stopped is set.

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

  • The UI and the backend share the ft-network bridge, or the backend host is reachable by name.

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

Troubleshooting

Start with:

docker compose logs --tail 100 ft-configs-ui
docker inspect ft-configs-ui --format '{{json .Config.Env}}'

Container Fails to Start

Symptom: the container starts and immediately exits.

Check:

docker compose logs ft-configs-ui
docker compose exec ft-configs-ui cat /etc/nginx/nginx.conf

Fix:

  • A malformed BACKEND_URL breaks the envsubst rendering and leaves a config nginx refuses to load. It must be scheme://host[:port] with no path and no trailing slash.

  • If you mounted certificates, confirm both files exist at /etc/nginx/ssl/tls.crt and /etc/nginx/ssl/tls.key and are readable. A missing file is not fatal — the entrypoint falls back to HTTP-only — but an unreadable or non-PEM file stops nginx.

UI Loads but Requests Fail

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

Check:

curl -s http://localhost:3001/runtime-config.js
docker exec ft-configs-ui wget -qO- "$BACKEND_URL/configs-service/actuator/health" 2>&1 \
  || echo "Backend unreachable"

Fix:

  • Verify BACKEND_URL points at a running FT Configs Service.

  • If the backend runs on the host outside Docker, use host.docker.internal (with extra_hosts on Linux) or the host IP — never localhost.

  • If both run in Docker, put them on the same network and use the backend’s container name.

Port Already in Use

Symptom: address already in use when starting the container.

Check: lsof -i :3001 or docker ps.

Fix: stop the conflicting process, or set a free FT_CONFIGS_UI_HTTP_PORT / FT_CONFIGS_UI_HTTPS_PORT in ft-configs-ui/.env. If you change the UI port, update CORS_ALLOWED_ORIGINS on the backend to match.

Blank Page or JavaScript Errors

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

Check:

docker compose exec ft-configs-ui cat /usr/share/nginx/html/runtime-config.js

Fix: entrypoint.sh regenerates that file on every start, so an empty or missing one means the entrypoint did not run — check docker compose logs ft-configs-ui for the [runtime-config] API_URL=/configs-service line. src/lib/runtime-config.ts rejects a malformed object with an explicit error, which the console will show verbatim.

CORS Errors

Symptom: the browser console shows Access-Control-Allow-Origin errors.

Fix: in the supported deployment there is no cross-origin request at all — the browser calls the fixed relative path /configs-service, same origin as the SPA, and nginx proxies it. A CORS error therefore means the request did not go through the proxy: check that the /configs-service/ location is present in the rendered /etc/nginx/nginx.conf and that BACKEND_URL is set. If you genuinely need cross-origin access, the backend’s CORS_ALLOWED_ORIGINS must list the UI origin and allow credentials — the axios client sends withCredentials: true.

Login Fails with 401 or 403

Symptom: the login form submits and the backend answers 401 or 403.

Check: browser DevTools → Network: the /configs-service/csrf call must succeed before the login POST, and the session cookie must be sent back.

Fix:

  • Confirm the CSRF endpoint is reachable through the proxy.

  • Confirm the backend accepts cookies from the UI origin — the SameSite and Secure attributes must match how the UI is served. Over plain HTTP, COOKIE_SECURE=true on the backend makes the browser drop the cookie.

  • Clear the browser cookies and retry.

Getting Support

Collect the following before contacting the Friendly Technologies support team:

docker compose logs --tail 500 ft-configs-ui > ft-configs-ui.log
docker compose exec ft-configs-ui cat /etc/nginx/nginx.conf > nginx.rendered.conf
docker compose exec ft-configs-ui cat /usr/share/nginx/html/runtime-config.js
docker inspect ft-configs-ui --format '{{json .Config.Env}}'
curl -s http://localhost:3001/version.json

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