Docker & Deploy (Advanced)

First-time setup? Start with the Installation & Deployment guide instead. This page covers advanced topics: building from source, developer Compose setup, and Kubernetes deployment notes.

ft-configs-service is a Spring Boot service that manages configuration data and exposes HTTP APIs under /configs-service. The recommended deployment approach is Docker (standalone docker run, or Docker Compose for local stacks). For profiles and the full configuration matrix, see Configuration.

2) Getting the Docker image

For pulling the release image from hub.friendly-tech.com (including registry login and offline transfer), see Installation & Deployment — Registry Authentication. The rest of this section covers building the image yourself.

Build locally (developer-only)

The canonical release image is built with Dockerfile.simple (Java 25, Temurin). Dockerfile.simple copy the application artifact from the build context as build/libs/ft-configs-service.jar, so you must prepare that file before running docker build. Or You can use Dockerfile which has 2 steps—build and run, so you dont need build project separately.

Build the boot jar (and place it at the path expected by the Dockerfiles)
./gradlew clean bootJar

JAR_PATH=$(find build/libs -maxdepth 1 -name "*.jar" ! -name "*-plain.jar" -print -quit)
cp "$JAR_PATH" build/libs/ft-configs-service.jar

Expected outcome:

  • build/libs/ft-configs-service.jar exists.

If the Gradle build fails to resolve internal dependencies from GitHub Packages, set GITHUB_USERNAME and GITHUB_TOKEN (see Build & Test).
Build the image (CI-equivalent Dockerfile)
docker build -f Dockerfile.simple \
  -t ft-configs-service:local \
  --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
  .

Expected outcome:

  • docker images shows ft-configs-service:local.

Verified runtime characteristics (from Dockerfile.simple and src/main/resources/application.yml):

  • Runtime base image: eclipse-temurin:25-jre-alpine

  • Default profile inside the image: SPRING_PROFILES_ACTIVE=mysql (override per environment)

  • Runs as non-root user appuser (uid/gid 1001)

  • Exposes port 8080 (context path /configs-service)

  • Image healthcheck: GET http://localhost:8080/configs-service/actuator/health

3) Working directory and docker run (production pattern)

For the production-friendly working directory layout, the --env-file pattern, and the Docker Compose deployment, see Installation & Deployment — Preparation and Deployment — Docker Compose.

Inline env vars (quick, local testing only)

Prefer --env-file (see the guide linked above) to avoid leaking secrets into shell history. This variant sets DB, Hazelcast, and JWT settings inline for a throwaway local run; adjust values to your environment.

IMAGE="<YOUR_REGISTRY>/configs/ft-configs-service:<tag>"   # or: "ft-configs-service:local"

docker run -d --name ft-configs-service \
  -e SPRING_PROFILES_ACTIVE=mysql \
  -e MYSQL_JDBC_URL="jdbc:mysql://<DB_HOST>:3306/configs?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC" \
  -e MYSQL_USER="<DB_USER>" \
  -e MYSQL_PASSWORD="<DB_PASSWORD>" \
  -e HZ_MEMBERS="<HZ_1>:5701,<HZ_2>:5701" \
  -e JWT_SECRET="<BASE64_JWT_SECRET>" \
  -p 8080:8080 \
  --restart unless-stopped \
  "$IMAGE"

4) Run with Docker Compose (developer/operator)

docker-compose.yaml starts:

  • ft-configs-service (HTTP :8080, context path /configs-service)

  • mysql:8.4 (TCP :3306, with a named volume for persistence)

It uses SPRING_PROFILES_ACTIVE=local and overrides the datasource via SPRING_DATASOURCE_*. It does not start a Hazelcast member, and it does not set HZ_MEMBERS by default.

Quick start

If you run Compose from this repo and keep build: . enabled, prepare the jar first (see [build-locally]).

docker compose up -d
docker compose logs -f ft-configs-service mysql
curl -fsS http://localhost:8080/configs-service/actuator/health

Create a small override file (for example docker-compose.hazelcast.yaml):

services:
  ft-configs-service:
    environment:
      HZ_MEMBERS: "hazelcast:5701"

  hazelcast:
    image: hazelcast/hazelcast:5.6.0
    restart: unless-stopped

Start with both files:

docker compose -f docker-compose.yaml -f docker-compose.hazelcast.yaml up -d

Stop and clean up

docker compose down

# Also remove the MySQL volume (drops local DB data)
docker compose down -v

5) Run & Verify

Health (Actuator):

curl -fsS http://localhost:8080/configs-service/actuator/health

Prometheus metrics (Actuator; exposed by default in this repo):

curl -fsS http://localhost:8080/configs-service/actuator/prometheus | head

Check logs:

