diff --git a/server/package-lock.json b/server/package-lock.json
index 54862802d1..3752dd95b5 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -11,7 +11,6 @@
       "dependencies": {
         "@nestjs/bullmq": "^10.0.1",
         "@nestjs/common": "^10.2.2",
-        "@nestjs/config": "^3.0.0",
         "@nestjs/core": "^10.2.2",
         "@nestjs/event-emitter": "^2.0.4",
         "@nestjs/platform-express": "^10.2.2",
@@ -63,7 +62,8 @@
         "tailwindcss-preset-email": "^1.3.2",
         "thumbhash": "^0.1.1",
         "typeorm": "^0.3.17",
-        "ua-parser-js": "^1.0.35"
+        "ua-parser-js": "^1.0.35",
+        "validator": "^13.12.0"
       },
       "devDependencies": {
         "@eslint/eslintrc": "^3.1.0",
@@ -2108,20 +2108,6 @@
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
       "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
     },
-    "node_modules/@nestjs/config": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz",
-      "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==",
-      "dependencies": {
-        "dotenv": "16.4.5",
-        "dotenv-expand": "10.0.0",
-        "lodash": "4.17.21"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
-        "rxjs": "^7.1.0"
-      }
-    },
     "node_modules/@nestjs/core": {
       "version": "10.4.5",
       "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.5.tgz",
@@ -8050,14 +8036,6 @@
         "url": "https://dotenvx.com"
       }
     },
-    "node_modules/dotenv-expand": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
-      "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -14901,9 +14879,10 @@
       }
     },
     "node_modules/validator": {
-      "version": "13.11.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
-      "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.10"
       }
@@ -16665,16 +16644,6 @@
         }
       }
     },
-    "@nestjs/config": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz",
-      "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==",
-      "requires": {
-        "dotenv": "16.4.5",
-        "dotenv-expand": "10.0.0",
-        "lodash": "4.17.21"
-      }
-    },
     "@nestjs/core": {
       "version": "10.4.5",
       "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.5.tgz",
@@ -20806,11 +20775,6 @@
       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
       "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
     },
