Installation & Deployment

Overview

This guide walks you through installing and running FT Configs UI — a Vite + React single-page application for managing TR-069/CWMP device configuration. Docker is the recommended deployment method for consistency and ease of use.

The deployment includes:

  • FT Configs UI — static SPA served by nginx:alpine, which also reverse-proxies /configs-service/ to the backend

  • Docker container — Pre-built image from Harbor registry or loaded from archive

FT Configs UI is a frontend-only application that requires an external backend service (ft-configs-service) for all data operations.

Prerequisites

System Requirements

  • Docker Engine 20.10+

  • Docker Compose 2.0+

  • Minimum 512 MB RAM (1+ GB recommended)

  • 500 MB free disk space for the image

Node.js is only required for local development and for producing a build from source. The production runtime image is nginx:alpine — it does not contain Node.js.

Required External Components

The following components must be installed and accessible before starting:

Component Purpose Default Port

ft-configs-service

Backend API for authentication, configuration CRUD, import/export

8080

Supported Operating Systems

  • Linux (recommended)

  • macOS

  • Windows with WSL2

Before starting, make sure:

  • Docker Engine 20.10+ and Docker Compose 2.0+ installed. If Docker is not installed, follow the Docker Installation Guide.

  • You have network access to ft-configs-service backend.

Quick Start

For experienced users who already have Docker installed and the backend running.

# 1. Get the Docker image (choose one):

#    Option A: Pull from Harbor registry (recommended)
#    Read-only pull credentials:
#      Username: readonly
#      Password: fokxuw-fymte1-taSxyc
docker login hub.friendly-tech.com
docker pull hub.friendly-tech.com/configs/ft-configs-ui:latest

#    Option B: Load from archive (for offline servers — see "Transfer Image to Offline Server" below)
#    gzip -dc ft-configs-ui-<version>.tar.gz | docker load

# 2. Start the application
docker run --rm -p 80:80 \
  -e BACKEND_URL="http://ft-configs-service:8080" \
  hub.friendly-tech.com/configs/ft-configs-ui:latest

# 3. Verify
curl -s http://localhost/ | head -5

After startup the application is available at http://localhost/.

Preparation

1. Get the Docker Image

# Authenticate with the registry.
# Read-only pull credentials:
#   Username: readonly
#   Password: fokxuw-fymte1-taSxyc
docker login hub.friendly-tech.com

# Pull the latest image (optional if using docker compose —
# "docker compose up" will pull the image automatically)
docker pull hub.friendly-tech.com/configs/ft-configs-ui:latest

To pin to a specific release version:

docker pull hub.friendly-tech.com/configs/ft-configs-ui:v1.0.0

The version v1.0.0 is an example. Always use the version tag that corresponds to your deployment.

Optionally, retag the image for shorter references in docker run commands:

docker tag hub.friendly-tech.com/configs/ft-configs-ui:latest ft-configs-ui:latest

If you skip retagging, use the full image name in all subsequent commands.

The readonly account above provides pull-only access and is sufficient for installation and upgrades. To request elevated Harbor access (for example, push rights or a dedicated robot token), contact the DevOps team.

Option B: Transfer Image to Offline Server

Use this option when the target server has no internet access and cannot pull images from Harbor directly. The image is pulled on a machine that does have Harbor access, exported to a tar archive, transferred to the offline server, and loaded there.

  1. On a machine with Harbor access, log in:

    # Read-only pull credentials:
    #   Username: readonly
    #   Password: fokxuw-fymte1-taSxyc
    docker login hub.friendly-tech.com
  2. Pull the image for the target server’s architecture:

    docker pull --platform linux/amd64 hub.friendly-tech.com/configs/ft-configs-ui:<version>

    An explicit --platform matching the offline target server’s architecture is required. The image in Harbor is multi-arch (linux/amd64, linux/arm64); without --platform, docker pull selects the host architecture, which may not match the target. For example, on an Apple Silicon (arm64) Mac without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The example uses linux/amd64; replace it with the platform of your offline target server (linux/arm64, etc.).

  3. Save the image to a tar archive and compress it:

    docker save hub.friendly-tech.com/configs/ft-configs-ui:<version> -o ft-configs-ui-<version>.tar
    gzip ft-configs-ui-<version>.tar
  4. Transfer ft-configs-ui-<version>.tar.gz to the offline server (e.g., via scp or removable media).

  5. On the offline server, load the image:

    gzip -dc ft-configs-ui-<version>.tar.gz | docker load