docker logs -f ft-configs-service

Swagger UI (if enabled in your environment):

6) Troubleshooting

docker run --env-file …​ fails with "no such file or directory"

  • Cause: wrong path, missing file, or the file is not readable.

  • Fix:

    • Confirm the file exists: ls -l /usr/local/ft-configs-service/.env.mysql

    • Use an absolute path in --env-file.

Container is restarting / healthcheck is failing

  • Check logs: docker logs --tail=200 ft-configs-service

  • Common causes:

    • DB is not reachable or credentials are wrong (the service won’t start without a working datasource).

    • Liquibase migrations fail (schema mismatch, missing privileges, or wrong DB type/profile).

DB connection failures (MySQL / Oracle)

  • Cause: wrong profile or wrong JDBC URL for the selected profile.

  • Fix:

    • Ensure SPRING_PROFILES_ACTIVE matches your DB (mysql or oracle).

    • For mysql, set either MYSQL_JDBC_URL or MYSQL_HOST/MYSQL_PORT/MYSQL_SCHEMA plus MYSQL_USER/MYSQL_PASSWORD.

    • For oracle, set either ORACLE_JDBC_URL or ORACLE_HOST/ORACLE_PORT/ORACLE_SERVICE plus ORACLE_USER/ORACLE_PASSWORD.

    • In Compose, containers must connect to mysql:3306 (service name on the compose network), not the published host port.

Hazelcast client cannot connect

  • Cause: HZ_MEMBERS is unset or points to hosts not reachable from inside the container/pod.

  • Fix:

    • Set HZ_MEMBERS to endpoints reachable from the container network (for Compose, use hazelcast:5701).

    • If you changed the Hazelcast port, include it in each member entry.

Wrong profile selected

  • Symptom: Oracle settings applied while targeting MySQL (or vice versa).

  • Fix: set SPRING_PROFILES_ACTIVE explicitly in .env.mysql / your deployment manifest.

Port conflicts (bind: address already in use)

  • Fix:

    • Pick a different host port (example: -p 18080:8080).

    • Identify the process using the port with your OS tooling.

Compose build fails with "`build/libs/ft-configs-service.jar` not found"

  • Cause: the Dockerfiles copy build/libs/ft-configs-service.jar from the build context.

  • Fix: run the jar preparation step from [build-locally] before docker compose up.

7) Kubernetes deployment notes

This repo does not ship Kubernetes manifests. The runtime contract is:

  • Stateless pods (horizontally scalable).

  • External dependencies:

    • shared database (MySQL or Oracle)

    • shared Hazelcast cluster

Probes must include the servlet context path /configs-service:

  • Readiness/Liveness: /configs-service/actuator/health

  • Metrics: /configs-service/actuator/prometheus

readinessProbe:
  httpGet:
    path: /configs-service/actuator/health
    port: 8080
livenessProbe:
  httpGet:
    path: /configs-service/actuator/health
    port: 8080

If you enable separate readiness/liveness actuator groups in your environment, prefer:

  • /configs-service/actuator/health/readiness

  • /configs-service/actuator/health/liveness

At a minimum, model the following inputs as ConfigMaps/Secrets:

  • DB connectivity and credentials

  • HZ_MEMBERS

  • JWT_SECRET

8) JWT secret rotation

JWT_SECRET signs every access and refresh token. Persistent across normal redeploys (so live sessions survive restarts), but must be rotated on fresh infrastructure.

When to rotate

  • Fresh install — new database created from scratch, regardless of whether the application image is the same. Generate a new secret.

  • Suspected compromise — secret leaked via logs, backups, screenshots, or otherwise exposed.

  • Scheduled rotation — every 6-12 months as defence-in-depth, even without incident.

When NOT to rotate

Routine redeploy of the same service against the same database. Rotating the secret here forcibly logs out every active user without security benefit.

How to generate

openssl rand -base64 32

Store in your secret manager (Vault, AWS Secrets Manager, Kubernetes Secret) and expose to the pod as the JWT_SECRET env var.

Why this matters

users.tokens_valid_after plus the pwh JWT claim already prevent stale-token bypass on a fresh DB: a freshly bootstrapped admin gets a fresh tokens_valid_after and a new BCrypt salt, so old tokens fail both checks. JWT_SECRET rotation is defence in depth — it neutralises a separate threat: long-lived production secrets that may have leaked over time.

9) Environment variables

Do not commit secrets (*_PASSWORD, JWT_SECRET) into Git. Use .env locally, and use Secret managers (Kubernetes Secrets, Vault, etc.) in shared environments.

HZ_MEMBERS must be reachable from inside the container/pod (Docker and Kubernetes networking apply).

A) Quick start (most common knobs)

Variable Required Default Description

