Skip to content

Run with Docker

This repository includes a main Docker Compose file and extra files for production, infrastructure running on or outside the Docker host, and end-to-end tests. The main Compose file builds the five application images and the MkDocs site. It also starts PostgreSQL, Redis, and one Kafka broker.

Compose files

File What it does When to use it
docker-compose.yml Builds and starts the complete development setup, including the applications, MkDocs, health checks, automatic recovery, and Newman tests Development and integration testing; it makes infrastructure and documentation ports available on the host
docker-compose.prod.yml Hides infrastructure and Eureka ports from the host, limits resources and logs, and disables Newman by default A safer starting point for production on one host; you must still review secrets, TLS, Kafka storage, and Docker socket access
docker-compose.host-infra.yml Connects the postgres, redis, and kafka names to ${HOST_IP} and does not start those services in Compose Application containers with PostgreSQL, Redis, and Kafka running on the Docker host
docker-compose.external.yml Connects infrastructure names to an external IP address currently written in the file A specific external environment; update it for your network before use
docker-compose.e2e.yml Adds end-to-end test behavior and a mock provider Testing only

The extra Compose files use YAML tags such as !reset and !override. Use a recent Docker Compose plugin. Before deployment, check the final configuration created by combining the files:

docker compose version
docker compose \
  --env-file .env.prod \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  config --quiet

Set production values

Copy the included template and protect the new environment file:

cp .env.example .env.prod
chmod 600 .env.prod

Replace every <CHANGE_ME> value. Set at least these values:

POSTGRES_DB=sms_gateway
POSTGRES_USER=appuser
POSTGRES_PASSWORD=<strong-random-database-password>

# Base64-encoded HS512 key; use the same value for the client and gateway.
JWT_SECRET_KEY=<output-of-openssl-rand-base64-64>

# Use the same internal management key for the client, router, and aggregator.
ADMIN_API_KEY=<output-of-openssl-rand-hex-32>

MNPSP_BASE_URL=https://api.mnpspbd.com
MNPSP_AGGREGATOR_API_KEY=<infozillion-api-key>

Use these commands to generate strong values. Do not copy the results into documentation or source control:

openssl rand -base64 64
openssl rand -hex 32
openssl rand -base64 24

The main Compose file provides a development value for JASYPT_ENCRYPTOR_PASSWORD, but .env.example does not include it. For production, add a strong JASYPT_ENCRYPTOR_PASSWORD to .env.prod. The router and aggregator must use the same value so they can encrypt and decrypt stored carrier credentials.

Build and start the base stack

For local development, Docker Compose loads the root .env file automatically:

docker compose -f docker-compose.yml config --quiet
docker compose -f docker-compose.yml build
docker compose -f docker-compose.yml up -d
docker compose -f docker-compose.yml ps

The main up command also starts the newman service after the applications become healthy. Newman runs the tests once and then stops; this is expected. To start only the services that should keep running, list them in the command:

docker compose -f docker-compose.yml up -d \
  docs postgres redis kafka discovery-service client-service \
  sms-router-service aggregator-integration-service gateway-service autoheal

The main Compose setup makes MkDocs available on ${MKDOCS_PORT:-8001}, which uses port 8001 by default. It also makes the PostgreSQL, Redis, Kafka, Eureka, and gateway ports available on the host. After the documentation container becomes healthy, open http://localhost:8001/. The image builds a static site with mkdocs build --strict. Rebuild the documentation container after changing a source file:

docker compose up -d --build docs

Do not run the main Compose setup unchanged on an untrusted machine because it exposes internal ports.

Build and start the production setup

docker compose \
  --env-file .env.prod \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  build

docker compose \
  --env-file .env.prod \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  up -d

docker compose \
  --env-file .env.prod \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  ps

The combined production configuration:

  • Keeps gateway port 8000:8000 and the MkDocs port available on the host.
  • Hides PostgreSQL, Redis, Kafka, and Eureka ports from the host.
  • Adds CPU and memory limits and reserved amounts.
  • Limits JSON logs to five files of 50 MiB each for each configured service.
  • Prevents the Newman test runner from starting unless its profile is enabled.

