diff --git a/Makefile b/Makefile
index 8cc41062bb..ee0a7ea309 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,10 @@ dev-update:
 	docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 
 dev-scale:
-	docker-compose -f ./docker/docker-compose.dev.yml up --build -V  --scale immich_server=3 --remove-orphans 
+	docker-compose -f ./docker/docker-compose.dev.yml up --build -V  --scale immich_server=3 --remove-orphans
+
+test-e2e:
+	docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
 
 prod:
 	docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
diff --git a/docker/.env.test b/docker/.env.test
new file mode 100644
index 0000000000..f5438bb2b1
--- /dev/null
+++ b/docker/.env.test
@@ -0,0 +1,16 @@
+DB_HOSTNAME=immich_postgres_test
+# Database
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+DB_DATABASE_NAME=e2e_test
+
+# Upload File Config
+UPLOAD_LOCATION=./upload
+
+# JWT SECRET
+JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
+
+# MAPBOX
+## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
+ENABLE_MAPBOX=false
+MAPBOX_KEY=
\ No newline at end of file
diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml
new file mode 100644
index 0000000000..6d7a83700b
--- /dev/null
+++ b/docker/docker-compose.test.yml
@@ -0,0 +1,52 @@
+version: "3.8"
+
+services:
+  immich_server_test:
+    image: immich-server-dev:1.9.0
+    build:
+      context: ../server
+      dockerfile: Dockerfile
+    command: npm run test:e2e
+    expose:
+      - "3000"
+    volumes:
+      - ../server:/usr/src/app
+      - /usr/src/app/node_modules
+    env_file:
+      - .env.test
+    environment:
+      - NODE_ENV=development
+    depends_on:
+      - redis
+      - database
+    networks:
+      - immich_network_test
+
+
+  redis:
+    container_name: immich_redis_test
+    image: redis:6.2
+    networks:
+      - immich_network_test
+
+  database:
+    container_name: immich_postgres_test
+    image: postgres:14
+    env_file:
+      - .env.test
+    environment:
+      POSTGRES_PASSWORD: ${DB_PASSWORD}
+      POSTGRES_USER: ${DB_USERNAME}
+      POSTGRES_DB: ${DB_DATABASE_NAME}
+      PG_DATA: /var/lib/postgresql/data
+    volumes:
+      - pgdata-test:/var/lib/postgresql/data
+    ports:
+      - 5432:5432
+    networks:
+      - immich_network_test
+
+networks:
+  immich_network_test:
+volumes:
+  pgdata-test:
diff --git a/server/src/config/database.config.ts b/server/src/config/database.config.ts
index dec963812e..90a09f5ae8 100644
--- a/server/src/config/database.config.ts
+++ b/server/src/config/database.config.ts
@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
 
 export const databaseConfig: TypeOrmModuleOptions = {
   type: 'postgres',
-  host: 'immich_postgres',
+  host: process.env.DB_HOSTNAME || 'immich_postgres',
   port: 5432,
   username: process.env.DB_USERNAME,
   password: process.env.DB_PASSWORD,
diff --git a/server/test/app.e2e-spec.ts b/server/test/app.e2e-spec.ts
deleted file mode 100644
index 50cda62332..0000000000
--- a/server/test/app.e2e-spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Test, TestingModule } from '@nestjs/testing';
-import { INestApplication } from '@nestjs/common';
-import * as request from 'supertest';
-import { AppModule } from './../src/app.module';
-
-describe('AppController (e2e)', () => {
-  let app: INestApplication;
-
-  beforeEach(async () => {
-    const moduleFixture: TestingModule = await Test.createTestingModule({
-      imports: [AppModule],
-    }).compile();
-
-    app = moduleFixture.createNestApplication();
-    await app.init();
-  });
-
-  it('/ (GET)', () => {
-    return request(app.getHttpServer())
-      .get('/')
-      .expect(200)
-      .expect('Hello World!');
-  });
-});
diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts
new file mode 100644
index 0000000000..04aa276d74
--- /dev/null
+++ b/server/test/test-utils.ts
@@ -0,0 +1,37 @@
+import { getConnection } from 'typeorm';
+import { CanActivate, ExecutionContext } from '@nestjs/common';
+import { TestingModuleBuilder } from '@nestjs/testing';
+import { AuthUserDto } from '../src/decorators/auth-user.decorator';
+import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard';
+
+type CustomAuthCallback = () => AuthUserDto;
+
+export async function clearDb() {
+  const entities = getConnection().entityMetadatas;
+  for (const entity of entities) {
+    const repository = getConnection().getRepository(entity.name);
+    await repository.query(`TRUNCATE ${entity.tableName} RESTART IDENTITY CASCADE;`);
+  }
+}
+
+export function getAuthUser(): AuthUserDto {
+  return {
+    id: '3108ac14-8afb-4b7e-87fd-39ebb6b79750',
+    email: 'test@email.com',
+  };
+}
+
+export function auth(builder: TestingModuleBuilder): TestingModuleBuilder {
+  return authCustom(builder, getAuthUser);
+}
+
+export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCallback): TestingModuleBuilder {
+  const canActivate: CanActivate = {
+    canActivate: (context: ExecutionContext) => {
+      const req = context.switchToHttp().getRequest();
+      req.user = callback();
+      return true;
+    },
+  };
+  return builder.overrideGuard(JwtAuthGuard).useValue(canActivate);
+}
diff --git a/server/test/user.e2e-spec.ts b/server/test/user.e2e-spec.ts
new file mode 100644
index 0000000000..4d6d373eb0
--- /dev/null
+++ b/server/test/user.e2e-spec.ts
@@ -0,0 +1,96 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { INestApplication } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import request from 'supertest';
+import { clearDb, authCustom } from './test-utils';
+import { databaseConfig } from '../src/config/database.config';
+import { UserModule } from '../src/api-v1/user/user.module';
+import { AuthModule } from '../src/api-v1/auth/auth.module';
+import { AuthService } from '../src/api-v1/auth/auth.service';
+import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
+import { SignUpDto } from '../src/api-v1/auth/dto/sign-up.dto';
+import { AuthUserDto } from '../src/decorators/auth-user.decorator';
+
+function _createUser(authService: AuthService, data: SignUpDto) {
+  return authService.signUp(data);
+}
+
+describe('User', () => {
+  let app: INestApplication;
+
+  afterAll(async () => {
+    await clearDb();
+    await app.close();
+  });
+
+  describe('without auth', () => {
+    beforeAll(async () => {
+      const moduleFixture: TestingModule = await Test.createTestingModule({
+        imports: [UserModule, ImmichJwtModule, TypeOrmModule.forRoot(databaseConfig)],
+      }).compile();
+
+      app = moduleFixture.createNestApplication();
+      await app.init();
+    });
+
+    afterAll(async () => {
+      await app.close();
+    });
+
+    it('prevents fetching users if not auth', async () => {
+      const { status } = await request(app.getHttpServer()).get('/user');
+      expect(status).toEqual(401);
+    });
+  });
+
+  describe('with auth', () => {
+    let authService: AuthService;
+    let authUser: AuthUserDto;
+
+    beforeAll(async () => {
+      const builder = Test.createTestingModule({
+        imports: [UserModule, AuthModule, TypeOrmModule.forRoot(databaseConfig)],
+      });
+      const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile();
+
+      app = moduleFixture.createNestApplication();
+      authService = app.get(AuthService);
+      await app.init();
+    });
+
+    describe('with users in DB', () => {
+      const authUserEmail = 'auth-user@test.com';
+      const userOneEmail = 'one@test.com';
+      const userTwoEmail = 'two@test.com';
+
+      beforeAll(async () => {
+        await Promise.allSettled([
+          _createUser(authService, { email: authUserEmail, password: '1234' }).then((user) => (authUser = user)),
+          _createUser(authService, { email: userOneEmail, password: '1234' }),
+          _createUser(authService, { email: userTwoEmail, password: '1234' }),
+        ]);
+      });
+
+      it('fetches the user collection excluding the auth user', async () => {
+        const { status, body } = await request(app.getHttpServer()).get('/user');
+        expect(status).toEqual(200);
+        expect(body).toHaveLength(2);
+        expect(body).toEqual(
+          expect.arrayContaining([
+            {
+              email: userOneEmail,
+              id: expect.anything(),
+              createdAt: expect.anything(),
+            },
+            {
+              email: userTwoEmail,
+              id: expect.anything(),
+              createdAt: expect.anything(),
+            },
+          ]),
+        );
+        expect(body).toEqual(expect.not.arrayContaining([expect.objectContaining({ email: authUserEmail })]));
+      });
+    });
+  });
+});
diff --git a/server/tsconfig.json b/server/tsconfig.json
index b9c6829156..7039c7253a 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -9,15 +9,13 @@
     "target": "es2017",
     "sourceMap": true,
     "outDir": "./dist",
-    "baseUrl": "./",
     "incremental": true,
     "skipLibCheck": true,
     "esModuleInterop": true,
   },
   "exclude": [
+    "dist",
+    "node_modules",
     "upload"
   ],
-  "include": [
-    "src"
-  ]
 }
\ No newline at end of file