CLI commands
Besides the server, the image ships maintenance commands under the
litestar CLI. Run them inside the container with
docker compose exec -u geometrikks app litestar <command> (or
docker compose run --rm app litestar <command> when the stack is
stopped). The image sets LITESTAR_APP so the bare command works there;
outside the container, point the CLI at the app yourself:
uv run litestar --app geometrikks.server.core:create_app <command>.
Every command supports --help.
import-logs: backfill history
Section titled “import-logs: backfill history”Live tailing only picks up lines written after the app starts. To backfill
rotated or archived access logs (nginx, Traefik JSON or Caddy JSON, plain
or gzip), use import-logs:
docker compose exec -u geometrikks app litestar import-logs /var/log/access/access.log.1.gzIt reuses the live ingestion pipeline (same parsing, GeoIP lookup and DB
writes), uses the timestamps in each log line rather than wall-clock time,
and refreshes the continuous aggregates for the imported range when done.
The log format is auto-detected per file, as with live tailing; pass
--format geometrikks-json, --format nginx, --format traefik-json or
--format caddy-json to pin it. You can pass several
files in one invocation. Paths are container paths, and the import runs
as the non-root geometrikks user (PUID:PGID, default 1000:1000), so
host files must be readable by it (-u geometrikks keeps exec from
running the import as root).
exec requires the app service to be running. If the stack is stopped,
use run --rm instead:
docker compose run --rm app litestar import-logs /var/log/access/access.log.1.gz[!IMPORTANT] Import archived (rotated) files only. Importing a file that is also being live-tailed double-counts its lines.
- Each imported file is fingerprinted by content checksum; importing the
same content again (even under a different filename) is skipped. Pass
--forceto re-import. That updates the bookkeeping row but does not delete rows written by the earlier import. - A file that matches no supported log format is rejected up front, before anything is written.
- Without
--format, the format is detected per file. If detection can only match the relaxed IP-and-timestamp pattern, the file imports as map events with no access-log rows. Pin the format (--format geometrikks-jsonor--format nginx) to require a full parse; lines that do not match then show up in the skipped count instead. - The TimescaleDB retention policy drops rows older than the raw retention
window (
ANALYTICS_RAW_RETENTION_DAYS, default 180 days), so history beyond that window will not persist. Raise the retention setting before importing older archives if you want to keep them.
backfill-hostname: fix up historical hostnames
Section titled “backfill-hostname: fix up historical hostnames”Every ingested row records which GeoMetrikks instance wrote it
(LOGPARSER_HOST_NAME; defaults to the machine hostname, which the compose
file pins to geometrikks). Rows ingested by older versions have no
hostname, so the access-logs hostname filter cannot see them.
backfill-hostname stamps them retroactively:
docker compose exec -u geometrikks app litestar backfill-hostname myhostThe plain form fills only rows with no hostname. It is idempotent, cannot overwrite stamped values, and runs immediately without a confirmation prompt.
If your database has accumulated many bogus hostnames, add
--consolidate to rewrite all existing hostnames to the given name as
well. The classic cause is running in Docker with LOGPARSER_HOST_NAME
unset before the compose file pinned a hostname: every container
recreation minted a new 12-hex container-ID “hostname”. Consolidate lists
every hostname it will rewrite, with row counts, and asks for confirmation
first (--yes skips the prompt):
docker compose exec -u geometrikks app litestar backfill-hostname myhost --consolidateEither form decompresses compressed history chunks first (a full-table update would trip TimescaleDB’s tuple decompression limit), so disk usage grows until the compression policy recompresses them. It then refreshes the affected continuous aggregates so the filter dropdowns update. It may run for minutes on a large database.
backfill-asn: fill in ASN data for historical rows
Section titled “backfill-asn: fill in ASN data for historical rows”Rows ingested before the ASN feature (or while the ASN database was
missing) have no ASN data. backfill-asn resolves their IPs against the
local GeoLite2 ASN database and stamps them retroactively:
docker compose exec -u geometrikks app litestar backfill-asnIt fills only rows with no ASN data (idempotent, never overwrites
stamped values) and asks for confirmation after reporting how many rows and
distinct IPs are affected (--yes skips the prompt). IPs the database
cannot resolve stay empty. Like backfill-hostname, it decompresses
compressed history chunks first (disk usage grows until the compression
policy recompresses them) and refreshes the ASN continuous aggregates
afterwards so the Top ASNs view picks up the history. It may run for
minutes on a large database.
If the aggregate refresh fails, the command exits non-zero and names the stale aggregates: the rows are stamped, but the Top ASNs view will not show the backfilled range until they refresh. Rerunning is safe.
Today’s ASN database describes today’s network ownership; stamping years-old traffic with it is an approximation.
backfill-timings: clear placeholder response times
Section titled “backfill-timings: clear placeholder response times”Archives in nginx’s built-in combined format have no $request_time.
Older versions stored those rows with a response time of 0.0, which
dragged every average and percentile toward zero. Rows ingested by this
version store no timing at all for such lines; backfill-timings does
the same for the rows that predate it:
docker compose exec -u geometrikks app litestar backfill-timingsIt only touches rows the legacy nginx format wrote without a host, which is
how a combined line looks after import (the custom format always logs
$host), and whose response time is exactly 0. A genuine sub-millisecond
timing on a row with a host is left alone. --hostname NAME and
--before 2026-08-20 narrow the set; the command prints the row count and
time span and asks for confirmation (--yes skips it). Like the other
backfills it decompresses history chunks first and refreshes the affected
continuous aggregates afterwards, so it may run for minutes on a large
database.
Large imports and backfills
Section titled “Large imports and backfills”Bulk operations (import-logs over months of archives, either backfill
command on a database with real history) write WAL much faster than live
tailing does. With PostgreSQL’s default max_wal_size of 1GB the database
checkpoints every few seconds and logs:
LOG: checkpoints are occurring too frequently (21 seconds apart)HINT: Consider increasing the configuration parameter "max_wal_size".Nothing is at risk, but each checkpoint re-triggers full-page writes for the pages the run touches next, so the operation slows down the longer this goes on. Both settings reload without a restart, so you can raise them while a backfill is already running:
docker compose exec timescale_db psql -U geouser -d geometrikks \ -c "ALTER SYSTEM SET max_wal_size = '4GB';" \ -c "ALTER SYSTEM SET checkpoint_timeout = '15min';" \ -c "SELECT pg_reload_conf();"Expect up to max_wal_size of extra disk used for WAL during the run, on
top of the temporarily decompressed chunks. To keep the settings
permanently, add max_wal_size=4GB and checkpoint_timeout=15min to the
database service’s command: block in the compose file, then run
ALTER SYSTEM RESET max_wal_size and ALTER SYSTEM RESET checkpoint_timeout once. Command-line flags override ALTER SYSTEM
values, and keeping both means the compose file no longer tells the whole
truth.