immich-app/immich · error

Geodata file not found

Error message

Geodata file ${cities500} not found

What it means

loadCities500 verifies that the cities500 geodata file (path from resourcePaths.geodata.cities500 in the env config) exists before importing it during the map geodata import. If existsSync reports the file missing, it aborts the import with this error, since the stream/file read would otherwise fail ungracefully. It indicates a deployment/configuration problem: the geodata asset was never downloaded or the configured path is wrong.

Solutions

  1. Download/place the cities500 geodata file (e.g. GeoNames cities500.txt) at the configured path
  2. Verify the geodata path in the env config (configRepository.getEnv().resourcePaths.geodata.cities500) points to the actual file location
  3. Check the container/volume mount actually includes the geodata directory
  4. Re-run the geodata import after the file is present

Example fix

// before (config points to missing file)
// GEODATA_CITIES500=./data/cities500.txt   (file absent)
// after
// curl -o ./data/cities500.txt https://download.geonames.org/export/dump/cities500.zip && unzip cities500.zip
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from 'fs';
const { cities500 } = config.getEnv().resourcePaths.geodata;
if (!existsSync(cities500)) throw new Error(`Geodata missing: ${cities500} — run the geodata download step first`);

Type guard

const hasGeodata = (p: string): p is string => typeof p === 'string' && p.length > 0 && existsSync(p);

Try / catch

try {
  await mapRepo.importGeodata();
} catch (e) {
  if (String(e.message).includes('not found')) {
    logger.error('Geodata assets missing; skipping map import', { path: e.message });
  } else throw e;
}

Prevention

When it happens

Trigger: importGeodata() -> loadCities500() runs and existsSync(cities500) returns false, i.e. the path in config resourcePaths.geodata.cities500 does not exist on disk at import time.

Common situations: Docker image or deploy artifact shipped without the cities500 download step; GEODATA path env var misconfigured; volume mount missing the geodata directory; file deleted by cleanup job; wrong relative path because the server was started from a different working directory.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/8fed490b4f258e00. Report an issue: GitHub.

Appendix: source

Thrown at server/src/repositories/map.repository.ts:274

                )`.execute(manager);
      await manager.schema.dropTable('geodata_places').execute();
      await manager.schema.alterTable('geodata_places_tmp').renameTo('geodata_places').execute();
    });
    await this.db.schema
      .createIndex('IDX_geodata_gist_earthcoord')
      .on('geodata_places')
      .using('gist')
      .expression(sql`ll_to_earth_public(latitude, longitude)`)
      .execute();
    await this.loadCities500(admin1, admin2);
    await this.createGeodataIndices();
  }

  private async loadCities500(admin1Map: Map<string, string>, admin2Map: Map<string, string>) {
    const { resourcePaths } = this.configRepository.getEnv();
    const cities500 = resourcePaths.geodata.cities500;
    if (!existsSync(cities500)) {
      throw new Error(`Geodata file ${cities500} not found`);
    }

    this.logger.log(`Starting geodata import`);
    const startTime = performance.now();

    const input = createReadStream(cities500, { highWaterMark: 512 * 1024 * 1024 });
    let bufferGeodata = [];
    const lineReader = readLine.createInterface({ input });
    let count = 0;

    let futures = [];
    for await (const line of lineReader) {
      const lineSplit = line.split('\t');
      if ((lineSplit[7] === 'PPLX' && lineSplit[8] !== 'AU') || lineSplit[7] === 'PPLH') {
        continue;
      }

      const geoData = {

View on GitHub (pinned to e55ac299a4)