Merge pull request #3640 from akhilmhdh/feat/redis-sentinel-support

Feat/redis sentinel support
This commit is contained in:
Maidul Islam
2025-05-26 18:47:15 -04:00
committed by GitHub
12 changed files with 324 additions and 44 deletions
+4 -4
View File
@@ -15,8 +15,8 @@ import { mockSmtpServer } from "./mocks/smtp";
import { initDbConnection } from "@app/db"; import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue"; import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore"; import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { buildRedisFromConfig } from "@app/lib/config/redis";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default { export default {
@@ -30,7 +30,7 @@ export default {
dbRootCert: envConfig.DB_ROOT_CERT dbRootCert: envConfig.DB_ROOT_CERT
}); });
const redis = new Redis(envConfig.REDIS_URL); const redis = buildRedisFromConfig(envConfig);
await redis.flushdb("SYNC"); await redis.flushdb("SYNC");
try { try {
@@ -55,8 +55,8 @@ export default {
}); });
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); const queue = queueServiceFactory(envConfig, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
const keyStore = keyStoreFactory(envConfig.REDIS_URL); const keyStore = keyStoreFactory(envConfig);
const hsmModule = initializeHsmModule(envConfig); const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize(); hsmModule.initialize();
+3 -4
View File
@@ -1,5 +1,4 @@
import { Redis } from "ioredis"; import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { applyJitter } from "@app/lib/dates"; import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay"; import { delay as delayMs } from "@app/lib/delay";
@@ -68,8 +67,8 @@ type TWaitTillReady = {
jitter?: number; jitter?: number;
}; };
export const keyStoreFactory = (redisUrl: string) => { export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys) => {
const redis = new Redis(redisUrl); const redis = buildRedisFromConfig(redisConfigKeys);
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 }); const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
const setItem = async (key: string, value: string | number | Buffer, prefix?: string) => const setItem = async (key: string, value: string | number | Buffer, prefix?: string) =>
+25 -5
View File
@@ -30,7 +30,19 @@ const envSchema = z
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
.transform((el) => el === "true"), .transform((el) => el === "true"),
REDIS_URL: zpStr(z.string()), REDIS_URL: zpStr(z.string().optional()),
REDIS_SENTINEL_HOSTS: zpStr(
z
.string()
.optional()
.describe("Comma-separated list of Sentinel host:port pairs. Eg: 192.168.65.254:26379,192.168.65.254:26380")
),
REDIS_SENTINEL_MASTER_NAME: zpStr(
z.string().optional().default("mymaster").describe("The name of the Redis master set monitored by Sentinel")
),
REDIS_SENTINEL_ENABLE_TLS: zodStrBool.optional().describe("Whether to use TLS/SSL for Redis Sentinel connection"),
REDIS_SENTINEL_USERNAME: zpStr(z.string().optional().describe("Authentication username for Redis Sentinel")),
REDIS_SENTINEL_PASSWORD: zpStr(z.string().optional().describe("Authentication password for Redis Sentinel")),
HOST: zpStr(z.string().default("localhost")), HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
@@ -259,26 +271,34 @@ const envSchema = z
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
) )
.refine(
(data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS),
"Either REDIS_URL or REDIS_SENTINEL_HOSTS must be defined."
)
.transform((data) => ({ .transform((data) => ({
...data, ...data,
DB_READ_REPLICAS: data.DB_READ_REPLICAS DB_READ_REPLICAS: data.DB_READ_REPLICAS
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
: undefined, : undefined,
isCloud: Boolean(data.LICENSE_SERVER_KEY), isCloud: Boolean(data.LICENSE_SERVER_KEY),
isSmtpConfigured: Boolean(data.SMTP_HOST), isSmtpConfigured: Boolean(data.SMTP_HOST),
isRedisConfigured: Boolean(data.REDIS_URL), isRedisConfigured: Boolean(data.REDIS_URL || data.REDIS_SENTINEL_HOSTS),
isDevelopmentMode: data.NODE_ENV === "development", isDevelopmentMode: data.NODE_ENV === "development",
isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE, isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",")
.map((el) => {
const [host, port] = el.trim().split(":");
return { host: host.trim(), port: Number(port.trim()) };
}),
isSecretScanningConfigured: isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
isHsmConfigured: isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
})); }));
+24
View File
@@ -0,0 +1,24 @@
import { Redis } from "ioredis";
export type TRedisConfigKeys = Partial<{
REDIS_URL: string;
REDIS_SENTINEL_HOSTS: { host: string; port: number }[];
REDIS_SENTINEL_MASTER_NAME: string;
REDIS_SENTINEL_ENABLE_TLS: boolean;
REDIS_SENTINEL_USERNAME: string;
REDIS_SENTINEL_PASSWORD: string;
}>;
export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => {
if (cfg.REDIS_URL) return new Redis(cfg.REDIS_URL, { maxRetriesPerRequest: null });
return new Redis({
// refine at tope will catch this case
sentinels: cfg.REDIS_SENTINEL_HOSTS!,
name: cfg.REDIS_SENTINEL_MASTER_NAME!,
maxRetriesPerRequest: null,
sentinelUsername: cfg.REDIS_SENTINEL_USERNAME,
sentinelPassword: cfg.REDIS_SENTINEL_PASSWORD,
enableTLSForSentinelMode: cfg.REDIS_SENTINEL_ENABLE_TLS
});
};
+4 -4
View File
@@ -1,7 +1,6 @@
import "./lib/telemetry/instrumentation"; import "./lib/telemetry/instrumentation";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
@@ -9,6 +8,7 @@ import { runMigrations } from "./auto-start-migrations";
import { initAuditLogDbConnection, initDbConnection } from "./db"; import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore"; import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
import { buildRedisFromConfig } from "./lib/config/redis";
import { removeTemporaryBaseDirectory } from "./lib/files"; import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger"; import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue"; import { queueServiceFactory } from "./queue";
@@ -44,15 +44,15 @@ const run = async () => {
const smtp = smtpServiceFactory(formatSmtpConfig()); const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(envConfig.REDIS_URL, { const queue = queueServiceFactory(envConfig, {
dbConnectionUrl: envConfig.DB_CONNECTION_URI, dbConnectionUrl: envConfig.DB_CONNECTION_URI,
dbRootCert: envConfig.DB_ROOT_CERT dbRootCert: envConfig.DB_ROOT_CERT
}); });
await queue.initialize(); await queue.initialize();
const keyStore = keyStoreFactory(envConfig.REDIS_URL); const keyStore = keyStoreFactory(envConfig);
const redis = new Redis(envConfig.REDIS_URL); const redis = buildRedisFromConfig(envConfig);
const hsmModule = initializeHsmModule(envConfig); const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize(); hsmModule.initialize();
+3 -3
View File
@@ -1,5 +1,4 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq"; import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
import PgBoss, { WorkOptions } from "pg-boss"; import PgBoss, { WorkOptions } from "pg-boss";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
@@ -13,6 +12,7 @@ import {
TScanPushEventPayload TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { import {
@@ -265,10 +265,10 @@ export type TQueueJobTypes = {
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
export const queueServiceFactory = ( export const queueServiceFactory = (
redisUrl: string, redisCfg: TRedisConfigKeys,
{ dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string } { dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string }
) => { ) => {
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null }); const connection = buildRedisFromConfig(redisCfg);
const queueContainer = {} as Record< const queueContainer = {} as Record<
QueueName, QueueName,
Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]> Queue<TQueueJobTypes[QueueName]["payload"], void, TQueueJobTypes[QueueName]["name"]>
+6 -3
View File
@@ -1,9 +1,9 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Redis } from "ioredis";
import { Knex } from "knex"; import { Knex } from "knex";
import { createTransport } from "nodemailer"; import { createTransport } from "nodemailer";
import { formatSmtpConfig, getConfig } from "@app/lib/config/env"; import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig } from "@app/lib/config/redis";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -65,12 +65,15 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
}); });
console.log("Testing redis connection"); console.log("Testing redis connection");
const redis = new Redis(appCfg.REDIS_URL); const redis = buildRedisFromConfig(appCfg);
const redisPing = await redis?.ping(); const redisPing = await redis?.ping();
if (!redisPing) { if (!redisPing) {
console.error("Redis - Failed to connect"); console.error("Redis - Failed to connect");
} else { } else {
console.error("Redis successfully connected"); console.log("Redis successfully connected");
if (appCfg.isRedisSentinelMode) {
console.log("Redis Sentinel Mode");
}
redis.disconnect(); redis.disconnect();
} }
+2 -4
View File
@@ -1,14 +1,12 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { buildRedisFromConfig } from "@app/lib/config/redis";
import { RateLimitError } from "@app/lib/errors"; import { RateLimitError } from "@app/lib/errors";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
const appCfg = getConfig(); const appCfg = getConfig();
const redis = appCfg.isRedisConfigured const redis = appCfg.isRedisConfigured ? buildRedisFromConfig(appCfg) : null;
? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 })
: null;
return { return {
errorResponseBuilder: (_, context) => { errorResponseBuilder: (_, context) => {
+54 -5
View File
@@ -1,5 +1,5 @@
--- ---
title: "Configurations" title: "Environment Variables"
description: "Read how to configure environment variables for self-hosted Infisical." description: "Read how to configure environment variables for self-hosted Infisical."
--- ---
@@ -37,9 +37,11 @@ Used to configure platform-specific security and operational settings
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces. To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
Example values: Example values:
- `localhost` (default, same as `127.0.0.1`) - `localhost` (default, same as `127.0.0.1`)
- `0.0.0.0` (all interfaces, accessible externally) - `0.0.0.0` (all interfaces, accessible externally)
- `192.168.1.100` (specific interface IP) - `192.168.1.100` (specific interface IP)
</ParamField> </ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional> <ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
@@ -86,8 +88,9 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
### PostgreSQL ### PostgreSQL
<Info> <Info>
Please note that the database user you create must be granted all privileges on the Infisical database. Please note that the database user you create must be granted all privileges
This includes the ability to create new schemas, create, update, delete, modify tables and indexes, etc. on the Infisical database. This includes the ability to create new schemas,
create, update, delete, modify tables and indexes, etc.
</Info> </Info>
<ParamField query="DB_CONNECTION_URI" type="string" default="" required> <ParamField query="DB_CONNECTION_URI" type="string" default="" required>
@@ -119,10 +122,42 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
</ParamField> </ParamField>
### Redis ### Redis
Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup.
<Tabs>
<Tab title="Redis standalone">
<ParamField query="REDIS_URL" type="string" default="none" required> <ParamField query="REDIS_URL" type="string" default="none" required>
Redis connection string. Redis connection string.
</ParamField> </ParamField>
</Tab>
<Tab title="Redis Sentinel">
<ParamField
query="REDIS_SENTINEL_HOSTS"
type="string"
default="none"
required
>
Comma-separated list of Sentinel host:port pairs. ```
192.168.65.254:26379,192.168.65.254:26380 ```
</ParamField>
<ParamField
query="REDIS_SENTINEL_MASTER_NAME"
type="string"
default="mymaster"
>
The name of the Redis master set monitored by Sentinel
</ParamField>
<ParamField query="REDIS_SENTINEL_ENABLE_TLS" type="bool" default="false">
Whether to use TLS/SSL for Redis Sentinel connection
</ParamField>
<ParamField query="REDIS_SENTINEL_USERNAME" type="string" default="none">
Authentication username for Redis Sentinel
</ParamField>
<ParamField query="REDIS_SENTINEL_PASSWORD" type="string" default="none">
Authentication password for Redis Sentinel
</ParamField>
</Tab>
</Tabs>
## Email Service ## Email Service
@@ -640,11 +675,25 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I
The App ID of your GitHub App. The App ID of your GitHub App.
</ParamField> </ParamField>
<ParamField query="SECRET_SCANNING_GIT_APP_SLUG" type="string" default="none" optional> {" "}
<ParamField
query="SECRET_SCANNING_GIT_APP_SLUG"
type="string"
default="none"
optional
>
The slug of your GitHub App. The slug of your GitHub App.
</ParamField> </ParamField>
<ParamField query="SECRET_SCANNING_PRIVATE_KEY" type="string" default="none" optional> {" "}
<ParamField
query="SECRET_SCANNING_PRIVATE_KEY"
type="string"
default="none"
optional
>
A private key for your GitHub App. A private key for your GitHub App.
</ParamField> </ParamField>
+1
View File
@@ -0,0 +1 @@
HOST_IP=
+20
View File
@@ -0,0 +1,20 @@
## Sink for Redis Sentinel
1. Create a `.env` from `.env.example`
2. The HOST_IP value must be your host ip, so that redis inside the containers can connect to it and switch over as needed.
To test Sentinel is working correctly
1. Run
```
docker exec -it sentinel-2 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
```
2. Run
```
docker-compose -f docker-compose.sentinel.yml stop redis-master
```
3. Again running step 1 should show the other replica port.
@@ -0,0 +1,166 @@
version: "3.8"
services:
redis-master:
image: redis:latest
container_name: redis-master
hostname: redis-master
ports:
- "6380:6379"
volumes:
- ./data/master:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6380",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.3
redis-slave-1:
image: redis:latest
container_name: redis-slave-1
hostname: redis-slave-1
depends_on:
- redis-master
ports:
- "6381:6379"
volumes:
- ./data/slave1:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--replicaof",
"redis-master",
"6379",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6381",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.4
redis-slave-2:
image: redis:latest
container_name: redis-slave-2
hostname: redis-slave-2
depends_on:
- redis-master
ports:
- "6382:6379"
volumes:
- ./data/slave2:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--replicaof",
"redis-master",
"6379",
"--repl-diskless-load",
"on-empty-db",
"--replica-announce-ip",
"${HOST_IP}",
"--replica-announce-port",
"6382",
"--protected-mode",
"no",
]
networks:
redis-net:
ipv4_address: 172.21.0.5
sentinel-1:
image: redis:latest
container_name: sentinel-1
hostname: sentinel-1
depends_on:
- redis-master
ports:
- "26379:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.6
sentinel-2:
image: redis:latest
container_name: sentinel-2
hostname: sentinel-2
depends_on:
- redis-master
ports:
- "26380:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.7
sentinel-3:
image: redis:latest
container_name: sentinel-3
hostname: sentinel-3
depends_on:
- redis-master
ports:
- "26381:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster ${HOST_IP} 6380 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
networks:
redis-net:
ipv4_address: 172.21.0.8
redisinsight:
image: redis/redisinsight:latest
container_name: redisinsight
ports:
- "5540:5540"
networks:
redis-net:
ipv4_address: 172.21.0.9
networks:
redis-net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16