The application Dockerfiles already run their processes as appuser:appgroup. The production Compose file does not set one user for every infrastructure image. Check the user for each image before claiming that every container runs without root access.

Stop the production setup with the same Compose files and environment file:

docker compose \
  --env-file .env.prod \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  down

Add --volumes only when you want to delete PostgreSQL and Kafka data. You cannot recover that data from Docker after the volumes are deleted unless you have a backup.

Startup order and health checks

The main Compose file waits for services to become healthy in this order:

  1. PostgreSQL, Redis and Kafka.
  2. Discovery.
  3. Client-service.
  4. SMS router.
  5. Aggregator integration.
  6. Gateway.

Use these commands to check the services and read recent logs:

docker compose ps
docker compose logs --tail=200 discovery-service client-service sms-router-service
docker compose logs --tail=200 aggregator-integration-service gateway-service
docker compose exec gateway-service \
  curl --fail http://localhost:8000/actuator/health/liveness
docker compose exec client-service \
  curl --fail http://localhost:9014/actuator/health/readiness

The gateway and aggregator health checks confirm that the process is running and that Eureka reports it as UP. Both services have autoheal=true. The autoheal container restarts them if they remain unhealthy. Docker's restart: unless-stopped setting restarts only a stopped process; it does not restart a process that is still running but unhealthy.

Docker socket access

autoheal mounts /var/run/docker.sock. This gives the container control over Docker on the host. Review this security risk, protect the host, or replace autoheal with the health-recovery feature provided by your container platform.

Client image port mismatch

services/client-service/Dockerfile declares EXPOSE 8081 and checks service health on port 8081. However, the development profile and Compose run the client service on port 9014. The main Compose file replaces the image health check and registers port 9014, so the service works correctly when started with Compose.

If you run the image without Compose, choose one of these options:

  • Change the health check to http://localhost:9014/actuator/health/liveness.
  • Run the service on the port declared by the image and update service discovery and routing.
  • Change the Dockerfile to use port 9014.

The EXPOSE instruction only documents a port in the image. It does not make the port available on the host or change the Spring application port.

Use infrastructure on the Docker host

Set HOST_IP to an address the containers can reach. Do not use 127.0.0.1, because that address points back to each container:

cp .env.host .env.host.local
# Edit HOST_IP and every credential/provider value.

docker compose \
  --env-file .env.host.local \
  -f docker-compose.yml \
  -f docker-compose.host-infra.yml \
  config --quiet

docker compose \
  --env-file .env.host.local \
  -f docker-compose.yml \
  -f docker-compose.host-infra.yml \
  up -d discovery-service client-service sms-router-service \
        aggregator-integration-service gateway-service autoheal

Before starting, check the following:

  • PostgreSQL listens on an address the containers can reach, and pg_hba.conf allows connections from the Docker bridge network.
  • Redis listens on an address the containers can reach and is protected with authentication and firewall rules.
  • Kafka publishes ${HOST_IP}:9092, not localhost, as the address containers should use.
  • The firewall allows the Docker bridge network to connect only to the required infrastructure ports.
  • The database, schemas, and users described in the installation overview have been created.

Use external infrastructure

docker-compose.external.yml currently connects infrastructure names to 47.181.76.216. This address is specific to one environment. Review or replace every extra_hosts entry and create a separate protected environment file before starting the application services.

For a new environment, prefer DNS names in service environment variables instead of saving an IP address in the repository.

Run containers without Compose

To run the containers on one host without Compose, first create a Docker network and start the infrastructure containers. The following commands create a setup similar to the main development setup. Replace every example secret:

docker network create sms-gateway-network