Option C: Build from Source (developers only)

End-to-end build (Node builder + nginx runner in one image):

git clone <repository-url> ft-configs-ui
cd ft-configs-ui

docker build -f docker/Dockerfile -t ft-configs-ui:local .

If you already have a pre-built dist/ (for example, produced in CI), use the thin runner image:

npm ci
npm run build

docker build \
  -f docker/Dockerfile.simple \
  --build-arg DIST_DIR=dist \
  -t ft-configs-ui:local .

Building from source requires Node.js 20 to install dependencies. Loading a pre-built image is faster and avoids build-environment issues.

After loading or pulling, verify the image is available:

docker images | grep ft-configs-ui

2. Configure Environment

FT Configs UI reads its configuration from environment variables passed to the container. The main knob is:

  • BACKEND_URL — used by nginx proxy_pass for the /configs-service/ location. This is the real upstream the proxy talks to.

The browser’s API base path is not configurable: entrypoint.sh writes a fixed window.RUNTIME_CONFIG.API_URL = "/configs-service" into runtime-config.js, matching the nginx /configs-service/ location and the backend context-path. The browser calls the relative path /configs-service/…​, nginx intercepts that and forwards it to BACKEND_URL.

Variable Description Default Required

BACKEND_URL

Upstream URL used by nginx proxy_pass for /configs-service/

http://ft-configs-service:8080

No

METRICS_ALLOW_IP

Extra CIDR added to the allow-list of /nginx_status

0.0.0.0/0

No

If the backend runs on the host machine (not in Docker), use one of the following as the hostname in BACKEND_URL:

  • host.docker.internal — works on Docker Desktop (macOS, Windows) and on Linux with --add-host=host.docker.internal:host-gateway.

  • The host machine’s real IP address (e.g., 192.168.1.10).

localhost or 127.0.0.1 will not work — inside the container these point to the container itself, not the host.

Deployment

Create a compose.yml file:

services:
  ft-configs-ui:
    image: hub.friendly-tech.com/configs/ft-configs-ui:latest
    # build:
    #   context: .
    #   dockerfile: docker/Dockerfile
    container_name: ft-configs-ui
    ports:
      - "80:80"               # HTTP
      # - "443:443"           # Uncomment when mounting TLS certs (see HTTPS Deployment)
    environment:
      BACKEND_URL: "http://ft-configs-service:8080"
      # METRICS_ALLOW_IP: "10.0.0.0/8"
    # volumes:
    #   - ./certs:/etc/nginx/ssl:ro   # Mount tls.crt + tls.key to enable HTTPS
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:80/health"]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3

To change the external port, edit only the left side of the mapping (<host-port>:<container-port>), for example 8080:80 to serve on port 8080 externally.

# Start the application
docker compose up -d

# View logs
docker compose logs -f ft-configs-ui

# Stop
docker compose down

Option 2: docker run

docker run -d \
  --name ft-configs-ui \
  -p 80:80 \
  -e BACKEND_URL="http://ft-configs-service:8080" \
  --add-host=host.docker.internal:host-gateway \
  --restart unless-stopped \
  hub.friendly-tech.com/configs/ft-configs-ui:latest

If the default host port is busy, change only the left side: -p 8080:80.

If you retagged the image locally, use ft-configs-ui:latest instead of the full Harbor path.

Docker Networking

If the UI container must communicate with the backend on the same Docker network:

docker network create configs-net

Add --network configs-net to your docker run command or add the network section to compose.yml. Use the container name (e.g., ft-configs-service) as the hostname in BACKEND_URL.

# compose.yml (with shared network)
services:
  ft-configs-ui:
    image: hub.friendly-tech.com/configs/ft-configs-ui:latest
    ports:
      - "80:80"
    environment:
      BACKEND_URL: "http://ft-configs-service:8080"
    networks:
      - configs-net
    restart: unless-stopped

