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 |
Prerequisites
Host Requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
Docker Engine |
20.10 |
20.10 or newer |
|
Docker Compose |
2.0 |
v2 ( |
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 |
Disk |
verify with FT DevOps |
verify with FT DevOps |
Sized by the |
|
Node.js is required only to produce the Vite bundle from source. The runtime image is |
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 |
8087 → 8080 (HTTP). On the |
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 |
8080 on the bridge, 8087 across hosts |
HTTP |
Outbound: nginx |
|
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 |
FT Configs UI |
3443 → 443 |
HTTPS |
Inbound from browsers: the TLS listener, active only when |
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 |
|
Password |
|
|
The |
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 |
-
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=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 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 ft-configs-ui.tar.gz ft-configs-ui-$TAG.tar. -
Transfer the archive to the offline host, together with
compose.yml, the shared.env,ft-configs-ui/.env, and theft-configs-ui/certs/directory holdingtls.crtandtls.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=latest gzip -dc "ft-configs-ui-$TAG.tar.gz" | docker load docker compose up -d ft-configs-uiWindows (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 |
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 |
|---|---|---|
|
Stack definition — image tag, published ports, the |
Yes |
|
Shared stack environment: |
Yes |
|
FT Configs UI only: |
Yes |
|
|
Yes |
Environment Configuration
The stack uses two environment layers, and this container reads both:
| File | Holds |
|---|---|
|
Shared by the whole stack: |
|
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 |
|---|---|---|---|
|
Upstream URL used by nginx |
No |
|
|
Extra CIDR added to the |
|
No |
|
Published host port mapped to container port 80. Read by Docker Compose, not by the container |
|
No |
|
Published host port mapped to container port 443. Read by Docker Compose, not by the container |
|
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 |
|
Check the startup log for |
|
The file is missing, malformed, or not PEM |
Validate it with |
|
The key does not match the certificate, or it is encrypted with a passphrase |
Compare the moduli: |
The browser shows |
A self-signed certificate, including the output of |
Import the certificate into the OS or browser trust store, or replace the pair with one issued by a trusted CA |
Deployment
Startup Dependencies
-
The image must be available locally (pulled or loaded — see Registry Authentication).
-
FT Configs Service must be healthy. The platform
compose.ymlgates the UI on it withdepends_on: ft-configs-service: condition: service_healthy, so Compose starts the UI only after the backend’s health check passes. -
The certificate pair is not a startup dependency: with no
tls.crt/tls.keythe container starts in HTTP-only mode. -
The backend hostname does not have to resolve at start: nginx resolves the
proxy_passupstream 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.
|
|
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:
|
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 |
Public |
3443 → 443 |
HTTPS |
TLS listener; present only when |
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 |
8080 on the bridge, 8087 across hosts |
HTTP |
|
|
443 |
HTTPS |
Image pull and update |
HTTP Endpoints
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
|
The SPA and its assets, with |
None |
GET |
|
The injected |
None |
GET |
|
Version and build number shown in the UI |
None |
GET |
|
Liveness; answered by nginx itself with |
None |
GET |
|
Readiness; proxied to |
None |
GET |
|
nginx |
IP allow-list |
any |
|
Reverse-proxied to |
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_URLpoints at the correct FT Configs Service instance and isscheme://host[:port]with no path. -
A CA-issued
tls.crt+tls.keyare inft-configs-ui/certs/, andtls.keyischmod 600— or the pair is deliberately absent because TLS is terminated upstream. -
Both mappings are published:
3001:80and, when TLS is served here,3443:443. -
The backend’s
CORS_ALLOWED_ORIGINSlists the browser-facing UI host and port, andCOOKIE_SECURE=truewhen the UI is served over HTTPS. -
The image tag is pinned to a release tag, never
latest. -
METRICS_ALLOW_IPis narrowed from the default0.0.0.0/0to the monitoring scraper’s CIDR. -
restart: unless-stoppedis set. -
Resource limits are set (
deploy.resources.limitsor--memory) — nothing is set by default. -
The UI and the backend share the
ft-networkbridge, 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_URLbreaks theenvsubstrendering and leaves a config nginx refuses to load. It must bescheme://host[:port]with no path and no trailing slash. -
If you mounted certificates, confirm both files exist at
/etc/nginx/ssl/tls.crtand/etc/nginx/ssl/tls.keyand 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_URLpoints at a running FT Configs Service. -
If the backend runs on the host outside Docker, 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.
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
SameSiteandSecureattributes must match how the UI is served. Over plain HTTP,COOKIE_SECURE=trueon 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.