From d980438a4d056b62d5b46991c2421c7d6b0d2b51 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Wed, 25 Oct 2023 20:04:14 -0700 Subject: [PATCH] redis connection pool implementation --- config/redis.conf | 18 ++++ config/redis.conf/.gitkeep | 0 environment/docker-compose.dev.yml | 4 +- environment/docker-compose.prod.yml | 68 -------------- src/jest/redisV4Mock.ts | 16 ++++ src/providers/cronjobs/CronJobs.ts | 7 +- src/providers/cronjobs/RedisCrons.ts | 15 +++ src/providers/database/InMemoryHashTable.ts | 27 +++--- src/providers/database/InMemoryRepository.ts | 2 +- src/providers/database/RedisManager.ts | 49 ---------- .../database/RedisManager/RedisBareClient.ts | 42 +++++++++ .../database/RedisManager/RedisIOClient.ts | 91 +++++++++++++++++++ .../database/RedisManager/RedisManager.ts | 35 +++++++ src/providers/inversify/container.ts | 2 +- swarm-stack.yml | 50 +++++----- 15 files changed, 261 insertions(+), 165 deletions(-) create mode 100644 config/redis.conf delete mode 100644 config/redis.conf/.gitkeep delete mode 100644 environment/docker-compose.prod.yml create mode 100644 src/providers/cronjobs/RedisCrons.ts delete mode 100644 src/providers/database/RedisManager.ts create mode 100644 src/providers/database/RedisManager/RedisBareClient.ts create mode 100644 src/providers/database/RedisManager/RedisIOClient.ts create mode 100644 src/providers/database/RedisManager/RedisManager.ts diff --git a/config/redis.conf b/config/redis.conf new file mode 100644 index 000000000..69d4a1877 --- /dev/null +++ b/config/redis.conf @@ -0,0 +1,18 @@ +# Set the maximum allowed number of connected clients. +# However, ensure your system has adequate resources to handle this. +maxclients 10000 + +# Set a timeout to close idle connections after a period (in seconds). +timeout 60 + +# Enable append only mode for better data durability. +appendonly yes + +# Set the frequency at which Redis fsyncs the AOF. +appendfsync everysec + +# Slow log logging level (0 = don't log, 1 = log the slower queries, 2 = log all) +slowlog-log-slower-than 10000 + +# Maximum length of the slow query log +slowlog-max-len 128 diff --git a/config/redis.conf/.gitkeep b/config/redis.conf/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/environment/docker-compose.dev.yml b/environment/docker-compose.dev.yml index 33a516be5..7ee5757a5 100644 --- a/environment/docker-compose.dev.yml +++ b/environment/docker-compose.dev.yml @@ -59,8 +59,8 @@ services: - "$REDIS_PORT" env_file: .env volumes: - - ./config/redis.conf:/redis.conf - command: ["redis-server", "/redis.conf", "--port", "${REDIS_PORT}"] + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--port", "${REDIS_PORT}"] networks: - rpg-network diff --git a/environment/docker-compose.prod.yml b/environment/docker-compose.prod.yml deleted file mode 100644 index 50c4930e2..000000000 --- a/environment/docker-compose.prod.yml +++ /dev/null @@ -1,68 +0,0 @@ -version: "3" - -services: - laundry-api: - container_name: laundry-api - restart: always - image: laundrobot/laundrobot-team:api-latest - logging: - options: - max-size: "10m" - max-file: "3" - env_file: .env - volumes: - - .:/usr/src/app - - /usr/src/app/node_modules - links: - - rpg-db - - rpg-redis - depends_on: - - rpg-db - - rpg-redis - network_mode: bridge - ports: - - "${SERVER_PORT}:${SERVER_PORT}" - - "$SOCKET_PORT:$SOCKET_PORT" - # - "$SOCKET_UDP_RANGE" #!COMMENT THIS IN IF USING UDP - environment: - FORCE_COLOR: "true" - VIRTUAL_HOST: ${API_SUBDOMAIN} - LETSENCRYPT_HOST: ${API_SUBDOMAIN} - LETSENCRYPT_EMAIL: ${ADMIN_EMAIL} - VIRTUAL_PORT: ${SERVER_PORT} - NEW_RELIC_LICENSE_KEY: "${NEW_RELIC_LICENSE_KEY}" - NEW_RELIC_APP_NAME: "Laundrobot.AI API" - - rpg-db: - container_name: rpg-db - image: mongo - restart: always - command: mongod --port ${MONGO_PORT} - logging: - options: - max-size: "10m" - max-file: "3" - volumes: - - ./docker_scripts/:/docker-entrypoint-initdb.d - ports: - - "$MONGO_PORT:$MONGO_PORT" - environment: - MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE} - MONGO_PORT: ${MONGO_PORT} - MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} - MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} - network_mode: bridge - - rpg-redis: - container_name: rpg-redis - restart: always - image: redis:latest - ports: - - "$REDIS_PORT:$REDIS_PORT" - expose: - - "$REDIS_PORT" - env_file: .env - volumes: - - ./config/redis.conf:/redis.conf - command: ["redis-server", "/redis.conf", "--port", "${REDIS_PORT}"] - network_mode: bridge diff --git a/src/jest/redisV4Mock.ts b/src/jest/redisV4Mock.ts index 96044ebdc..2279afb9a 100644 --- a/src/jest/redisV4Mock.ts +++ b/src/jest/redisV4Mock.ts @@ -29,5 +29,21 @@ const v4Client = { pSetEx: (key: string, ms: number, value: string) => setEx(key, ms / 1000, value), on: () => undefined, // Add additional functions as needed... + + // IORedis compatible functions + hset: promisify(client.hset).bind(client), + keys: promisify(client.keys).bind(client), + hkeys: promisify(client.hkeys).bind(client), + hsetnx: promisify(client.hsetnx).bind(client), + hget: promisify(client.hget).bind(client), + hdel: promisify(client.hdel).bind(client), + hgetall: promisify(client.hgetall).bind(client), + + hexists: promisify(client.hexists).bind(client), + flushall: promisify(client.flushall).bind(client), + setex: promisify(client.setex).bind(client), + mget: promisify(client.mget).bind(client), + pttl: promisify(client.pttl).bind(client), + psetex: (key: string, ms: number, value: string) => setEx(key, ms / 1000, value), }; export default { ...redis, createClient: () => v4Client }; diff --git a/src/providers/cronjobs/CronJobs.ts b/src/providers/cronjobs/CronJobs.ts index 89c790c94..8c367c3e3 100644 --- a/src/providers/cronjobs/CronJobs.ts +++ b/src/providers/cronjobs/CronJobs.ts @@ -2,10 +2,11 @@ import { appEnv } from "@providers/config/env"; import { PM2Helper } from "@providers/server/PM2Helper"; import { EnvType } from "@rpg-engine/shared"; import { provide } from "inversify-binding-decorators"; +import { RedisCrons } from "./RedisCrons"; @provide(Cronjob) export class Cronjob { - constructor(private pm2Helper: PM2Helper) {} + constructor(private pm2Helper: PM2Helper, private redisCrons: RedisCrons) {} public start(): void { this.scheduleCrons(); @@ -16,13 +17,13 @@ export class Cronjob { switch (appEnv.general.ENV) { case EnvType.Development: - // schedule here + this.redisCrons.schedule(); break; case EnvType.Staging: case EnvType.Production: // make sure it only runs in one instance if (process.env.pm_id === this.pm2Helper.pickLastCPUInstance()) { - // schedule here + this.redisCrons.schedule(); } break; } diff --git a/src/providers/cronjobs/RedisCrons.ts b/src/providers/cronjobs/RedisCrons.ts new file mode 100644 index 000000000..f17d8e140 --- /dev/null +++ b/src/providers/cronjobs/RedisCrons.ts @@ -0,0 +1,15 @@ +import { RedisManager } from "@providers/database/RedisManager/RedisManager"; +import { provide } from "inversify-binding-decorators"; +import { CronJobScheduler } from "./CronJobScheduler"; + +@provide(RedisCrons) +export class RedisCrons { + constructor(private cronJobScheduler: CronJobScheduler, private redisManager: RedisManager) {} + + public schedule(): void { + // every 5 minutes + this.cronJobScheduler.uniqueSchedule("redis-client-connection-count", "* * * * *", async () => { + await this.redisManager.getClientCount(); + }); + } +} diff --git a/src/providers/database/InMemoryHashTable.ts b/src/providers/database/InMemoryHashTable.ts index df94efcad..581735999 100644 --- a/src/providers/database/InMemoryHashTable.ts +++ b/src/providers/database/InMemoryHashTable.ts @@ -1,17 +1,18 @@ import { appEnv } from "@providers/config/env"; import { provide } from "inversify-binding-decorators"; -import { RedisManager } from "./RedisManager"; +import { RedisManager } from "./RedisManager/RedisManager"; @provide(InMemoryHashTable) export class InMemoryHashTable { constructor(private redisManager: RedisManager) {} public async set(namespace: string, key: string, value: any): Promise { - await this.redisManager.client.hSet(namespace?.toString(), key?.toString(), JSON.stringify(value)); + await this.redisManager.client.hset(namespace?.toString(), key?.toString(), JSON.stringify(value)); } public async setNx(namespace: string, key: string, value: any): Promise { - return await this.redisManager.client.hSetNX(namespace?.toString(), key?.toString(), JSON.stringify(value)); + const result = await this.redisManager.client.hsetnx(namespace?.toString(), key?.toString(), JSON.stringify(value)); + return result === 1; } public async expire(key: string, seconds: number, mode: "NX" | "XX" | "GT" | "LT"): Promise { @@ -25,13 +26,12 @@ export class InMemoryHashTable { throw new Error("Namespace is undefined or null."); } - const timeLeft = await this.redisManager.client.pTTL(namespace.toString()); - + const timeLeft = await this.redisManager.client.pttl(namespace.toString()); return timeLeft; } public async getAll(namespace: string): Promise | undefined> { - const values = await this.redisManager.client.hGetAll(namespace?.toString()); + const values = await this.redisManager.client.hgetall(namespace?.toString()); if (!values) { return; @@ -41,20 +41,17 @@ export class InMemoryHashTable { } public async has(namespace: string, key: string): Promise { - const result = await this.redisManager.client.hExists(namespace?.toString(), key?.toString()); - + const result = await this.redisManager.client.hexists(namespace?.toString(), key?.toString()); return Boolean(result); } public async hasAll(namespace: string): Promise { const result = await this.redisManager.client.exists(namespace?.toString()); - - // @ts-ignore return result === 1; } public async get(namespace: string, key: string): Promise | undefined> { - const value = await this.redisManager.client.hGet(namespace?.toString(), key?.toString()); + const value = await this.redisManager.client.hget(namespace?.toString(), key?.toString()); if (!value) { return; @@ -64,19 +61,17 @@ export class InMemoryHashTable { } public async getAllKeysWithPrefix(prefix: string): Promise { - const keys = await this.redisManager.client.keys?.(`${prefix}*`); - + const keys = await this.redisManager.client.keys(`${prefix}*`); return keys ?? []; } public async getAllKeys(namespace: string): Promise { - const keys = await this.redisManager.client.hKeys(namespace?.toString()); - + const keys = await this.redisManager.client.hkeys(namespace?.toString()); return keys ?? []; } public async delete(namespace: string, key: string): Promise { - await this.redisManager.client.hDel(namespace?.toString(), key?.toString()); + await this.redisManager.client.hdel(namespace?.toString(), key?.toString()); } public async deleteAll(namespace: string): Promise { diff --git a/src/providers/database/InMemoryRepository.ts b/src/providers/database/InMemoryRepository.ts index fce3df5d5..e48e09526 100644 --- a/src/providers/database/InMemoryRepository.ts +++ b/src/providers/database/InMemoryRepository.ts @@ -1,4 +1,4 @@ -import { RedisManager } from "@providers/database/RedisManager"; +import { RedisManager } from "@providers/database/RedisManager/RedisManager"; import { IResource } from "@rpg-engine/shared"; import { provide } from "inversify-binding-decorators"; diff --git a/src/providers/database/RedisManager.ts b/src/providers/database/RedisManager.ts deleted file mode 100644 index ab40ff8ae..000000000 --- a/src/providers/database/RedisManager.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable no-async-promise-executor */ -/* eslint-disable no-void */ -import { appEnv } from "@providers/config/env"; -import { provideSingleton } from "@providers/inversify/provideSingleton"; -import { RedisClientType } from "@redis/client"; -import { createClient } from "redis"; - -import mongoose from "mongoose"; -import { applySpeedGooseCacheLayer } from "speedgoose"; - -@provideSingleton(RedisManager) -export class RedisManager { - public client: RedisClientType; - - constructor() {} - - public connect(): Promise { - return new Promise(async (resolve, reject) => { - const redisConnectionUrl = `redis://${appEnv.database.REDIS_CONTAINER}:${appEnv.database.REDIS_PORT}`; - - this.client = createClient({ - url: redisConnectionUrl, - }); - - this.client.on("error", (err) => { - console.log("❌ Redis error:", err); - reject(err); // If you want connection failures to reject the Promise - }); - - try { - await this.client.connect(); - - // @ts-ignore - void applySpeedGooseCacheLayer(mongoose, { - redisUri: redisConnectionUrl, - }); - - if (!appEnv.general.IS_UNIT_TEST) { - console.log("✅ Redis Client Connected"); - } - - resolve(); - } catch (error) { - console.log("❌ Redis initialization error: ", error); - reject(error); // Reject the Promise if there's an error during connection - } - }); - } -} diff --git a/src/providers/database/RedisManager/RedisBareClient.ts b/src/providers/database/RedisManager/RedisBareClient.ts new file mode 100644 index 000000000..1546afa23 --- /dev/null +++ b/src/providers/database/RedisManager/RedisBareClient.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-async-promise-executor */ +import { appEnv } from "@providers/config/env"; +import { provideSingleton } from "@providers/inversify/provideSingleton"; +import mongoose from "mongoose"; +import { createClient } from "redis"; +import { applySpeedGooseCacheLayer } from "speedgoose"; + +//! in unit test, just use the traditional redis because its already being mocked by redisV4Mock and changing support to IORedis would be a pain + +@provideSingleton(RedisBareClient) +export class RedisBareClient { + public async connect(): Promise { + return await new Promise(async (resolve, reject) => { + let client; + + try { + const redisConnectionUrl = `redis://${appEnv.database.REDIS_CONTAINER}:${appEnv.database.REDIS_PORT}`; + + client = createClient({ + url: redisConnectionUrl, + }); + + client.on("error", (err) => { + console.log("❌ Redis error:", err); + reject(err); + }); + + await client.connect(); + + // @ts-ignore + void applySpeedGooseCacheLayer(mongoose, { + redisUri: redisConnectionUrl, + }); + + resolve(client); + } catch (error) { + console.log("❌ Redis initialization error: ", error); + reject(error); + } + }); + } +} diff --git a/src/providers/database/RedisManager/RedisIOClient.ts b/src/providers/database/RedisManager/RedisIOClient.ts new file mode 100644 index 000000000..aa221f1e2 --- /dev/null +++ b/src/providers/database/RedisManager/RedisIOClient.ts @@ -0,0 +1,91 @@ +import { NewRelic } from "@providers/analytics/NewRelic"; +import { appEnv } from "@providers/config/env"; +import { provideSingleton } from "@providers/inversify/provideSingleton"; +import { NewRelicMetricCategory, NewRelicSubCategory } from "@providers/types/NewRelicTypes"; +import IORedis from "ioredis"; +import mongoose from "mongoose"; +import { applySpeedGooseCacheLayer } from "speedgoose"; + +//! We use RedisIOClient because it has a built in pooling mechanism +@provideSingleton(RedisIOClient) +export class RedisIOClient { + public client: IORedis.Redis | null = null; + + constructor(private newRelic: NewRelic) {} + + public async connect(): Promise { + return await new Promise((resolve, reject) => { + try { + const redisConnectionUrl = `redis://${appEnv.database.REDIS_CONTAINER}:${appEnv.database.REDIS_PORT}`; + + this.client = new IORedis(redisConnectionUrl, { + maxRetriesPerRequest: null, // Disables the retry mechanism for individual commands; be cautious + enableAutoPipelining: true, // Enables command pipelining for better performance + keepAlive: 3000, // Keeps idle sockets open for 3 seconds; however, this doesn't close idle connections + connectTimeout: 10000, // 10 seconds to timeout if a connection can't be established + lazyConnect: true, // Connection will be lazily created on the first command + autoResendUnfulfilledCommands: false, // Prevents re-sending of queued commands on reconnect, avoiding command duplication + }); + + this.client.setMaxListeners(20); + + if (!appEnv.general.IS_UNIT_TEST) { + this.client.on("connect", () => { + if (!appEnv.general.IS_UNIT_TEST) { + console.log("✅ Redis Client Connected"); + } + }); + } + + this.client.on("error", (err) => { + console.log("❌ Redis error:", err); + + this.client?.disconnect(); + this.client?.removeAllListeners("error"); + + this.client = null; + + reject(err); + }); + + // @ts-ignore + void applySpeedGooseCacheLayer(mongoose, { + redisUri: redisConnectionUrl, + }); + + // track new client + this.newRelic.trackMetric(NewRelicMetricCategory.Count, NewRelicSubCategory.Server, "RedisClient", 1); + + resolve(this.client); + } catch (error) { + if (!appEnv.general.IS_UNIT_TEST) { + this.client?.removeAllListeners("error"); + } + + console.log("❌ Redis initialization error: ", error); + reject(error); + } + }); + } + + public async getTotalConnectedClients(): Promise { + let clientCount = 0; + + try { + const clientList = await this.client.client("LIST"); // Assuming this.client is the IORedis client + clientCount = clientList.split("\n").length - 1; // Each client info is separated by a newline + + console.log("📕 Redis - Total connected clients: ", clientCount); + + this.newRelic.trackMetric( + NewRelicMetricCategory.Count, + NewRelicSubCategory.Server, + "RedisClientCount", + clientCount + ); + } catch (error) { + console.error("Could not fetch the total number of connected clients", error); + } + return clientCount; + } +} diff --git a/src/providers/database/RedisManager/RedisManager.ts b/src/providers/database/RedisManager/RedisManager.ts new file mode 100644 index 000000000..f17ec887e --- /dev/null +++ b/src/providers/database/RedisManager/RedisManager.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-async-promise-executor */ +import { appEnv } from "@providers/config/env"; +import { provideSingleton } from "@providers/inversify/provideSingleton"; +import IORedis from "ioredis"; +import { RedisBareClient } from "./RedisBareClient"; +import { RedisIOClient } from "./RedisIOClient"; +@provideSingleton(RedisManager) +export class RedisManager { + public client: IORedis.Redis | null = null; + + constructor(private redisBareClient: RedisBareClient, private redisIOClient: RedisIOClient) {} + + public async connect(): Promise { + // if we do already have a client, just return it + if (this.client) { + return; + } + + if (appEnv.general.IS_UNIT_TEST) { + this.client = await this.redisBareClient.connect(); + } else { + this.client = await this.redisIOClient.connect(); + } + } + + public async getClientCount(): Promise { + return await this.redisIOClient.getTotalConnectedClients(); + } + + public async disconnect(): Promise { + if (this.client) { + await this.client.disconnect(); + } + } +} diff --git a/src/providers/inversify/container.ts b/src/providers/inversify/container.ts index a347d3ad0..efc7b7fd2 100644 --- a/src/providers/inversify/container.ts +++ b/src/providers/inversify/container.ts @@ -1,7 +1,7 @@ import { NewRelic } from "@providers/analytics/NewRelic"; import { InMemoryHashTable } from "@providers/database/InMemoryHashTable"; -import { RedisManager } from "@providers/database/RedisManager"; +import { RedisManager } from "@providers/database/RedisManager/RedisManager"; import { HashGenerator } from "@providers/hash/HashGenerator"; import { Locker } from "@providers/locks/Locker"; diff --git a/swarm-stack.yml b/swarm-stack.yml index c44b7fe3b..3ec79d353 100644 --- a/swarm-stack.yml +++ b/swarm-stack.yml @@ -1,8 +1,8 @@ version: "3.8" services: - laundry-api: - image: laundrobot/laundrobot-team:api-latest + rpg-api: + image: definya/definya-team:api-latest healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:5002"] interval: 60s @@ -28,23 +28,23 @@ services: labels: # Basic setup for API - traefik.enable=true - - traefik.http.routers.laundry-api.rule=Host(`api.laundrobot.ai`) - - traefik.http.routers.laundry-api.entrypoints=websecure - - traefik.http.routers.laundry-api.tls.certresolver=myresolver - - traefik.http.services.laundry-api-service.loadbalancer.server.port=5002 - - traefik.http.routers.laundry-api.service=laundry-api-service + - traefik.http.routers.rpg-api.rule=Host(`na.definya.com`) + - traefik.http.routers.rpg-api.entrypoints=websecure + - traefik.http.routers.rpg-api.tls.certresolver=myresolver + - traefik.http.services.rpg-api-service.loadbalancer.server.port=5002 + - traefik.http.routers.rpg-api.service=rpg-api-service # Websockets - - traefik.http.routers.laundry-api-socket.rule=Host(`api.laundrobot.ai`) && Path(`/socket.io/`) - - traefik.http.routers.laundry-api-socket.entrypoints=websecure - - traefik.http.routers.laundry-api-socket.tls.certresolver=myresolver - - traefik.http.services.laundry-api-socket-service.loadbalancer.server.port=5101 - - traefik.http.routers.laundry-api-socket.service=laundry-api-socket-service + - traefik.http.routers.rpg-api-socket.rule=Host(`na.definya.com`) && Path(`/socket.io/`) + - traefik.http.routers.rpg-api-socket.entrypoints=websecure + - traefik.http.routers.rpg-api-socket.tls.certresolver=myresolver + - traefik.http.services.rpg-api-socket-service.loadbalancer.server.port=5101 + - traefik.http.routers.rpg-api-socket.service=rpg-api-socket-service # Add sticky session for websockets - - "traefik.http.services.laundry-api-socket-service.loadbalancer.sticky=true" - - "traefik.http.services.laundry-api-socket-service.loadbalancer.sticky.cookie.name=StickyCookie" - - "traefik.http.services.laundry-api-socket-service.loadbalancer.sticky.cookie.secure=true" + - "traefik.http.services.rpg-api-socket-service.loadbalancer.sticky=true" + - "traefik.http.services.rpg-api-socket-service.loadbalancer.sticky.cookie.name=StickyCookie" + - "traefik.http.services.rpg-api-socket-service.loadbalancer.sticky.cookie.secure=true" logging: options: max-size: "10m" @@ -52,12 +52,12 @@ services: environment: FORCE_COLOR: "true" NEW_RELIC_LICENSE_KEY: "${NEW_RELIC_LICENSE_KEY}" - NEW_RELIC_APP_NAME: "Laundrobot.AI API" + NEW_RELIC_APP_NAME: "Definya API" networks: - rpg-network - laundry-client: - image: laundrobot/laundrobot-team:client-latest + rpg-client: + image: definya/definya-team:client-latest healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8080"] interval: 120s @@ -73,10 +73,10 @@ services: condition: on-failure labels: - "traefik.enable=true" - - "traefik.http.routers.laundry-client.rule=Host(`laundrobot.ai`)" - - "traefik.http.routers.laundry-client.entrypoints=websecure" - - "traefik.http.routers.laundry-client.tls.certresolver=myresolver" - - "traefik.http.services.laundry-client.loadbalancer.server.port=8080" + - "traefik.http.routers.rpg-client.rule=Host(`play.definya.com`)" + - "traefik.http.routers.rpg-client.entrypoints=websecure" + - "traefik.http.routers.rpg-client.tls.certresolver=myresolver" + - "traefik.http.services.rpg-client.loadbalancer.server.port=8080" placement: constraints: - node.role == manager @@ -118,8 +118,8 @@ services: - "${REDIS_PORT}:${REDIS_PORT}" volumes: - rpg-redis-data:/data - - ./config/redis.conf:/redis.conf - command: ["redis-server", "/redis.conf", "--port", "${REDIS_PORT}"] + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--port", "${REDIS_PORT}"] networks: - rpg-network deploy: @@ -136,7 +136,7 @@ services: image: newrelic/infrastructure:latest environment: NRIA_LICENSE_KEY: "${NEW_RELIC_LICENSE_KEY}" - NRIA_DISPLAY_NAME: "Laundrobot.AI API Docker" + NRIA_DISPLAY_NAME: "Laundry API Docker" volumes: - "/:/host:ro" - "/var/run/docker.sock:/var/run/docker.sock"