networks:
  configs-net:
    external: true

Use the backend container name (e.g. ft-configs-service) inside BACKEND_URL. Nginx is configured with resolver 127.0.0.11 (Docker’s embedded DNS) so the upstream name is resolved at request time, which lets the UI container start even before the backend is ready.

HTTPS Deployment

TLS is enabled automatically when the container starts with certificate files mounted at a known path. There is no HTTPS_ENABLED switch.

On startup, docker/ops/entrypoint.sh looks for both files:

  • /etc/nginx/ssl/tls.crt

  • /etc/nginx/ssl/tls.key

If both exist, it uses nginx.conf.template (listens on 80 and 443, TLSv1.2+1.3, ECDHE ciphers). If either is missing, it uses nginx-http-only.conf.template (listens on 80 only), which is the expected mode behind a Kubernetes Ingress where TLS is terminated upstream.

To enable HTTPS in Docker Compose:

  1. Generate or obtain certificate files. For local testing, docker/generate-cert.sh can produce a self-signed pair.

  2. Mount them read-only into /etc/nginx/ssl/:

        volumes:
          - ./certs:/etc/nginx/ssl:ro
        ports:
          - "80:80"
          - "443:443"

For further details (certificate provisioning, Kubernetes Ingress integration, troubleshooting), see HTTPS Deployment Guide.

Verify Deployment

After starting the container, run the following checks:

# 1. Check container status
docker ps -f name=ft-configs-ui
# Expected: container with status "Up" and port 0.0.0.0:80->80/tcp

# 2. Check the liveness endpoint
curl -s -o /dev/null -w "%{http_code}" http://localhost/health
# Expected: 200

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

# 4. Check readiness (proxied to backend /actuator/health)
curl -s -o /dev/null -w "%{http_code}" http://localhost/ready

# 5. Check application logs
docker logs ft-configs-ui --tail 20

# 6. Open in browser
# http://localhost/

If the UI loads but shows connection errors, verify that BACKEND_URL points to a running ft-configs-service instance. Check directly from inside the container with: docker exec ft-configs-ui wget -qO- "$BACKEND_URL/actuator/health"

Runtime Configuration Injection

At container start, docker/ops/entrypoint.sh:

  1. Writes /usr/share/nginx/html/runtime-config.js with a fixed window.RUNTIME_CONFIG = {"API_URL":"/configs-service"}; (the path must match the nginx /configs-service/ location and the backend context-path).

  2. Reads BACKEND_URL and METRICS_ALLOW_IP and exports them for envsubst.

  3. Detects TLS certificates at /etc/nginx/ssl/tls.crt + tls.key, then picks nginx.conf.template (HTTPS + HTTP) or nginx-http-only.conf.template (HTTP only).

  4. Renders the chosen template with envsubst to /etc/nginx/nginx.conf and execs nginx -g 'daemon off;'.

This mechanism allows the same Docker image to connect to different backends and to run with or without TLS without rebuilding.

index.html loads /runtime-config.js via a plain <script> synchronously before the app bundle, so window.RUNTIME_CONFIG.API_URL is available to the first module that imports src/lib/runtime-config.ts. The loaded object is validated against a Zod schema and rejected with a clear error if malformed.

Environment Variables (Complete Reference)

Runtime variables (container)

These are read by docker/ops/entrypoint.sh at container start.

Variable Description Default Required

BACKEND_URL

Upstream URL for nginx proxy_pass on /configs-service/. Accepts a container DNS name; nginx resolves it at request time via resolver 127.0.0.11.

http://ft-configs-service:8080

No

METRICS_ALLOW_IP

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

0.0.0.0/0

No

Build-time variables (Vite)

These are consumed by Vite at build time via import.meta.env.VITE_* and injected into the bundle. They are defined in vite.config.ts and read from version.json.

Variable Description Default

VITE_APP_VERSION

Application version string shown in the UI.

Read from version.json

VITE_APP_BUILD

Build identifier shown in the UI.