SPRING_PROFILES_ACTIVE

Yes

local

Active Spring profile: local, mysql, or oracle. Controls which application-*.yml is loaded. Must be set explicitly in Docker and Kubernetes.

SERVER_PORT

No

8080

Primary server port. When SERVER_SSL_ENABLED=true, this becomes the HTTPS port.

SERVER_HTTP_PORT

No

8080

Plain HTTP port. In dual-mode (HTTP + HTTPS), set this to a different value than SERVER_PORT (e.g., 8080 for HTTP, 8443 for HTTPS).

HZ_MEMBERS

No

from Hazelcast client config

Comma-separated Hazelcast members (host:port). Required in Docker/Kubernetes. Default port 5701 is added automatically if missing.

CORS_ALLOWED_ORIGINS

No

http://localhost:9002

Comma-separated list of allowed CORS origins. Must match the URL the user sees in the browser address bar (where the frontend is loaded from), never the internal Docker service name. Scheme-sensitive: http:// and https:// are different origins — the UI is served on both HTTP (default port 3001) and HTTPS (default port 3443, both published by Docker), so list both. Examples: http://localhost:9002 (dev), http://<ui-host>:3001,https://<ui-host>:3443 (Docker, HTTP + HTTPS UI), https://management.example.com (production).

LOGGING_LEVEL_COM_FRIENDLY_FTCONFIGSSERVICE

No

INFO

Log level for the application package. Values: ERROR, WARN, INFO, DEBUG. Can also be changed at runtime via Actuator.

B) Database (MySQL profile: SPRING_PROFILES_ACTIVE=mysql)

Provide either MYSQL_JDBC_URL or the decomposed MYSQL_HOST/MYSQL_PORT/MYSQL_SCHEMA inputs.

Variable Required Default Description

MYSQL_JDBC_URL

No

derived if unset

Full MySQL JDBC URL. Recommended for Docker/Kubernetes to avoid host, port, and schema drift.

DB_HOST

No

localhost

Shared DB host fallback used by MySQL and Oracle when profile-specific host is not set.

MYSQL_HOST

No

${DB_HOST:localhost}

MySQL host used if MYSQL_JDBC_URL is not set.

MYSQL_PORT

No

3306

MySQL port used if MYSQL_JDBC_URL is not set.

MYSQL_SCHEMA

No

configs

MySQL schema used if MYSQL_JDBC_URL is not set.

MYSQL_USER

No

ftacs (template: ftacs_configs)

MySQL username for the service. The env templates in docker/ ship with ftacs_configs.

MYSQL_PASSWORD

No

ftacs (template: ftacs_configs)

MySQL password for the service. The env templates in docker/ ship with ftacs_configs. Treat as a secret.

MYSQL_DRIVER_CLASS_NAME

No

com.mysql.cj.jdbc.Driver

Override JDBC driver class if needed.

DB_MAX_POOL_SIZE

No

10

HikariCP max pool size.

DB_MIN_IDLE

No

5

HikariCP minimum idle connections.

DB_CONNECTION_TIMEOUT_MS

No

30000

HikariCP connection timeout in milliseconds.

When both *_JDBC_URL and decomposed variables are set, the explicit JDBC URL always takes precedence.

C) Database (Oracle profile: SPRING_PROFILES_ACTIVE=oracle)

Provide either ORACLE_JDBC_URL or the decomposed ORACLE_HOST/ORACLE_PORT/ORACLE_SERVICE (plus optional ORACLE_SCHEMA) inputs.

Variable Required Default Description

ORACLE_JDBC_URL

No

derived if unset

Full Oracle JDBC URL. Recommended for Docker/Kubernetes to avoid host, port, and service drift.

ORACLE_HOST

No

${DB_HOST:localhost}

Oracle host used if ORACLE_JDBC_URL is not set.

ORACLE_PORT

No

1521

Oracle port used if ORACLE_JDBC_URL is not set.

ORACLE_SERVICE

No

XEPDB1

Oracle service name used if ORACLE_JDBC_URL is not set.

ORACLE_SCHEMA

No

empty

Optional schema mapped to hibernate.default_schema.

ORACLE_USER

No

ftacs

Oracle username for the service.

ORACLE_PASSWORD

No

ftacs

Oracle password for the service. Treat as a secret.

ORACLE_DRIVER_CLASS_NAME

No

oracle.jdbc.OracleDriver

Override Oracle JDBC driver class if needed.

D) Hazelcast / cache

Variable Required Default Description

CACHE_CONFIG_PATH

No

classpath:

Base path for Hazelcast config files. hazelcast-client.yaml is resolved relative to this path.

CACHE_IS_SERVER

No

false

