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
- Download/place the cities500 geodata file (e.g. GeoNames cities500.txt) at the configured path
- Verify the geodata path in the env config (configRepository.getEnv().resourcePaths.geodata.cities500) points to the actual file location
- Check the container/volume mount actually includes the geodata directory
- 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
- Download geodata files in the image build / setup script (cities500 + admin1/admin2)
- Assert geodata presence in a startup health check before enabling map features
- Use absolute paths resolved from a known root instead of cwd-relative paths
- Mount geodata as a persistent volume in containerized deployments
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
- Geodata file not found
- Device ' ' does not exist. If using Docker, make sure this…
- Failed to add job handler for
- Failed to find job handler for Job.
- Failed to verify SMTP configuration
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)