Read from version.json

TLS (no environment variables)

There is no HTTPS_ENABLED switch. TLS is activated when both /etc/nginx/ssl/tls.crt and /etc/nginx/ssl/tls.key are present at container start — mount them via a volume or a Kubernetes secret. The nginx templates configure TLSv1.2+1.3 and ECDHE-based ciphers. See HTTPS Deployment Guide for details.

Container Management

Command Description

docker compose up -d

Start the service in the background

docker compose down

Stop and remove containers

docker compose logs -f ft-configs-ui

Follow application logs

docker compose ps

Show container status

docker compose restart ft-configs-ui

Restart the application

docker compose pull

Update images to latest versions

docker logs --tail 100 ft-configs-ui

View last 100 log lines

docker stats ft-configs-ui --no-stream

View resource usage (CPU, memory)

docker inspect ft-configs-ui --format '{{json .Config.Env}}'

Check environment variables

Updating

docker compose pull downloads the latest image from the registry, then docker compose up -d recreates the container with the new version.

# 1. Pull the latest image
docker compose pull ft-configs-ui

# 2. Recreate the container with the new image
docker compose up -d ft-configs-ui

# 3. Verify
curl -s -o /dev/null -w "%{http_code}" http://localhost/health
# Expected: 200

Via docker load (offline servers)

When the upgrade target server has no access to the Harbor registry, the new image must be pulled on a machine that does have Harbor access, exported to a tar archive, transferred to the offline server, and loaded there.

  1. On a machine with Harbor access, log in:

    # Read-only pull credentials:
    #   Username: readonly
    #   Password: fokxuw-fymte1-taSxyc
    docker login hub.friendly-tech.com
  2. Pull the new image for the target server’s architecture:

    docker pull --platform linux/amd64 hub.friendly-tech.com/configs/ft-configs-ui:<new-version>

    An explicit --platform matching the offline target server’s architecture is required. The image in Harbor is multi-arch (linux/amd64, linux/arm64); without --platform, docker pull selects the host architecture, which may not match the target. For example, on an Apple Silicon (arm64) Mac without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The example uses linux/amd64; replace it with the platform of your offline target server (linux/arm64, etc.).

  3. Save the image to a tar archive and compress it:

    docker save hub.friendly-tech.com/configs/ft-configs-ui:<new-version> -o ft-configs-ui-<new-version>.tar
    gzip ft-configs-ui-<new-version>.tar
  4. Transfer ft-configs-ui-<new-version>.tar.gz to the offline server (e.g., via scp or removable media).

  5. On the offline server, load the new image and replace the running container:

    # 1. Load the new image from archive
    gzip -dc ft-configs-ui-<new-version>.tar.gz | docker load
    
    # 2. Replace the container
    docker stop ft-configs-ui
    docker rm ft-configs-ui
    
    docker run -d \
      --name ft-configs-ui \
      -p 80:80 \
      -e BACKEND_URL="http://ft-configs-service:8080" \
      --restart unless-stopped \
      hub.friendly-tech.com/configs/ft-configs-ui:<new-version>
    
    # 3. Verify
    curl -s -o /dev/null -w "%{http_code}" http://localhost/health

Rollback

docker stop ft-configs-ui
docker rm ft-configs-ui

# Re-run with the previous image tag
docker run -d --name ft-configs-ui \
  -p 80:80 \
  -e BACKEND_URL="http://ft-configs-service:8080" \
  --restart unless-stopped \
  hub.friendly-tech.com/configs/ft-configs-ui:<previous-version-tag>

Production Checklist

# Item Notes

1

Backend URL

Verify BACKEND_URL points to the correct ft-configs-service instance

2

Restart policy

restart: unless-stopped is already configured in the examples

3

Health check

Configured in Docker Compose example above

4

HTTPS

Mount TLS certificates into /etc/nginx/ssl/ to enable HTTPS (see HTTPS Deployment Guide). Behind a Kubernetes Ingress, leave TLS termination upstream and let the container run in HTTP-only mode.

5

Metrics allow-list

Restrict METRICS_ALLOW_IP from the default 0.0.0.0/0 to the CIDR of your monitoring scraper

