Self-hosted location history is becoming its own little category: not just “where is my phone”, but a private memory layer for trips, commutes, photos, favorite places, stats, and long-term movement patterns.

GeoPulse is another serious option in that space. It takes raw GPS points from mobile trackers and imports, then turns them into a searchable timeline with stays, trips, analytics, map views, sharing, and Immich photo context.

What is GeoPulse?

GeoPulse is a self-hosted location timeline platform by tess1o. The project describes itself as a privacy-first Google Timeline alternative, and the feature set backs that up: GPS source ingestion, import/export, trip detection, journey insights, friends, share links, OIDC, admin settings, geofences, notifications, and optional AI-assisted insights.

The important licensing detail: GeoPulse is source-available under BSL 1.1, not a permissive or copyleft open-source license in the normal OSI sense. The license grants personal, private, educational, and non-profit use. Commercial use requires a separate license. The change date listed in the repository is 2075-01-01, when it changes to AGPL-3.0 only.

For a personal home lab, that may be fine. For business use, read the license before deploying it.

Why Self-Host GeoPulse?

  • Own your location archive: GPS history is sensitive enough that keeping it on your own PostGIS database matters.
  • Use different trackers: GeoPulse supports OwnTracks, Overland, GPSLogger, Home Assistant, Traccar, Dawarich, Colota, and bulk imports.
  • Get more than raw points: It builds stays, trips, movement types, insights, reports, and analytics on top of the data.
  • Connect your photo library: Immich integration can bring photo context into your location timeline.
  • Run a multi-user setup: Friends, sharing, invitations, admin roles, OIDC, and audit logs make it more than a single-user map toy.

Tech Overview of GeoPulse

GeoPulse has a clean split between backend, frontend, and database.

The backend is a Java/Quarkus application, shipped as a native container image. It uses PostgreSQL with PostGIS for geospatial persistence, Flyway for migrations, Hibernate Spatial/JTS for geometry work, JWT auth, optional OIDC, MQTT support, and Prometheus metrics.

The frontend is Vue 3 with Vite, PrimeVue, Pinia, Tailwind, Chart.js, Leaflet, and MapLibre GL. That stack fits the product: the app is map-heavy, dashboard-heavy, and form-heavy.

The Docker setup has three mandatory services and one one-shot helper:

  • GeoPulse UI: nginx-served Vue frontend and same-origin API proxy.
  • GeoPulse backend: Quarkus API and processing service.
  • PostGIS database: stores users, GPS points, timeline entities, geofences, trips, places, settings, and spatial indexes.
  • Key generator: creates JWT keys and an AI settings encryption key under ./keys.

Optional pieces include Mosquitto for OwnTracks MQTT mode and Apprise for geofence notifications.

Self-Hosting GeoPulse with Docker

The official repository ships Docker Compose files for a regular deployment and a complete MQTT-enabled deployment. For the Foss Engineer/Home-Lab version, I kept the regular deployment shape and made the database password explicit instead of inheriting upstream placeholder secrets.

Docker Compose Configuration

The reusable Home-Lab compose is here:

GeoPulse Home-Lab Docker config

And the site includes the same snippet below:

# https://github.com/tess1o/geopulse
# https://tess1o.github.io/geopulse/docs/getting-started/deployment/docker-compose
# Validated with: GEOPULSE_POSTGRES_PASSWORD=replace-me GEOPULSE_MQTT_PASSWORD=replace-me docker compose config

name: geopulse