-    "dotenv-expand": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
-      "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A=="
-    },
     "eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -25625,9 +25589,9 @@
       }
     },
     "validator": {
-      "version": "13.11.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
-      "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ=="
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg=="
     },
     "vary": {
       "version": "1.1.2",
diff --git a/server/package.json b/server/package.json
index 4716783371..1d53dffcc6 100644
--- a/server/package.json
+++ b/server/package.json
@@ -36,7 +36,6 @@
   "dependencies": {
     "@nestjs/bullmq": "^10.0.1",
     "@nestjs/common": "^10.2.2",
-    "@nestjs/config": "^3.0.0",
     "@nestjs/core": "^10.2.2",
     "@nestjs/event-emitter": "^2.0.4",
     "@nestjs/platform-express": "^10.2.2",
@@ -88,7 +87,8 @@
     "tailwindcss-preset-email": "^1.3.2",
     "thumbhash": "^0.1.1",
     "typeorm": "^0.3.17",
-    "ua-parser-js": "^1.0.35"
+    "ua-parser-js": "^1.0.35",
+    "validator": "^13.12.0"
   },
   "devDependencies": {
     "@eslint/eslintrc": "^3.1.0",
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index ef02416e58..8ed9d5f6ed 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -1,13 +1,11 @@
 import { BullModule } from '@nestjs/bullmq';
 import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common';
-import { ConfigModule } from '@nestjs/config';
 import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core';
 import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { ClsModule } from 'nestjs-cls';
 import { OpenTelemetryModule } from 'nestjs-otel';
 import { commands } from 'src/commands';
-import { immichAppConfig } from 'src/config';
 import { controllers } from 'src/controllers';
 import { entities } from 'src/entities';
 import { ImmichWorker } from 'src/enum';
@@ -43,7 +41,6 @@ const imports = [
   BullModule.forRoot(bull.config),
   BullModule.registerQueue(...bull.queues),
   ClsModule.forRoot(cls.config),
-  ConfigModule.forRoot(immichAppConfig),
   OpenTelemetryModule.forRoot(otel),
   TypeOrmModule.forRootAsync({
     inject: [ModuleRef],
diff --git a/server/src/config.ts b/server/src/config.ts
index 7bc93d7608..12e6e6576b 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -1,12 +1,9 @@
-import { ConfigModuleOptions } from '@nestjs/config';
 import { CronExpression } from '@nestjs/schedule';
-import Joi, { Root } from 'joi';
 import {
   AudioCodec,
   Colorspace,
   CQMode,
   ImageFormat,
-  ImmichEnvironment,
   LogLevel,
   ToneMapping,
   TranscodeHWAccel,
@@ -306,48 +303,3 @@ export const defaults = Object.freeze<SystemConfig>({
     deleteDelay: 7,
   },
 });
-
-const WHEN_DB_URL_SET = Joi.when('DB_URL', {
-  is: Joi.exist(),
-  then: Joi.string().optional(),
-  otherwise: Joi.string().required(),
-});
-
-export const immichAppConfig: ConfigModuleOptions = {
-  envFilePath: '.env',
-  isGlobal: true,
-  validationSchema: Joi.object({
-    IMMICH_ENV: Joi.string()
-      .optional()
-      .valid(...Object.values(ImmichEnvironment))
-      .default(ImmichEnvironment.PRODUCTION),
-    IMMICH_LOG_LEVEL: Joi.string()
-      .optional()
-      .valid(...Object.values(LogLevel)),
-
-    DB_USERNAME: WHEN_DB_URL_SET,
-    DB_PASSWORD: WHEN_DB_URL_SET,
-    DB_DATABASE_NAME: WHEN_DB_URL_SET,
-    DB_URL: Joi.string().optional(),
-    DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'),
-    DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
-
-    IMMICH_PORT: Joi.number().optional(),
-    IMMICH_API_METRICS_PORT: Joi.number().optional(),
-    IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(),
-
-    IMMICH_TRUSTED_PROXIES: Joi.extend((joi: Root) => ({
-      type: 'stringArray',
-      base: joi.array(),
-      coerce: (value) => (value.split ? value.split(',') : value),
-    }))
-      .stringArray()
-      .single()
-      .items(
-        Joi.string().ip({
-          version: ['ipv4', 'ipv6'],
-          cidr: 'optional',
-        }),
-      ),
-  }),
-};
diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts
new file mode 100644
index 0000000000..6c238252a6
--- /dev/null
+++ b/server/src/dtos/env.dto.ts
@@ -0,0 +1,190 @@
+import { Transform, Type } from 'class-transformer';
+import { IsEnum, IsInt, IsString } from 'class-validator';
+import { ImmichEnvironment, LogLevel } from 'src/enum';
+import { IsIPRange, Optional, ValidateBoolean } from 'src/validation';
+
+export class EnvDto {
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_API_METRICS_PORT?: number;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_DATA?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_IMAGE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_BUILD_IMAGE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_CONFIG_FILE?: string;
+
+  @IsEnum(ImmichEnvironment)
+  @Optional()
+  IMMICH_ENV?: ImmichEnvironment;
+
+  @IsString()
+  @Optional()
+  IMMICH_HOST?: string;
+
+  @ValidateBoolean({ optional: true })
+  IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean;
+
+  @IsEnum(LogLevel)
+  @Optional()
+  IMMICH_LOG_LEVEL?: LogLevel;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_MICROSERVICES_METRICS_PORT?: number;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  IMMICH_PORT?: number;
+
+  @IsString()
+  @Optional()
+  IMMICH_REPOSITORY?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_REPOSITORY_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_REF?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_COMMIT?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_SOURCE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_TELEMETRY_INCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_TELEMETRY_EXCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_SOURCE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_THIRD_PARTY_SUPPORT_URL?: string;
+
+  @IsIPRange({ requireCIDR: false }, { each: true })
+  @Transform(({ value }) =>
+    value && typeof value === 'string'
+      ? value
+          .split(',')
+          .map((value) => value.trim())
+          .filter(Boolean)
+      : value,
+  )
+  @Optional()
+  IMMICH_TRUSTED_PROXIES?: string[];
+
+  @IsString()
+  @Optional()
+  IMMICH_WORKERS_INCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  IMMICH_WORKERS_EXCLUDE?: string;
+
+  @IsString()
+  @Optional()
+  DB_DATABASE_NAME?: string;
+
+  @IsString()
+  @Optional()
+  DB_HOSTNAME?: string;
+
+  @IsString()
+  @Optional()
+  DB_PASSWORD?: string;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  DB_PORT?: number;
+
+  @ValidateBoolean({ optional: true })
+  DB_SKIP_MIGRATIONS?: boolean;
+
+  @IsString()
+  @Optional()
+  DB_URL?: string;
+
+  @IsString()
+  @Optional()
+  DB_USERNAME?: string;
+
+  @IsEnum(['pgvector', 'pgvecto.rs'])
+  @Optional()
+  DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs';
+
+  @IsString()
+  @Optional()
+  NO_COLOR?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_HOSTNAME?: string;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  REDIS_PORT?: number;
+
+  @IsInt()
+  @Optional()
+  @Type(() => Number)
+  REDIS_DBINDEX?: number;
+
+  @IsString()
+  @Optional()
+  REDIS_USERNAME?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_PASSWORD?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_SOCKET?: string;
+
+  @IsString()
+  @Optional()
+  REDIS_URL?: string;
+}
diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts
index d2777a2e46..2ff5f53073 100644
--- a/server/src/repositories/config.repository.spec.ts
+++ b/server/src/repositories/config.repository.spec.ts
@@ -8,6 +8,7 @@ const getEnv = () => {
 
 const resetEnv = () => {
   for (const env of [
+    'IMMICH_ENV',
     'IMMICH_WORKERS_INCLUDE',
     'IMMICH_WORKERS_EXCLUDE',
     'IMMICH_TRUSTED_PROXIES',
@@ -62,6 +63,18 @@ describe('getEnv', () => {
     resetEnv();
   });
 
+  it('should use defaults', () => {
+    const config = getEnv();
+
+    expect(config).toMatchObject({
+      host: undefined,
+      port: 2283,
+      environment: 'production',
+      configFile: undefined,
+      logLevel: undefined,
+    });
+  });
+
   describe('database', () => {
     it('should use defaults', () => {
       const { database } = getEnv();
@@ -202,6 +215,11 @@ describe('getEnv', () => {
         trustedProxies: ['10.1.0.0', '10.2.0.0', '169.254.0.0/16'],
       });
     });
+
+    it('should reject invalid trusted proxies', () => {
+      process.env.IMMICH_TRUSTED_PROXIES = '10.1';
+      expect(() => getEnv()).toThrowError('Invalid environment variables: IMMICH_TRUSTED_PROXIES');
+    });
   });
 
   describe('telemetry', () => {
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
index 4fdda028e3..76b0bb0c83 100644
--- a/server/src/repositories/config.repository.ts
+++ b/server/src/repositories/config.repository.ts
@@ -1,17 +1,18 @@
 import { Injectable } from '@nestjs/common';
+import { plainToInstance } from 'class-transformer';
+import { validateSync } from 'class-validator';
 import { Request, Response } from 'express';
 import { CLS_ID } from 'nestjs-cls';
 import { join, resolve } from 'node:path';
 import { citiesFile, excludePaths } from 'src/constants';
 import { Telemetry } from 'src/decorators';
-import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
+import { EnvDto } from 'src/dtos/env.dto';
+import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum';
 import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
 import { QueueName } from 'src/interfaces/job.interface';
 import { setDifference } from 'src/utils/set';
 
-// TODO replace src/config validation with class-validator, here
-
 const productionKeys = {
   client:
     'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=',
@@ -35,8 +36,16 @@ const asSet = <T>(value: string | undefined, defaults: T[]) => {
 };
 
 const getEnv = (): EnvData => {
-  const includedWorkers = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
-  const excludedWorkers = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []);
+  const dto = plainToInstance(EnvDto, process.env);
+  const errors = validateSync(dto);
+  if (errors.length > 0) {
+    throw new Error(
+      `Invalid environment variables: ${errors.map((error) => `${error.property}=${error.value}`).join(', ')}`,
+    );
+  }
+
+  const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
+  const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []);
   const workers = [...setDifference(includedWorkers, excludedWorkers)];
   for (const worker of workers) {
     if (!WORKER_TYPES.has(worker)) {
@@ -44,9 +53,9 @@ const getEnv = (): EnvData => {
     }
   }
 
-  const environment = process.env.IMMICH_ENV as ImmichEnvironment;
+  const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION;
   const isProd = environment === ImmichEnvironment.PRODUCTION;
-  const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
+  const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
   const folders = {
     // eslint-disable-next-line unicorn/prefer-module
     dist: resolve(`${__dirname}/..`),
@@ -54,18 +63,18 @@ const getEnv = (): EnvData => {
     web: join(buildFolder, 'www'),
   };
 
-  const databaseUrl = process.env.DB_URL;
+  const databaseUrl = dto.DB_URL;
 
   let redisConfig = {
-    host: process.env.REDIS_HOSTNAME || 'redis',
-    port: Number.parseInt(process.env.REDIS_PORT || '') || 6379,
-    db: Number.parseInt(process.env.REDIS_DBINDEX || '') || 0,
-    username: process.env.REDIS_USERNAME || undefined,
-    password: process.env.REDIS_PASSWORD || undefined,
-    path: process.env.REDIS_SOCKET || undefined,
+    host: dto.REDIS_HOSTNAME || 'redis',
+    port: dto.REDIS_PORT || 6379,
+    db: dto.REDIS_DBINDEX || 0,
+    username: dto.REDIS_USERNAME || undefined,
+    password: dto.REDIS_PASSWORD || undefined,
+    path: dto.REDIS_SOCKET || undefined,
   };
 
-  const redisUrl = process.env.REDIS_URL;
+  const redisUrl = dto.REDIS_URL;
   if (redisUrl && redisUrl.startsWith('ioredis://')) {
     try {
       redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString());
@@ -75,11 +84,11 @@ const getEnv = (): EnvData => {
   }
 
   const includedTelemetries =
-    process.env.IMMICH_TELEMETRY_INCLUDE === 'all'
+    dto.IMMICH_TELEMETRY_INCLUDE === 'all'
       ? new Set(Object.values(ImmichTelemetry))
-      : asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_INCLUDE, []);
+      : asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_INCLUDE, []);
 
-  const excludedTelemetries = asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_EXCLUDE, []);
+  const excludedTelemetries = asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_EXCLUDE, []);
   const telemetries = setDifference(includedTelemetries, excludedTelemetries);
   for (const telemetry of telemetries) {
     if (!TELEMETRY_TYPES.has(telemetry)) {
@@ -88,26 +97,26 @@ const getEnv = (): EnvData => {
   }
 
   return {
-    host: process.env.IMMICH_HOST,
-    port: Number(process.env.IMMICH_PORT) || 2283,
+    host: dto.IMMICH_HOST,
+    port: dto.IMMICH_PORT || 2283,
     environment,
-    configFile: process.env.IMMICH_CONFIG_FILE,
-    logLevel: process.env.IMMICH_LOG_LEVEL as LogLevel,
+    configFile: dto.IMMICH_CONFIG_FILE,
+    logLevel: dto.IMMICH_LOG_LEVEL,
 
     buildMetadata: {
-      build: process.env.IMMICH_BUILD,
-      buildUrl: process.env.IMMICH_BUILD_URL,
-      buildImage: process.env.IMMICH_BUILD_IMAGE,
-      buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL,
-      repository: process.env.IMMICH_REPOSITORY,
-      repositoryUrl: process.env.IMMICH_REPOSITORY_URL,
-      sourceRef: process.env.IMMICH_SOURCE_REF,
-      sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
-      sourceUrl: process.env.IMMICH_SOURCE_URL,
-      thirdPartySourceUrl: process.env.IMMICH_THIRD_PARTY_SOURCE_URL,
-      thirdPartyBugFeatureUrl: process.env.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
-      thirdPartyDocumentationUrl: process.env.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
-      thirdPartySupportUrl: process.env.IMMICH_THIRD_PARTY_SUPPORT_URL,
+      build: dto.IMMICH_BUILD,
+      buildUrl: dto.IMMICH_BUILD_URL,
+      buildImage: dto.IMMICH_BUILD_IMAGE,
+      buildImageUrl: dto.IMMICH_BUILD_IMAGE_URL,
+      repository: dto.IMMICH_REPOSITORY,
+      repositoryUrl: dto.IMMICH_REPOSITORY_URL,
+      sourceRef: dto.IMMICH_SOURCE_REF,
+      sourceCommit: dto.IMMICH_SOURCE_COMMIT,
+      sourceUrl: dto.IMMICH_SOURCE_URL,
+      thirdPartySourceUrl: dto.IMMICH_THIRD_PARTY_SOURCE_URL,
+      thirdPartyBugFeatureUrl: dto.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
+      thirdPartyDocumentationUrl: dto.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
+      thirdPartySupportUrl: dto.IMMICH_THIRD_PARTY_SUPPORT_URL,
     },
 
     bull: {
@@ -153,26 +162,22 @@ const getEnv = (): EnvData => {
           ? { connectionType: 'url', url: databaseUrl }
           : {
               connectionType: 'parts',
-              host: process.env.DB_HOSTNAME || 'database',
-              port: Number(process.env.DB_PORT) || 5432,
-              username: process.env.DB_USERNAME || 'postgres',
-              password: process.env.DB_PASSWORD || 'postgres',
-              database: process.env.DB_DATABASE_NAME || 'immich',
+              host: dto.DB_HOSTNAME || 'database',
+              port: dto.DB_PORT || 5432,
+              username: dto.DB_USERNAME || 'postgres',
+              password: dto.DB_PASSWORD || 'postgres',
+              database: dto.DB_DATABASE_NAME || 'immich',
             }),
       },
 
-      skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
-      vectorExtension:
-        process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
+      skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
+      vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
     },
 
     licensePublicKey: isProd ? productionKeys : stagingKeys,
 
     network: {
-      trustedProxies: (process.env.IMMICH_TRUSTED_PROXIES ?? '')
-        .split(',')
-        .map((value) => value.trim())
-        .filter(Boolean),
+      trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [],
     },
 
     otel: {
@@ -203,18 +208,18 @@ const getEnv = (): EnvData => {
     },
 
     storage: {
-      ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
+      ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS,
     },
 
     telemetry: {
-      apiPort: Number(process.env.IMMICH_API_METRICS_PORT || '') || 8081,
-      microservicesPort: Number(process.env.IMMICH_MICROSERVICES_METRICS_PORT || '') || 8082,
+      apiPort: dto.IMMICH_API_METRICS_PORT || 8081,
+      microservicesPort: dto.IMMICH_MICROSERVICES_METRICS_PORT || 8082,
       metrics: telemetries,
     },
 
     workers,
 
-    noColor: !!process.env.NO_COLOR,
+    noColor: !!dto.NO_COLOR,
   };
 };
 
diff --git a/server/src/validation.ts b/server/src/validation.ts
index 81b309d663..180d53ed56 100644
--- a/server/src/validation.ts
+++ b/server/src/validation.ts
@@ -25,6 +25,7 @@ import {
 import { CronJob } from 'cron';
 import { DateTime } from 'luxon';
 import sanitize from 'sanitize-filename';
+import { isIP, isIPRange } from 'validator';
 
 @Injectable()
 export class ParseMeUUIDPipe extends ParseUUIDPipe {
@@ -228,3 +229,32 @@ export function MaxDateString(
     validationOptions,
   );
 }
+
+type IsIPRangeOptions = { requireCIDR?: boolean };
+export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator {
+  const { requireCIDR } = { requireCIDR: true, ...options };
+
+  return ValidateBy(
+    {
+      name: 'isIPRange',
+      validator: {
+        validate: (value): boolean => {
+          if (isIPRange(value)) {
+            return true;
+          }
+
+          if (!requireCIDR && isIP(value)) {
+            return true;
+          }
+
+          return false;
+        },
+        defaultMessage: buildMessage(
+          (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range',
+          validationOptions,
+        ),
+      },
+    },
+    validationOptions,
+  );
+}