6

Resource limits

Add --memory=512m or deploy.resources.limits in compose.yml for production

7

Log management

Collect container stdout/stderr via Docker logging driver or log aggregator

8

Network isolation

Place UI and backend on a dedicated Docker network

Troubleshooting

Start with quick diagnostics:

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

Common Issues

UI loads but shows "Network Error" or connection errors

Symptom: The login page appears, but login fails or data does not load.

Check:

# Verify runtime-config.js contains the correct URL
curl -s http://localhost/runtime-config.js

# Check if backend is reachable from inside the container
docker exec ft-configs-ui wget -qO- "$BACKEND_URL/actuator/health" 2>&1 || echo "Backend unreachable"

Solution:

  • Verify BACKEND_URL is correct and the backend is running.

  • If backend runs on host, use host.docker.internal instead of localhost.

  • If using Docker networking, ensure both containers share the same network.

Container exits immediately

Symptom: Container starts and immediately stops.

Check:

docker logs ft-configs-ui

Solution:

  • Inspect the generated config: docker exec ft-configs-ui cat /etc/nginx/nginx.conf. A syntax error in an envsubst-rendered template will stop nginx on startup.

  • If you mounted TLS certificates, verify the files exist at /etc/nginx/ssl/tls.crt and /etc/nginx/ssl/tls.key inside the container and that the mount is readable. When either is missing, entrypoint.sh automatically falls back to the HTTP-only template — a container that still exits usually means a broken BACKEND_URL or malformed envsubst variable.

Port conflict

Symptom: address already in use error.

Check:

lsof -i :80
# or
docker ps

Solution:

  • Stop the conflicting service, or expose a different host port: -p 8080:80.

Blank page or JavaScript errors

Symptom: Page loads but shows a blank screen or console errors.

Check:

  • Open browser DevTools → Console tab.

  • Look for RUNTIME_CONFIG errors.

  • Check if runtime-config.js is loaded: view page source, search for runtime-config.

Solution:

  • Verify /usr/share/nginx/html/runtime-config.js is present inside the container: docker exec ft-configs-ui cat /usr/share/nginx/html/runtime-config.js. entrypoint.sh regenerates it on every start, so an empty or missing file points at a failed entrypoint run — check docker logs.

  • Rebuild the image from scratch: docker build --no-cache -f docker/Dockerfile -t ft-configs-ui:local .

CORS errors

Symptom: Browser console shows Access-Control-Allow-Origin errors.

Solution:

  • In the recommended deployment the browser talks to nginx on the same origin as the UI and nginx proxies /configs-service/ to the backend, so there should be no cross-origin request in the first place. The browser always uses the fixed relative path /configs-service, so a CORS error usually points to nginx not proxying it — check the /configs-service/ location and BACKEND_URL.

  • If you really need cross-origin access, ensure ft-configs-service CORS settings allow the UI origin and that credentials are permitted (the axios client uses withCredentials: true).

Login fails with 401 or CSRF errors

Symptom: Login form submits but returns 401 or 403.

Check:

  • Browser DevTools → Network tab → verify /csrf call succeeds before login POST.

  • Verify withCredentials cookies are being sent (check cookie headers).

Solution:

  • Ensure backend CSRF endpoint is accessible from the UI.

  • Verify backend allows cookies from the UI origin (SameSite, Secure flags).

  • Clear browser cookies and retry.

Port Reference

Port Protocol Description

80

HTTP

nginx inside the production container (always listens)

443

HTTPS

nginx TLS listener — active only when /etc/nginx/ssl/tls.crt and tls.key are both mounted

9002

HTTP

Vite dev server (local development only — see Local Development)

When TLS certificates are mounted, the container listens on both port 80 and 443 simultaneously. Publishing both Docker mappings (-p 80:80 and -p 443:443) is safe.

Maintenance

Restart Services

# Restart via compose
docker compose restart ft-configs-ui

# Full restart (recreate container)
docker compose down
docker compose up -d

Stop Services

# Stop (keeps data)
docker compose down

# Stop and remove all data
docker compose down -v