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:
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:
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:
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:8000and 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:
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:
- PostgreSQL, Redis and Kafka.
- Discovery.
- Client-service.
- SMS router.
- Aggregator integration.
- 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.confallows 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.