docker run -d --name postgres --network sms-gateway-network \
  -e POSTGRES_DB=sms_gateway \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD='<database-password>' \
  -v sms-gateway-pg-data:/var/lib/postgresql/data \
  -v "$(pwd)/scripts/init-db.sql:/docker-entrypoint-initdb.d/01-init.sql:ro" \
  postgres:16-alpine

docker run -d --name redis --network sms-gateway-network \
  redis:7-alpine

docker run -d --name kafka --network sms-gateway-network \
  -e KAFKA_NODE_ID=1 \
  -e KAFKA_PROCESS_ROLES=broker,controller \
  -e KAFKA_LISTENERS='PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093' \
  -e KAFKA_ADVERTISED_LISTENERS='PLAINTEXT://kafka:9092' \
  -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
  -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP='CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT' \
  -e KAFKA_CONTROLLER_QUORUM_VOTERS='1@kafka:9093' \
  -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT \
  -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
  -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \
  -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \
  -e KAFKA_AUTO_CREATE_TOPICS_ENABLE=true \
  -e CLUSTER_ID='4L6g3nShT-eMCtK--X86sw' \
  -v sms-gateway-kafka-data:/var/lib/kafka/data \
  confluentinc/cp-kafka:7.5.0

From the repository root, build the application images:

docker build -f services/discovery-service/Dockerfile \
  -t sms-gateway/discovery-service:local .
docker build -f services/client-service/Dockerfile \
  -t sms-gateway/client-service:local .
docker build -f services/sms-router-service/Dockerfile \
  -t sms-gateway/sms-router-service:local .
docker build -f services/aggregator-integration-service/Dockerfile \
  -t sms-gateway/aggregator-integration-service:local .
docker build -f Dockerfile \
  -t sms-gateway/gateway-service:local .

Copy each deploy/per-service/*.env file to a protected deployment directory and replace every <CHANGE_ME> value. Make sure services that share JWT, admin, or Jasypt secrets use the same values. Then start the containers in the required order:

docker run -d --name discovery-service --network sms-gateway-network \
  --restart unless-stopped \
  --env-file deploy/per-service/discovery.env \
  sms-gateway/discovery-service:local

docker run -d --name client-service --network sms-gateway-network \
  --restart unless-stopped \
  --env-file deploy/per-service/client.env \
  -e KAFKA_BOOTSTRAP_SERVERS=kafka:9092 \
  --health-cmd='curl -f http://localhost:9014/actuator/health/liveness || exit 1' \
  sms-gateway/client-service:local

docker run -d --name sms-router-service --network sms-gateway-network \
  --restart unless-stopped \
  --env-file deploy/per-service/router.env \
  sms-gateway/sms-router-service:local

docker run -d --name aggregator-integration-service --network sms-gateway-network \
  --restart unless-stopped \
  --env-file deploy/per-service/aggregator.env \
  sms-gateway/aggregator-integration-service:local

docker run -d --name gateway-service --network sms-gateway-network \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file deploy/per-service/gateway.env \
  sms-gateway/gateway-service:local

deploy/per-service/aggregator.env lists Redis settings for TPS limits, but the aggregator does not currently use Redis to limit message sending. Keep these settings for compatibility and possible future use. Do not rely on them to limit production traffic.

Production checklist

  • Put a TLS reverse proxy or load balancer in front of port 8000.
  • Keep all application, Eureka, and infrastructure ports private except the gateway port.
  • Store secrets in your container platform's secret manager instead of permanent environment files when possible.
  • Use separate PostgreSQL users with only the permissions they need. Back up the database and test that you can restore it.
  • Require authentication and encryption for Redis and Kafka. Run enough Kafka brokers and topic copies to handle failures.
  • Create outbound, reporting, and dead-letter topics in advance with suitable partition counts and retention periods.
  • Monitor Kafka consumer delay, old or failed outbox records, overdue delivery reports, pending refunds, reporting dead-letter messages, and Eureka registration.
  • Fix or override the client image port mismatch.
  • Review the Docker socket security risk and use fixed image versions or digests.

See the service configuration guides for all available settings.