services:
  geopulse-keygen:
    image: alpine:3.22
    container_name: geopulse-keygen
    restart: "no"
    volumes:
      - ./keys:/keys
    command:
      - sh
      - -ec
      - |
        set -e
        mkdir -p /keys
        if [ ! -f /keys/jwt-private-key.pem ] || [ ! -f /keys/jwt-public-key.pem ]; then
          command -v openssl >/dev/null 2>&1 || apk add --no-cache openssl
          openssl genpkey -algorithm RSA -out /keys/jwt-private-key.pem
          openssl rsa -pubout -in /keys/jwt-private-key.pem -out /keys/jwt-public-key.pem
          chmod 644 /keys/jwt-private-key.pem /keys/jwt-public-key.pem
        fi
        if [ ! -f /keys/ai-encryption-key.txt ]; then
          command -v openssl >/dev/null 2>&1 || apk add --no-cache openssl
          openssl rand -base64 32 > /keys/ai-encryption-key.txt
          chmod 644 /keys/ai-encryption-key.txt
        fi

  geopulse-postgres:
    image: postgis/postgis:17-3.5
    container_name: geopulse-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${GEOPULSE_POSTGRES_USERNAME:-geopulse}
      POSTGRES_PASSWORD: ${GEOPULSE_POSTGRES_PASSWORD:?Set GEOPULSE_POSTGRES_PASSWORD in .env}
      POSTGRES_DB: ${GEOPULSE_POSTGRES_DB:-geopulse}
    volumes:
      - geopulse-postgres-data:/var/lib/postgresql/data
    command: >
      postgres
      -c timezone=UTC
      -c shared_buffers=${GEOPULSE_POSTGRES_SHARED_BUFFERS:-256MB}
      -c work_mem=${GEOPULSE_POSTGRES_WORK_MEM:-8MB}
      -c maintenance_work_mem=${GEOPULSE_POSTGRES_MAINTENANCE_WORK_MEM:-64MB}
      -c effective_cache_size=${GEOPULSE_POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB}
      -c max_wal_size=${GEOPULSE_POSTGRES_MAX_WAL_SIZE:-512MB}
      -c checkpoint_completion_target=${GEOPULSE_POSTGRES_CHECKPOINT_TARGET:-0.9}
      -c wal_buffers=${GEOPULSE_POSTGRES_WAL_BUFFERS:-16MB}
      -c random_page_cost=${GEOPULSE_POSTGRES_RANDOM_PAGE_COST:-1.1}
      -c effective_io_concurrency=${GEOPULSE_POSTGRES_IO_CONCURRENCY:-100}
      -c autovacuum_naptime=${GEOPULSE_POSTGRES_AUTOVACUUM_NAPTIME:-60s}
      -c autovacuum_vacuum_scale_factor=${GEOPULSE_POSTGRES_VACUUM_SCALE_FACTOR:-0.2}
      -c log_min_duration_statement=${GEOPULSE_POSTGRES_LOG_SLOW_QUERIES:-5000}
      -c track_io_timing=on
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10

  geopulse-backend:
    image: tess1o/geopulse-backend:${GEOPULSE_VERSION:-1.35.0}-native
    container_name: geopulse-backend
    mem_limit: ${GEOPULSE_BACKEND_MEM_LIMIT:-512m}
    mem_reservation: ${GEOPULSE_BACKEND_MEM_RESERVATION:-128m}
    restart: unless-stopped
    env_file:
      - path: .env
        required: false
    environment:
      GEOPULSE_POSTGRES_HOST: geopulse-postgres
      GEOPULSE_POSTGRES_PORT: 5432
      GEOPULSE_POSTGRES_DB: ${GEOPULSE_POSTGRES_DB:-geopulse}
      GEOPULSE_POSTGRES_USERNAME: ${GEOPULSE_POSTGRES_USERNAME:-geopulse}
      GEOPULSE_POSTGRES_PASSWORD: ${GEOPULSE_POSTGRES_PASSWORD:?Set GEOPULSE_POSTGRES_PASSWORD in .env}
      GEOPULSE_POSTGRES_URL: jdbc:postgresql://geopulse-postgres:5432/${GEOPULSE_POSTGRES_DB:-geopulse}
      GEOPULSE_BACKEND_URL: http://geopulse-backend:8080
    volumes:
      - ./keys:/app/keys
      - ./import-drop:/data/geopulse-import
    depends_on:
      geopulse-keygen:
        condition: service_completed_successfully
      geopulse-postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 30
      start_period: 10s

  geopulse-ui:
    image: tess1o/geopulse-ui:${GEOPULSE_VERSION:-1.35.0}
    container_name: geopulse-ui
    restart: unless-stopped
    env_file:
      - path: .env
        required: false
    ports:
      - "${GEOPULSE_HTTP_PORT:-5555}:80"
    depends_on:
      geopulse-backend:
        condition: service_healthy

  # Optional MQTT broker for OwnTracks MQTT mode. If enabled, set
  # GEOPULSE_MQTT_ENABLED=true and use docker-compose-complete.yml upstream
  # or extend this stack with a Mosquitto service.

volumes:
  geopulse-postgres-data:

Environment File

Create a .env next to the compose file:

cp .env.sample .env

At minimum, replace:

  • GEOPULSE_POSTGRES_PASSWORD
  • GEOPULSE_ADMIN_EMAIL

For local testing:

GEOPULSE_VERSION=1.35.0
GEOPULSE_HTTP_PORT=5555
GEOPULSE_UI_URL=http://localhost:5555
GEOPULSE_POSTGRES_DB=geopulse
GEOPULSE_POSTGRES_USERNAME=geopulse
GEOPULSE_POSTGRES_PASSWORD=replace-with-a-long-random-password
GEOPULSE_ADMIN_EMAIL=[email protected]
GEOPULSE_AUTH_SECURE_COOKIES=false
GEOPULSE_MQTT_ENABLED=false

Then start it:

docker compose config
docker compose up -d

Open:

http://localhost:5555

alt text

There is no fixed default admin password in the docs I reviewed.

Instead, set GEOPULSE_ADMIN_EMAIL to your email, register a user with that email, and GeoPulse promotes that account to admin.

alt text

You will be greeted:

alt text

Field Note: Compose Validation

I validated the Home-Lab and site compose snippets with Docker Compose config rendering:

GEOPULSE_POSTGRES_PASSWORD=replace-me GEOPULSE_MQTT_PASSWORD=replace-me docker compose config

Local environment:

  • Docker 29.6.2
  • Docker Compose v5.3.1
  • 11 GB free on /, with Docker already using 22.57 GB in images and 9.959 GB in volumes
  • Dawarich still running locally on port 3333

Because the filesystem is tight and another location-history stack is already running, I skipped pulling/running the GeoPulse images in this pass. Treat this as compose validation and deployment review, not a completed runtime smoke test.

GPS Sources and Imports

GeoPulse is flexible about where location data comes from.

Live or app-based sources include:

  • OwnTracks: open-source location tracking with either HTTP or MQTT connections. HTTP is simpler for a small deployment; MQTT is useful if you already run a broker and want queue-friendly mobile telemetry.
  • GPSLogger: Android GPSLogger can send HTTP updates with Basic Auth using an OwnTracks-compatible payload. It is a pragmatic Android-first option when you want a lightweight tracker without a bigger app ecosystem.
  • Overland: a simple HTTP endpoint with token-based authentication. This is a clean fit for iOS-style location logging workflows and other clients that speak the Overland format.
  • Traccar: accepts Traccar Position Forwarding JSON with Bearer token authentication. That makes sense if you already use Traccar devices, vehicles, or gateways and want GeoPulse as the personal timeline/analytics layer.
  • Dawarich: GeoPulse can ingest from Dawarich with API key authentication. This is useful if you are testing both tools or want a migration/mirroring path between privacy-focused timeline apps.
  • Home Assistant: integrates with Home Assistant automations for automatic location tracking. It is especially interesting when your phone, router, Bluetooth presence, or smart-home automations already know whether you are home, away, or at a known place.
  • Colota: privacy-focused GPS tracker with batch sync and smart tracking. I would treat it as another mobile-first source option when you want periodic sync rather than always-on direct pushes.

Import sources include:

  • Google Timeline exports
  • GPX
  • GeoJSON
  • OwnTracks exports
  • CSV

For a small setup, OwnTracks over HTTP is likely the easiest first source. If you already run MQTT heavily, the MQTT-enabled compose path can make sense, but it adds Mosquitto and more credentials to manage.

Integrations Worth Noting

The Immich integration is the one I would look at first. If you already self-host Immich , GeoPulse can connect location history with photo context, which makes the map/timeline much more useful than points alone.

Home Assistant support is also interesting. If your phone, router, BLE beacons, or automations already expose location state through Home Assistant , GeoPulse can become the long-term historical layer while Home Assistant remains the automation layer.

The optional AI assistant is bring-your-own-provider. GeoPulse stores AI settings encrypted with a key generated during setup, but you should still treat location-aware AI queries as sensitive.

GeoPulse vs Dawarich

GeoPulse and Dawarich overlap, but they do not feel identical.

Pick GeoPulse if you want:

  • More admin/multi-user controls.
  • OIDC, invitations, audit logs, friends, and share links.
  • Heavier analytics around journeys, reports, movement patterns, and dashboards.
  • OwnTracks MQTT as a first-class optional deployment path.
  • Quarkus/Vue/PostGIS instead of Rails/Sidekiq/Redis/PostGIS.

Pick Dawarich if you want:

  • An AGPL-licensed project.
  • A Rails app with a simpler mental model if you already know that stack.
  • Official mobile apps as a central path.
  • Photo/travel integrations around Immich, PhotoPrism, and AirTrail.

For my own homelab decision, the license is the major dividing line. GeoPulse looks feature-rich, but BSL personal-use-only terms are something to consciously accept rather than ignore.

Reverse Proxy and Security Notes

GeoPulse stores highly personal data. I would not expose it casually.

For home-lab access, prefer one of:

  • VPN-only access through Tailscale, WireGuard, or similar.
  • A reverse proxy with HTTPS and strong identity in front.
  • A private Cloudflare Tunnel route protected with Access.

For a normal same-origin Docker deployment, keep:

GEOPULSE_COOKIE_DOMAIN=
GEOPULSE_CORS_ENABLED=false

When serving through HTTPS, set:

GEOPULSE_PUBLIC_BASE_URL=https://geopulse.example.com
GEOPULSE_AUTH_SECURE_COOKIES=true

Back up both the PostGIS volume and the ./keys directory. The database contains the timeline state; the keys are part of the auth/encryption state.

Conclusion

GeoPulse is a strong self-hosted location timeline platform if you want more than a map full of GPS dots. It has serious product surface: imports, live sources, trips, stays, analytics, sharing, friends, admin controls, OIDC, Immich, geofences, and optional AI.

The Docker architecture is also sensible for a home lab: one frontend, one backend, one PostGIS database, and generated local keys.

The main caveat is not technical. It is licensing. For personal self-hosting, GeoPulse is worth testing. For commercial or organizational use, review the BSL terms and get explicit permission where needed.

FAQ