feat(infra): add external redis (#3639)

This commit is contained in:
Meier Lukas
2025-07-20 17:13:57 +02:00
committed by GitHub
parent 732bce72ae
commit 8e960324bc
32 changed files with 201 additions and 84 deletions

View File

@@ -64,6 +64,7 @@ ENV DB_URL='/appdata/db/db.sqlite'
ENV DB_DIALECT='sqlite'
ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV REDIS_IS_EXTERNAL='false'
ENV NODE_ENV='production'
ENTRYPOINT [ "/app/entrypoint.sh" ]

View File

@@ -25,11 +25,11 @@
"@homarr/boards": "workspace:^0.1.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/gridstack": "^1.12.0",

View File

@@ -1,5 +1,4 @@
import { createEnv } from "@homarr/env";
import { createBooleanSchema } from "@homarr/env/schemas";
import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {

View File

@@ -1,9 +1,10 @@
import { describe, expect, test } from "vitest";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { createRedisContainer } from "./shared/redis-container";
describe("Health checks", () => {
test("ready and live should return 200 OK", async () => {
test("ready and live should return 200 OK with normal image and no extra configuration", async () => {
// Arrange
const homarrContainer = await createHomarrContainer().start();
@@ -15,4 +16,31 @@ describe("Health checks", () => {
expect(readyResponse.status).toBe(200);
expect(liveResponse.status).toBe(200);
}, 20_000);
test("ready and live should return 200 OK with external redis", async () => {
// Arrange
const redisContainer = await createRedisContainer().start();
const homarrContainer = await createHomarrContainer({
environment: {
REDIS_IS_EXTERNAL: "true",
REDIS_HOST: "host.docker.internal",
REDIS_PORT: redisContainer.getMappedPort(6379).toString(),
REDIS_PASSWORD: redisContainer.getPassword(),
},
}).start();
// Act
const readyResponse = await fetch(`http://localhost:${homarrContainer.getMappedPort(7575)}/api/health/ready`);
const liveResponse = await fetch(`http://localhost:${homarrContainer.getMappedPort(7575)}/api/health/live`);
// Assert
expect(
readyResponse.status,
`Expected ready to return OK statusCode=${readyResponse.status} content=${await readyResponse.text()}`,
).toBe(200);
expect(
liveResponse.status,
`Expected live to return OK statusCode=${liveResponse.status} content=${await liveResponse.text()}`,
).toBe(200);
}, 20_000);
});

View File

@@ -0,0 +1,5 @@
import { RedisContainer } from "@testcontainers/redis";
export const createRedisContainer = () => {
return new RedisContainer("redis:latest").withPassword("homarr");
};

View File

@@ -38,6 +38,7 @@
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@testcontainers/redis": "^11.2.1",
"@turbo/gen": "^2.5.5",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/coverage-v8": "^3.2.4",

View File

@@ -1,8 +1,7 @@
import { z } from "zod";
import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env";
import { supportedAuthProviders } from "@homarr/definitions";
import { createEnv } from "@homarr/env";
import { createBooleanSchema, createDurationSchema } from "@homarr/env/schemas";
const authProvidersSchema = z
.string()

View File

@@ -27,9 +27,9 @@
"@auth/drizzle-adapter": "^1.10.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0",

View File

@@ -1,7 +1,7 @@
import { randomBytes } from "crypto";
import { z } from "zod";
import { createEnv } from "@homarr/env";
import { createEnv } from "@homarr/core/infrastructure/env";
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;

View File

@@ -27,7 +27,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/env": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"dayjs": "^1.11.13",

View File

@@ -1,9 +1,4 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
export default [...baseConfig];

View File

@@ -1,12 +1,13 @@
{
"name": "@homarr/env",
"name": "@homarr/core",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./schemas": "./src/schemas.ts"
"./infrastructure/redis": "./src/infrastructure/redis/client.ts",
"./infrastructure/env": "./src/infrastructure/env/index.ts",
".": "./src/index.ts"
},
"typesVersions": {
"*": {
@@ -24,6 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.6.1",
"zod": "^3.25.76"
},
"devDependencies": {

View File

@@ -7,3 +7,6 @@ export const defaultEnvOptions = {
} satisfies Partial<Parameters<typeof createEnvT3>[0]>;
export const createEnv: typeof createEnvT3 = (options) => createEnvT3({ ...defaultEnvOptions, ...options });
export * from "./prefix";
export * from "./schemas";

View File

@@ -0,0 +1,13 @@
export const runtimeEnvWithPrefix = (prefix: `${string}_`) =>
Object.entries(process.env)
.filter(([key]) => key.startsWith(prefix))
.reduce(
(acc, [key, value]) => {
if (value === undefined) return acc;
const newKey = key.replace(prefix, "");
acc[newKey] = value;
return acc;
},
{} as Record<string, string>,
);

View File

@@ -0,0 +1,26 @@
import type { RedisOptions } from "ioredis";
import { Redis } from "ioredis";
import { redisEnv } from "./env";
const defaultRedisOptions = {
connectionName: "homarr",
} satisfies RedisOptions;
export type { Redis as RedisClient } from "ioredis";
export const createRedisClient = () =>
redisEnv.IS_EXTERNAL
? new Redis({
...defaultRedisOptions,
host: redisEnv.HOST,
port: redisEnv.PORT,
tls: redisEnv.TLS_CA
? {
ca: redisEnv.TLS_CA,
}
: undefined,
username: redisEnv.USERNAME,
password: redisEnv.PASSWORD,
})
: new Redis(defaultRedisOptions);

View File

@@ -0,0 +1,17 @@
import { z } from "zod/v4";
import { createEnv } from "../env";
import { runtimeEnvWithPrefix } from "../env/prefix";
import { createBooleanSchema } from "../env/schemas";
export const redisEnv = createEnv({
server: {
IS_EXTERNAL: createBooleanSchema(false),
HOST: z.string().optional(),
PORT: z.coerce.number().default(6379).optional(),
TLS_CA: z.string().optional(),
USERNAME: z.string().optional(),
PASSWORD: z.string().optional(),
},
runtimeEnv: runtimeEnvWithPrefix("REDIS_"),
});

View File

@@ -26,8 +26,8 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.83.0",
"@trpc/client": "^11.4.3",

View File

@@ -1,7 +1,7 @@
import { z } from "zod/v4";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/env";
import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {

View File

@@ -1,7 +1,7 @@
import { z } from "zod";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/env";
import { createEnv } from "@homarr/core/infrastructure/env";
const drivers = {
betterSqlite3: "better-sqlite3",

View File

@@ -40,8 +40,8 @@
"dependencies": {
"@auth/core": "^0.40.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.1.3",

View File

@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"dockerode": "^4.0.7"
},
"devDependencies": {

View File

@@ -1,7 +1,6 @@
import { z } from "zod";
import { createEnv } from "@homarr/env";
import { createBooleanSchema } from "@homarr/env/schemas";
import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {

View File

@@ -1 +0,0 @@
export * from "./src";

View File

@@ -24,8 +24,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/env": "workspace:^0.1.0",
"ioredis": "5.6.1",
"@homarr/core": "workspace:^0.1.0",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^3.25.76"

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { createEnv } from "@homarr/env";
import { createEnv } from "@homarr/core/infrastructure/env";
import { logLevels } from "./constants";

View File

@@ -1,7 +1,9 @@
import { Redis } from "ioredis";
import superjson from "superjson";
import Transport from "winston-transport";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
const messageSymbol = Symbol.for("message");
const levelSymbol = Symbol.for("level");
@@ -10,7 +12,7 @@ const levelSymbol = Symbol.for("level");
// of the base functionality and `.exceptions.handle()`.
//
export class RedisTransport extends Transport {
private redis: Redis | null = null;
private redis: RedisClient | null = null;
/**
* Log the info to the Redis channel
@@ -21,7 +23,7 @@ export class RedisTransport extends Transport {
});
// Is only initialized here because it did not work when initialized in the constructor or outside the class
this.redis ??= new Redis();
this.redis ??= createRedisClient();
this.redis
.publish(

View File

@@ -23,6 +23,8 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^",
"@homarr/core": "workspace:^",
"@homarr/db": "workspace:^",
"@homarr/definitions": "workspace:^",
"@homarr/log": "workspace:^",
"ioredis": "5.6.1",

View File

@@ -1,4 +1,5 @@
import { Redis } from "ioredis";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
/**
* Creates a new Redis connection
@@ -7,8 +8,8 @@ import { Redis } from "ioredis";
export const createRedisConnection = () => {
if (Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS)) {
// Return null if we are in CI as we don't want to connect to Redis
return null as unknown as Redis;
return null as unknown as RedisClient;
}
return new Redis();
return createRedisClient();
};

113
pnpm-lock.yaml generated
View File

@@ -37,6 +37,9 @@ importers:
'@semantic-release/release-notes-generator':
specifier: ^14.0.3
version: 14.0.3(semantic-release@24.2.7(typescript@5.8.3))
'@testcontainers/redis':
specifier: ^11.2.1
version: 11.2.1
'@turbo/gen':
specifier: ^2.5.5
version: 2.5.5(@types/node@22.16.4)(typescript@5.8.3)
@@ -112,6 +115,9 @@ importers:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../../packages/common
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../../packages/core
'@homarr/cron-job-status':
specifier: workspace:^0.1.0
version: link:../../packages/cron-job-status
@@ -124,9 +130,6 @@ importers:
'@homarr/docker':
specifier: workspace:^0.1.0
version: link:../../packages/docker
'@homarr/env':
specifier: workspace:^0.1.0
version: link:../../packages/env
'@homarr/form':
specifier: workspace:^0.1.0
version: link:../../packages/form
@@ -660,15 +663,15 @@ importers:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../core
'@homarr/db':
specifier: workspace:^0.1.0
version: link:../db
'@homarr/definitions':
specifier: workspace:^0.1.0
version: link:../definitions
'@homarr/env':
specifier: workspace:^0.1.0
version: link:../env
'@homarr/log':
specifier: workspace:^0.1.0
version: link:../log
@@ -823,9 +826,9 @@ importers:
packages/common:
dependencies:
'@homarr/env':
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../env
version: link:../core
'@homarr/log':
specifier: workspace:^0.1.0
version: link:../log
@@ -870,17 +873,45 @@ importers:
specifier: ^5.8.3
version: 5.8.3
packages/core:
dependencies:
'@t3-oss/env-nextjs':
specifier: ^0.13.8
version: 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.76)
ioredis:
specifier: 5.6.1
version: 5.6.1
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
version: link:../../tooling/eslint
'@homarr/prettier-config':
specifier: workspace:^0.1.0
version: link:../../tooling/prettier
'@homarr/tsconfig':
specifier: workspace:^0.1.0
version: link:../../tooling/typescript
eslint:
specifier: ^9.31.0
version: 9.31.0
typescript:
specifier: ^5.8.3
version: 5.8.3
packages/cron-job-api:
dependencies:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../core
'@homarr/cron-jobs':
specifier: workspace:^0.1.0
version: link:../cron-jobs
'@homarr/env':
specifier: workspace:^0.1.0
version: link:../env
'@homarr/log':
specifier: workspace:^0.1.0
version: link:../log
@@ -1056,12 +1087,12 @@ importers:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../core
'@homarr/definitions':
specifier: workspace:^0.1.0
version: link:../definitions
'@homarr/env':
specifier: workspace:^0.1.0
version: link:../env
'@homarr/log':
specifier: workspace:^0.1.0
version: link:../log
@@ -1166,9 +1197,9 @@ importers:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
'@homarr/env':
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../env
version: link:../core
dockerode:
specifier: ^4.0.7
version: 4.0.7
@@ -1192,31 +1223,6 @@ importers:
specifier: ^5.8.3
version: 5.8.3
packages/env:
dependencies:
'@t3-oss/env-nextjs':
specifier: ^0.13.8
version: 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.76)
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
version: link:../../tooling/eslint
'@homarr/prettier-config':
specifier: workspace:^0.1.0
version: link:../../tooling/prettier
'@homarr/tsconfig':
specifier: workspace:^0.1.0
version: link:../../tooling/typescript
eslint:
specifier: ^9.31.0
version: 9.31.0
typescript:
specifier: ^5.8.3
version: 5.8.3
packages/form:
dependencies:
'@homarr/common':
@@ -1461,12 +1467,9 @@ importers:
packages/log:
dependencies:
'@homarr/env':
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../env
ioredis:
specifier: 5.6.1
version: 5.6.1
version: link:../core
superjson:
specifier: 2.2.2
version: 2.2.2
@@ -1740,6 +1743,12 @@ importers:
'@homarr/common':
specifier: workspace:^
version: link:../common
'@homarr/core':
specifier: workspace:^
version: link:../core
'@homarr/db':
specifier: workspace:^
version: link:../db
'@homarr/definitions':
specifier: workspace:^
version: link:../definitions
@@ -4423,6 +4432,9 @@ packages:
'@testcontainers/mysql@11.3.0':
resolution: {integrity: sha512-5zOzAcsQUVbN9tCC6cbD0zkxOu/P8NdFsYc+w1YFctGUOf5DL0QBBwDv5LkFj92+eic4veggPlzVd2SX8LT3SQ==}
'@testcontainers/redis@11.2.1':
resolution: {integrity: sha512-Q5j+irNw0BLec3he30s2E0fhE06Zr9ROVutkyKUgcwQoZxEVW3xV69ke2AFCT5teEcIvTKqevObN4UDkq33Qow==}
'@tiptap/core@2.26.1':
resolution: {integrity: sha512-fymyd/XZvYiHjBoLt1gxs024xP/LY26d43R1vluYq7AHBL/7DE3ywzy+1GEsGyAv5Je2L0KBhNIR/izbq3Kaqg==}
peerDependencies:
@@ -13172,6 +13184,13 @@ snapshots:
- bare-buffer
- supports-color
'@testcontainers/redis@11.2.1':
dependencies:
testcontainers: 11.3.0
transitivePeerDependencies:
- bare-buffer
- supports-color
'@tiptap/core@2.26.1(@tiptap/pm@2.26.1)':
dependencies:
'@tiptap/pm': 2.26.1
@@ -16639,7 +16658,7 @@ snapshots:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.4.0
debug: 4.4.1
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0

View File

@@ -25,8 +25,15 @@ envsubst '${HOSTNAME}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
nginx -g 'daemon off;' &
NGINX_PID=$!
redis-server /app/redis.conf &
REDIS_PID=$!
if [ $REDIS_IS_EXTERNAL = "true" ]; then
echo "Using external Redis server at redis://$REDIS_HOST:$REDIS_PORT"
else
echo "Starting internal Redis server"
redis-server /app/redis.conf &
REDIS_PID=$!
fi
node apps/tasks/tasks.cjs &
TASKS_PID=$!