When true, starts an embedded Hazelcast member from ${cache-config.path}hazelcast.yaml. Not used in production.

FT_CONFIGS_ACS_EXCLUSION_MODELS_CACHE_NAME

No

qoeProductClassGroupCache

Name of the Hazelcast cache from which ACS publishes product-class groups, consumed by the dataStoreExclusionModels dropdown. Must match the name ACS publishes to; the default matches the standard ACS deployment.

E) Auth / JWT & cookies

Variable Required Default Description

JWT_SECRET

No

defined in profile

Base64-encoded HMAC-SHA256 signing key. Must decode to at least 32 bytes (256-bit) or startup fails. Generate with openssl rand -base64 32. Must be unique per environment. Never commit to Git.

JWT_EXPIRATION

No

86400000

Access token expiration in milliseconds.

JWT_REFRESH_TOKEN_EXPIRATION

No

604800000

Refresh token expiration in milliseconds.

COOKIE_SECURE

No

true (template: false)

Set true when HTTPS is used end-to-end or via a trusted TLS-terminating proxy. The env templates ship with false for local development.

COOKIE_DOMAIN

No

empty

Optional domain for cross-subdomain cookie sharing.

Mail

Variable Required Default Description

MAIL_MODE

No

AUTO

Email delivery mode (case-insensitive; blank or unknown falls back to AUTO with a warning log). The supported values are AUTO and OFFLINE. AUTO infers the mode from SMTP config: it is treated as OFFLINE when MAIL_HOST, MAIL_USERNAME, or MAIL_PASSWORD are not all set (non-blank); when they are all set it attempts email, but if the SMTP server is unreachable at runtime it degrades to offline behaviour. OFFLINE never sends email. In offline mode (configured or degraded at runtime), create-user and reset-password return the temporary password in the API response (passwordDelivery=RETURNED) instead of emailing it, and user-deletion requests skip OTP (returning otpRequired=false); the account is deleted in the confirm step, called without an OTP.

MAIL_HOST

No

smtp.gmail.com

SMTP host used for registration and deletion flows. Under MAIL_MODE=AUTO, leaving this blank (together with the credentials) selects offline mode.

MAIL_PORT

No

587

SMTP port.

MAIL_USERNAME

No

SMTP username. Under MAIL_MODE=AUTO, leaving this blank selects offline mode (no email is sent; secrets are returned in API responses). The local profile uses an empty default (${MAIL_USERNAME:}).

MAIL_PASSWORD

No

SMTP password. Treat as a secret. Under MAIL_MODE=AUTO, leaving this blank selects offline mode. The local profile uses an empty default (${MAIL_PASSWORD:}).

MAIL_PROTOCOL

No

smtp

Usually smtp.

MAIL_SMTP_AUTH

No

true

SMTP authentication toggle.

MAIL_SMTP_STARTTLS

No

true

STARTTLS toggle.

MAIL_FROM

No

no-reply@friendly.local

From address used in sent emails.

MAIL_LOGIN_URL

No

empty

URL for the "Sign In" call-to-action button in emails. Must start with http:// or https://. If blank or non-HTTP, the CTA button is replaced by a generic text hint. Example: https://management.example.com/configs-service.

The MAIL_FROM_NAME variable has been removed. The sender display name is now localized via the mail.brand message key (mail_messages*.properties), returning "Configuration Center" (EN) or the localized equivalent for other locales.

Audit

Variable Required Default Description

AUDIT_RETENTION_DAYS

No

30

Number of days to retain audit events. Minimum 3, maximum 90. Set to 0 to disable retention. Values outside range are clamped with a warning.

AUDIT_REDACTION_FIELDS

No

empty

Comma-separated list of additional sensitive field names to redact in audit snapshots and change records. Added on top of the 18 built-in defaults (password, token, secret, apiKey, etc.).

Bootstrap admin (first install)

Variable Required Default Description

FT_CONFIGS_BOOTSTRAP_ADMIN_USERNAME

No

admin

Username for first startup when no admin exists.

FT_CONFIGS_BOOTSTRAP_ADMIN_PASSWORD

Yes (first startup)

empty

Password for bootstrap admin. Must be set on first startup or the admin account will not be created. Rotate immediately after bootstrap via /auth/first-login. Treat as a secret.

FT_CONFIGS_BOOTSTRAP_ADMIN_EMAIL

No

empty

Optional email assigned to bootstrap admin.

FT_CONFIGS_BOOTSTRAP_ADMIN_LOCALE

No

empty

Optional locale for bootstrap admin.

JVM / container runtime

Variable Required Default Description

JAVA_OPTS

No

set in Docker image

JVM options passed by docker-entrypoint.sh. Add only needed flags.

TZ

No

UTC

Container timezone.