From c249a916d20332c1221e04608442df24a53dfd0b Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 9 Apr 2026 17:54:33 +0000 Subject: [PATCH] fix(redis): support unix socket connections Normalize Redis connection options in a shared helper so Bull and the cache adapter both support Unix socket paths in REDIS_HOST as well as regular TCP host and port settings. Add helper tests and clarify the REDIS_HOST and REDIS_PORT documentation in the README. --- CHANGELOG.md | 1 + README.md | 4 +- apps/api/src/app/app.module.ts | 7 +- .../src/app/redis-cache/redis-cache.module.ts | 12 +-- .../src/helper/redis-options.helper.spec.ts | 74 +++++++++++++++++++ apps/api/src/helper/redis-options.helper.ts | 65 ++++++++++++++++ 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/helper/redis-options.helper.spec.ts create mode 100644 apps/api/src/helper/redis-options.helper.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebab8184..364d3a18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed Redis and Valkey connectivity when `REDIS_HOST` points to a Unix socket - Improved the style of the activity type component ## 2.253.0 - 2026-03-06 diff --git a/README.md b/README.md index 44212b607..4532cb2e9 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,9 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c | `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | | `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | | `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | -| `REDIS_HOST` | `string` | | The host where _Redis_ is running | +| `REDIS_HOST` | `string` | | The host or Unix socket path where _Redis_ is running | | `REDIS_PASSWORD` | `string` | | The password of _Redis_ | -| `REDIS_PORT` | `number` | | The port where _Redis_ is running | +| `REDIS_PORT` | `number` | | The port where _Redis_ is running (ignored when `REDIS_HOST` is a Unix socket path) | | `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | | `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 8ebe05928..407c3ca5c 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,4 +1,5 @@ import { EventsModule } from '@ghostfolio/api/events/events.module'; +import { getBullRedisOptions } from '@ghostfolio/api/helper/redis-options.helper'; import { BullBoardAuthMiddleware } from '@ghostfolio/api/middlewares/bull-board-auth.middleware'; import { HtmlTemplateMiddleware } from '@ghostfolio/api/middlewares/html-template.middleware'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -98,12 +99,12 @@ import { UserModule } from './user/user.module'; ] : []), BullModule.forRoot({ - redis: { + redis: getBullRedisOptions({ db: parseInt(process.env.REDIS_DB ?? '0', 10), - host: process.env.REDIS_HOST, + host: process.env.REDIS_HOST ?? 'localhost', password: process.env.REDIS_PASSWORD, port: parseInt(process.env.REDIS_PORT ?? '6379', 10) - } + }) }), CacheModule, ConfigModule.forRoot(), diff --git a/apps/api/src/app/redis-cache/redis-cache.module.ts b/apps/api/src/app/redis-cache/redis-cache.module.ts index d0e3228b7..532036ffe 100644 --- a/apps/api/src/app/redis-cache/redis-cache.module.ts +++ b/apps/api/src/app/redis-cache/redis-cache.module.ts @@ -1,3 +1,4 @@ +import { getKeyvRedisOptions } from '@ghostfolio/api/helper/redis-options.helper'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -14,14 +15,15 @@ import { RedisCacheService } from './redis-cache.service'; imports: [ConfigurationModule], inject: [ConfigurationService], useFactory: async (configurationService: ConfigurationService) => { - const redisPassword = encodeURIComponent( - configurationService.get('REDIS_PASSWORD') - ); - return { stores: [ createKeyv( - `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}` + getKeyvRedisOptions({ + db: configurationService.get('REDIS_DB'), + host: configurationService.get('REDIS_HOST'), + password: configurationService.get('REDIS_PASSWORD'), + port: configurationService.get('REDIS_PORT') + }) ) ], ttl: configurationService.get('CACHE_TTL') diff --git a/apps/api/src/helper/redis-options.helper.spec.ts b/apps/api/src/helper/redis-options.helper.spec.ts new file mode 100644 index 000000000..e961f8094 --- /dev/null +++ b/apps/api/src/helper/redis-options.helper.spec.ts @@ -0,0 +1,74 @@ +import { + getBullRedisOptions, + getKeyvRedisOptions +} from './redis-options.helper'; + +describe('getBullRedisOptions', () => { + it('should return tcp options when using a hostname', () => { + expect( + getBullRedisOptions({ + db: 2, + host: 'localhost', + password: 'secret', + port: 6380 + }) + ).toStrictEqual({ + db: 2, + host: 'localhost', + password: 'secret', + port: 6380 + }); + }); + + it('should return unix socket options when using a socket path', () => { + expect( + getBullRedisOptions({ + db: 0, + host: '/run/valkey/valkey.sock', + password: '', + port: 6379 + }) + ).toStrictEqual({ + db: 0, + password: undefined, + path: '/run/valkey/valkey.sock' + }); + }); +}); + +describe('getKeyvRedisOptions', () => { + it('should return tcp options when using a hostname', () => { + expect( + getKeyvRedisOptions({ + db: 1, + host: 'redis', + password: 'secret', + port: 6379 + }) + ).toStrictEqual({ + database: 1, + password: 'secret', + socket: { + host: 'redis', + port: 6379 + } + }); + }); + + it('should return unix socket options when using a socket path', () => { + expect( + getKeyvRedisOptions({ + db: 5, + host: '/var/run/redis/redis.sock', + password: '', + port: 6379 + }) + ).toStrictEqual({ + database: 5, + password: undefined, + socket: { + path: '/var/run/redis/redis.sock' + } + }); + }); +}); diff --git a/apps/api/src/helper/redis-options.helper.ts b/apps/api/src/helper/redis-options.helper.ts new file mode 100644 index 000000000..d6c4d34f2 --- /dev/null +++ b/apps/api/src/helper/redis-options.helper.ts @@ -0,0 +1,65 @@ +import type { RedisClientOptions } from '@keyv/redis'; +import type { RedisOptions } from 'ioredis'; + +interface RedisConnectionOptions { + db: number; + host: string; + password?: string; + port: number; +} + +export function getBullRedisOptions({ + db, + host, + password, + port +}: RedisConnectionOptions): RedisOptions { + const redisPassword = password || undefined; + + if (isUnixSocketPath(host)) { + return { + db, + password: redisPassword, + path: host + }; + } + + return { + db, + host, + password: redisPassword, + port + }; +} + +export function getKeyvRedisOptions({ + db, + host, + password, + port +}: RedisConnectionOptions): RedisClientOptions { + const redisPassword = password || undefined; + + if (isUnixSocketPath(host)) { + return { + database: db, + password: redisPassword, + socket: { + path: host + } + }; + } + + return { + database: db, + password: redisPassword, + socket: { + host, + port + } + }; +} + +function isUnixSocketPath(host: string) { + return host.startsWith('/'); +}