roamly docs

setup, api reference, and guides for self-hosted gps tracking: map, adventures, visit analytics, and everything else that runs on your server.

introduction

roamly is a self-hosted location tracking platform. push gps from your phone, explore your history on a maplibre map, log adventures, analyze where you've been, and keep every byte on hardware you control.

what you get

  • map — heatmap, speed-colored dots, path lines, time-range filters; postgis vector tiles or classic clustered mode
  • multi-device — separate api keys per device, per-device colors on the map
  • adventures — shared, date-bounded journeys: a day-by-day log, a freeform story, rated places with photos, and every member's own gps track on one map, with a public slug + optional pin for sharing
  • visit analytics — dwell time per city/state/country, yearly period comparisons
  • stats & search — distance charts, location data table, place/date search
  • reverse geocoding — offline, from bundled census boundaries + place data (background job in settings)
  • import & export — csv, gpx, google takeout json, owntracks json, full backup restore
  • s3 backups — scheduled data + media backups to any s3-compatible storage
  • native android app — first-party kotlin client with a reboot-proof background tracker, offline-first uploads, and your map, stats, and adventures on your phone
  • gpslogger & owntracks — or any client that can post json to /api/push/
  • pwa — installable; dark and light themes

tech stack

  • backend: django + python, gunicorn; background work in threads (geocoding, backups, poi download)
  • database: sqlite by default; postgresql + postgis when DATABASE_URL is set (enables vector tiles)
  • frontend: vanilla javascript, maplibre gl, canvas charts
  • deployment: docker compose

docker setup

roamly ships as a prebuilt docker image (waxn/roamly) and runs as a three-container compose stack: postgis database, redis cache, and the django web app. no source checkout, no local build.

prerequisites

  • docker and docker compose
  • 2 gb ram minimum, 1 gb free disk
  • port 8001 available (and optionally 5433 for db access)

install & run

grab the compose file and a minimal .env, then bring everything up:

# pull the compose file
curl -O https://raw.githubusercontent.com/waxn/roamly/main/docker-compose.yml

# minimal .env (set your own password!)
cat > .env <<EOF
POSTGRES_PASSWORD=changeme
DATABASE_URL=postgis://roamly:changeme@db:5432/roamly
SECRET_KEY=$(openssl rand -hex 32)
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=http://localhost:8001,http://127.0.0.1:8001
EOF

# start everything (pulls waxn/roamly image automatically)
docker compose up -d
that's it. roamly is now running at http://localhost:8001. migrations and static files are handled automatically on startup.

the docker-compose.yml

for reference, here's the full compose file you just downloaded:

services:
  redis:
    image: redis:7-alpine
    container_name: roamly_redis
    restart: unless-stopped
    volumes:
      - ./data/redis:/data

  db:
    image: postgis/postgis:16-3.4
    container_name: roamly_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: roamly
      POSTGRES_USER: roamly
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./docker-init:/docker-entrypoint-initdb.d
    ports:
      - "5433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U roamly -d roamly"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    image: waxn/roamly:latest
    # To build locally instead of pulling the published image:
    #   build: .
    container_name: roamly_web
    restart: unless-stopped
    depends_on:
      - redis
      - db
    command: >
      sh -c "python manage.py migrate --noinput &&
             python manage.py collectstatic --noinput &&
             gunicorn roamly.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 300"
    volumes:
      - ./staticfiles:/app/staticfiles
      - ./media:/app/media
      - ./tracker/migrations:/app/tracker/migrations
    ports:
      - "8001:8000"
    environment:
      - DEBUG=${DEBUG:-True}
      - SECRET_KEY=${SECRET_KEY:-django-insecure-change-me-in-production}
      - DATABASE_URL=${DATABASE_URL}
      - ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
      - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS:-http://localhost,http://127.0.0.1}
      - REDIS_URL=redis://redis:6379/1

what gets started

serviceimageportdescription
db postgis/postgis:16-3.4 5433 → 5432 postgresql with postgis spatial extensions
redis redis:7-alpine internal cache backend for tiles, stats, and api responses
web waxn/roamly:latest 8001 → 8000 django app served by gunicorn (3 workers)

data persistence

data lives in bind-mount volumes that survive container rebuilds:

  • ./data/postgres/ — all database data
  • ./data/redis/ — redis cache snapshots
  • ./media/ — uploaded photos and profile pictures
  • ./staticfiles/ — collected static assets

updating

no git pull, no rebuild — just pull the latest image:

docker compose pull
docker compose up -d

useful commands

# view logs
docker compose logs -f web

# stop everything
docker compose down

# open a db shell
docker compose exec db psql -U roamly -d roamly

# back up the database
docker compose exec db pg_dump -U roamly roamly > backup.sql

# restore
docker compose exec -T db psql -U roamly roamly < backup.sql

first steps

after docker compose up -d, follow these steps to start tracking your first device.

1

create an account

open http://localhost:8001 and click get started. enter a username and password — no email required.

2

create an api key

go to settings → api keys → create api key. name it after your device (e.g., "pixel 9"). copy the 64-character key.

3

set up a tracking app

easiest path: build and install the roamly android app and paste your api key. or use gpslogger on android / owntracks on ios — see the gpslogger setup section.

4

verify it's working

after gpslogger sends its first location, check the map page. if nothing appears, see troubleshooting.

5

run geocoding

go to settings → start geocoding to convert coordinates to city/country names. runs in the background.

roamly geocodes new locations automatically as they arrive. the background job is for bulk-processing imports or locations that missed geocoding.

configuration

roamly is configured via environment variables — set them in a .env file in the project root, or directly in your hosting platform (portainer, etc.). no hostnames are hardcoded.

environment variables

variabledescriptiondefault
DEBUG django debug mode. set to False in production. False
SECRET_KEY django secret key. change this in production. change-me-in-production
POSTGRES_PASSWORD database password. roamly_secret
ALLOWED_HOSTS comma-separated hostnames. set to your domain in production. localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS comma-separated urls with protocol. must include your https url. http://localhost
DATABASE_URL full db connection string. auto-configured in docker compose. postgis://roamly:...@db:5432/roamly
ADMIN_SIGNUP_KEY secret that unlocks the admin checkbox on the signup form. set it to let the first account register as an instance admin. analytics snippets are configured in-app at Admin Panel → Custom JS (no env var needed). unset

example .env

DEBUG=False
SECRET_KEY=your-random-secret-key-at-least-50-chars
POSTGRES_PASSWORD=a-strong-database-password

# comma-separated, no protocol
ALLOWED_HOSTS=roamly.yourdomain.com

# comma-separated, include protocol
CSRF_TRUSTED_ORIGINS=https://roamly.yourdomain.com
always change SECRET_KEY and POSTGRES_PASSWORD before exposing roamly to the internet.

generating a secret key

python3 -c "import secrets; print(secrets.token_urlsafe(64))"

roamly android app

roamly ships with a first-party android client (in the mobile/ directory of the repo). it's the easiest way to track from a phone — no custom-url wiring, and a background service designed to keep logging through reboots, doze, and battery saver.

there's no play store or f-droid listing yet. build the app from source with android studio (or ./gradlew assembleRelease) and sideload the apk. it targets android 8.0+ (minsdk 26).

signing in

the app authenticates with an api key, not a password. that key is the durable credential, so the app stays signed in even after the django web session expires. on first launch:

1

enter your server url

point the app at your instance, e.g. https://roamly.yourdomain.com.

2

paste an api key

create one under settings → api keys on the web, then paste it into the app. a device id is assigned automatically, so uploads start working immediately.

3

grant location & allow background

allow precise location all the time and exempt roamly from battery optimization when prompted — that's what lets the tracker keep running with the screen off.

how tracking works

  • foreground service — tracking runs as a persistent foreground service with a notification, so android won't kill it under memory pressure. it restarts on reboot via a boot receiver and re-arms itself with START_STICKY.
  • doze-proof — a watchdog coroutine re-requests location updates if fixes stall under doze or battery saver.
  • offline-first — points are cached locally in a room database and uploaded by a background worker when there's a connection. nothing is lost if your server or network is down. a local csv mirror is written alongside.

tracking settings

everything is tunable live from the settings screen and applied to the running tracker without a restart:

settingwhat it does
intervalhow often to request a new fix
gps priorityauto, high accuracy, balanced, or low power
min movement distanceskip points you haven't moved far enough from
max accuracydiscard fixes worse than this many meters

beyond tracking

the app is a full client, not just an uploader. browse your map, check stats, and open your adventures from your phone, styled with the same Field Journal design as the web app.

prefer not to build an app? gpslogger (android) and owntracks (ios/android) push to the same /api/push/ endpoint with no build step.

gpslogger setup

gpslogger is a free, open-source android app for logging gps data to a custom url. it's lightweight, battery-friendly, and the recommended way to send locations to roamly.

install gpslogger

configuration

1

get your api key

settings → api keys → create api key. name it after your device (e.g., "pixel 9"). copy the key.

2

enable custom url logging

in gpslogger, tap the three-dot menu → logging details, then enable log to custom url.

3

configure the url

tap log to custom url to open its settings:

  • url: https://your-domain.com/api/push/
  • http method: post
  • http body:
{"api_key":"YOUR_KEY","device_id":"%SER","latitude":%LAT,"longitude":%LON,"altitude":%ALT,"speed":%SPD,"accuracy":%ACC,"battery":%BATT,"timestamp":"%TIME"}
  • http headers: Content-Type: application/json
4

set logging interval

go back, tap performance, and set your preferred interval (e.g., 60 seconds).

5

start logging

tap the start button. your points should appear on roamly's map within seconds.

the %SER placeholder sends the device's serial/id, which roamly uses as the device identifier. you can replace it with any fixed string if you prefer a custom device name.

gpslogger not working?

  • make sure the url ends with /api/push/ — trailing slash matters
  • verify Content-Type: application/json is set in http headers
  • check the api key in the json body has no extra spaces
  • confirm your server is reachable from your phone's network
  • check logs: docker compose logs -f web

owntracks (ios & android)

roamly is also fully compatible with owntracks — the recommended option for ios users.

in owntracks preferences, set mode to http and the url to:

https://your-domain.com/api/push/?api_key=YOUR_KEY

tap the publish button (↑) to send a test location.

sending locations

push locations from any app or script that can make http requests. the /api/push/ endpoint accepts json.

authentication

  1. authorization header: Authorization: Bearer YOUR_KEY
  2. url parameter: ?api_key=YOUR_KEY
  3. json body field: "api_key": "YOUR_KEY"

standard format

curl -X POST https://your-domain.com/api/push/ \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "my-phone",
    "latitude": 48.8566,
    "longitude": 2.3522,
    "timestamp": "2026-01-15T14:30:00Z",
    "altitude": 35.0,
    "accuracy": 5.0,
    "speed": 1.2,
    "battery": 85
  }'

field reference

fieldtyperequireddescription
device_idstringyesunique identifier for the device
latitudenumberyesdecimal degrees, -90 to 90
longitudenumberyesdecimal degrees, -180 to 180
timestampstring / numbernoiso 8601 or unix timestamp. defaults to now.
altitudenumbernometers
accuracynumbernogps accuracy in meters
speednumbernometers per second
batterynumberno0–100

owntracks format (compatibility)

roamly also accepts the native owntracks json format directly:

{
  "_type": "location",
  "tid": "ph",
  "lat": 48.8566,
  "lon": 2.3522,
  "tst": 1705325400,
  "alt": 35,
  "acc": 5,
  "vel": 4,
  "batt": 85
}

response

// success (200)
{ "status": "ok", "location_id": 12345, "device": "my-phone" }

// error (401)
{ "error": "Invalid or missing API key" }

python example

import requests, time

resp = requests.post(
    "https://your-domain.com/api/push/",
    json={
        "device_id": "python-script",
        "latitude": 51.5074,
        "longitude": -0.1278,
        "timestamp": int(time.time())
    },
    headers={"Authorization": "Bearer YOUR_KEY"}
)
print(resp.json())

adventures

adventures let you capture any outing — a 7-hour bike ride, a hiking day, a road trip, a vacation. they group location data by a precise start and end time for a specific device.

creating an adventure

1

go to adventures

click adventures in the navigation bar.

2

create adventure

fill in: name, device, start date & time, end date & time, optional description. time precision means you can log a morning run without including the afternoon commute.

3

view the adventure

click on an adventure to open a full-screen map. under 20k points: all dots shown immediately with a heatmap and trail line. over 20k: heatmap-first with progressive dot loading as you zoom.

adding places

places are named waypoints — trailheads, cafés, landmarks. roamly calculates time spent using haversine proximity.

  1. open an adventure → sidebar → places → click + add place
  2. click on the map to drop a pin
  3. set a radius in meters — gps points within the radius count as "at this place"
time calculation: roamly uses haversine to find which points fall within a place's radius, then sums gaps between consecutive in-radius points (gaps over 2 hours are skipped).

timeline & notes

each adventure has a sidebar with a notes area, a timeline of posts, and a milestones section.

  • notes: free-form text for the adventure; saved automatically.
  • timeline: post updates with optional photos and a pinned map location. useful for logging moments during the activity.
  • milestones: named events with an emoji and date — summit reached, border crossed, finish line.

sharing

adventures can be made public with a custom slug. optional access pin gates the public page — visitors enter the pin once per browser session. share link format: /adventure/your-slug/

import & export

your data is yours — export anytime in open formats, import from other services.

export formats

  • csv — all locations as a spreadsheet. columns: device, lat, lng, altitude, accuracy, speed, battery, timestamp, city, state, country, country_code.
  • gpx 1.1 — compatible with google earth, qgis, garmin, most gps software.
  • json backup — everything: devices, locations, adventures, places, api keys. use this to migrate servers or take a complete snapshot.

import formats

  • csv — flexible column detection (garmin, strava, gpslogger aliases). minimum: latitude, longitude, timestamp.
  • gpx — track points, route points, and waypoints from gpx 1.0 and 1.1.
  • json — google takeout {locations:[...]} or owntracks array format.
  • restore from backup — full json snapshot; duplicate locations are skipped automatically.

imports prompt you to pick or create a device name so all points land on the right track.

geocoding

reverse geocoding converts coordinates to city, state, and country names. roamly does this entirely offline, on your own server — no api, no api key, no rate limit, and no coordinate ever leaves the machine to be named.

how it works

  • us points: point-in-polygon against us census tiger boundaries, so a point resolves to the town that actually contains it rather than the nearest larger one whose centre happens to be closer
  • everywhere else: nearest city from a bundled offline place database
  • never on the push path: /api/push/ stores the point with a blank city and returns immediately; a background worker labels it moments later, so a slow lookup can never delay an upload

loading boundary data

the tiger boundaries are a one-off download, not something a migration can do. an admin runs:

sudo docker compose exec web python manage.py import_boundaries

--states ME,VA limits which states are fetched; --regeocode relabels every stored point with the new data. without this, us points fall back to the offline place database, which is coarser.

caching

the worker caches one lookup per ~111m grid cell for the duration of a run, so a hundred thousand points across a handful of cities cost a few thousand polygon tests rather than a hundred thousand.

what gets stored per location

  • city — city, town, village, or municipality
  • state — state, province, or region
  • country — full country name
  • country_code — iso 3166-1 alpha-2 (e.g., us, de)
  • place_name — the fuller place description, when one is available

automatic backups

roamly can back up all your data to any s3-compatible storage — aws s3, backblaze b2, cloudflare r2, minio, wasabi.

supported providers

providerendpoint url
aws s3https://s3.amazonaws.com
backblaze b2https://s3.us-west-000.backblazeb2.com
cloudflare r2https://<account-id>.r2.cloudflarestorage.com
miniohttp://your-minio-server:9000
wasabihttps://s3.wasabisys.com

setup

1

create a bucket

create a bucket in your provider. note the name, endpoint url, and region.

2

get credentials

create an access key and secret key with read/write access to the bucket.

3

configure in roamly

settings → automatic backups → fill in endpoint url, bucket name, access key, secret key, path prefix, region.

4

test and schedule

click test connection, then choose a schedule: daily, weekly, or monthly.

backup format

each backup is the same .zip you get from download backup in settings: a backup.json with all your data (devices, locations, adventures, journals, places, and more) plus a media/ folder with your photos and videos. restore it from settings → restore backup in one step.

{prefix}{username}/backup_{YYYY-MM-DD_HHmmss}.zip
# e.g.: roamly-backups/alice/backup_2026-02-17_143022.zip

photos & videos

media is included by default. turn off include photos & videos in the same settings panel for a smaller, data-only backup.

retention

set a max backups to keep value to have roamly prune the oldest data backups automatically. leave it at 0 to keep everything.

the backup scheduler checks every 15 minutes whether a backup is due, so the exact run time varies by up to 15 minutes from the configured interval.

reverse proxy

for production, put roamly behind a reverse proxy for https and clean domain routing.

nginx

server {
    listen 80;
    server_name roamly.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name roamly.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/roamly.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/roamly.yourdomain.com/privkey.pem;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:8001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

let's encrypt

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d roamly.yourdomain.com
update your .env when using a reverse proxy:
ALLOWED_HOSTS=roamly.yourdomain.com
CSRF_TRUSTED_ORIGINS=https://roamly.yourdomain.com

caddy (simpler)

caddy handles ssl automatically with zero configuration:

# Caddyfile
roamly.yourdomain.com {
    reverse_proxy localhost:8001
}

security

production checklist

  • set DEBUG=False
  • change SECRET_KEY to 50+ random characters
  • change POSTGRES_PASSWORD to something strong
  • set ALLOWED_HOSTS to your domain only
  • set CSRF_TRUSTED_ORIGINS to your https url
  • use https via nginx or caddy + let's encrypt
  • don't expose port 5433 to the internet

api key practices

  • create a separate key per device
  • deactivate keys you're not using
  • delete keys for retired devices
  • roamly tracks last-used time per key — audit occasionally

data deletion

from settings:

  • delete all locations — removes location data, keeps account and adventures
  • delete account — permanently removes everything

updating

docker compose pull
docker compose up -d

pulls the latest waxn/roamly image and restarts the web container. migrations run automatically on startup. no git pull, no rebuild.

back up before updating:
docker compose exec db pg_dump -U roamly roamly > backup_$(date +%Y%m%d).sql

api reference

all endpoints return json. api key auth uses the key in a header, query param, or body. session auth uses a logged-in browser session.

location data

methodendpointauthdescription
POST/api/push/api keypush a location point
GET/api/locations/sessionlist locations with filtering
GET/api/locations/geojson/sessionlocations as geojson
GET/api/tiles/<z>/<x>/<y>.pbfsessionmapbox vector tiles (postgis)
DELETE/api/locations/<id>/delete/sessiondelete a location point

statistics

methodendpointauthdescription
GET/api/stats/sessionsummary: total points, countries, cities, date range
GET/api/stats/yearly/sessionweek/month/year overview with period-over-period comparisons and top places
GET/api/visits/sessioncity/state/country visit counts with time spent
GET/api/distance/sessiondaily distance in km
GET/api/search/sessionsearch history by date, place, or cached osm poi

adventures

the paths keep the /api/trips/ prefix for backward compatibility — the ui and models call them adventures.

methodendpointauthdescription
GET/api/trips/sessionlist all adventures
POST/api/trips/create/sessioncreate an adventure
GET/api/trips/<id>/sessionadventure details with locations, places, and day notes
POST/api/trips/<id>/update/sessionupdate adventure name/description
DELETE/api/trips/<id>/delete/sessiondelete adventure (locations preserved)
POST/api/trips/<id>/places/create/sessionadd a place (with optional rating and category)
POST/api/trips/<id>/places/<pid>/update/sessionupdate a place
DELETE/api/trips/<id>/places/<pid>/delete/sessiondelete a place

geocoding

methodendpointauthdescription
POST/api/geocode/sessionstart background geocoding job
GET/api/geocode/status/sessioncheck geocoding progress
POST/api/geocode/stop/sessionstop the geocoding job

import & export

methodendpointauthdescription
GET/api/export/csv/sessiondownload all locations as csv
GET/api/export/gpx/sessiondownload all locations as gpx 1.1
POST/api/export/backup/start/sessionkick off a full json backup (runs as a background job)
GET/api/export/backup/status/<job>/sessionpoll backup job progress
GET/api/export/backup/download/<job>/sessiondownload the finished backup file
POST/api/import/csv/sessionupload and import csv
POST/api/import/gpx/sessionupload and import gpx
POST/api/import/backup/sessionrestore from json backup

backups

methodendpointauthdescription
GET/api/backup/config/sessionget s3 backup config
POST/api/backup/config/sessionsave s3 config and schedule
POST/api/backup/test/sessiontest s3 connection
POST/api/backup/now/sessiontrigger immediate backup
GET/api/backup/status/sessionlast backup status, time, size

query parameters

most GET endpoints accept:

parameterdescriptionexample
hourslast n hours?hours=168
allall-time data?all=1
device_idfilter by device?device_id=my-phone
start_datedate range start (iso)?start_date=2026-01-01
end_datedate range end (iso)?end_date=2026-01-31

architecture

system overview

mobile device (gpslogger / owntracks / custom app / script)
    │
    │ POST /api/push/  (api key auth)
    ▼
roamly web server (django + gunicorn)
    ├── api endpoints (push, locations, adventures, import/export)
    ├── web ui (maplibre gl, canvas charts, vanilla js)
    └── postgresql 16 + postgis 3.4
            ├── locations with point geometry (srid 4326)
            ├── vector tiles via st_asmvt (30s cache)
            └── spatial filtering with st_intersects

background threads
    ├── reverse geocoding (offline: tiger polygons + place database)
    ├── visit computation (precomputed stays per location)
    ├── osm poi download (overpass, for search)
    ├── s3 backup scheduler (data + images, checks every 15 min)
    └── auto-resume stale jobs on startup

data model

user (django auth)
├── apikey          — 64-char hex keys for device auth
├── device          — unique per (user, device_id)
│   ├── location    — lat/lng/point, timestamp, geocoded fields
│   └── visit       — precomputed stay (start/end, point count, optional poi)
├── adventure       — name, start/end time, public slug, block-based story body
│   ├── adventureblurb      — located place / timeline post (+ rating, category, photos, comments)
│   ├── adventuredaynote     — per-member, per-day log entry (+ photos)
│   ├── adventuremilestone  — titled event with emoji + date
│   ├── plannedstop         — forward-looking itinerary stop
│   └── adventuremember     — shared access (creator / member); each brings their own track
├── poi             — cached openstreetmap points of interest
├── userprofile     — profile picture
├── geocodingjob / visitjob / poidownloadjob — one per user, track progress
└── backupconfig    — s3 creds + schedule for data and (optionally) images

adventures were renamed from "trips" — the api paths (/api/trips/) and internal view names keep the old word for backward compatibility, while the ui and models use "adventure".

vector tiles

the map uses postgis-generated mapbox vector tiles. instead of sending thousands of geojson points, the server generates compact binary tiles via ST_AsMVT(). each tile has two layers:

  • locations — individual points
  • trails — connected line strings between consecutive points

tiles are cached for 30 seconds and capped at 100k points per tile.

troubleshooting

docker won't start

  • check docker is running: docker info
  • check port 8001 is free: ss -tlnp | grep 8001
  • view logs: docker compose logs web
  • db issues: docker compose logs db

locations not appearing on the map

  • verify the api key — copy-paste, check for spaces
  • check the key is active in settings
  • check the time range filter — try "all time"
  • test with curl:
    curl -X POST http://localhost:8001/api/push/ \
      -H "Content-Type: application/json" \
      -d '{
        "api_key": "YOUR_KEY",
        "device_id": "test",
        "latitude": 40.7128,
        "longitude": -74.006
      }'
  • check logs: docker compose logs -f web

gpslogger / owntracks not connecting

  • url must end with /api/push/ — trailing slash required
  • for owntracks: confirm you're using http mode, not mqtt
  • if using https, verify the ssl certificate is valid
  • open the push url in your phone's browser to test reachability

geocoding not working

geocoding is offline, so this is never a network or rate-limit problem.

  • points stay unlabelled until the background worker runs — start it from settings → background jobs
  • us towns resolving to the wrong place usually means the tiger boundaries were never loaded: run import_boundaries (see the geocoding section)
  • after loading boundaries, existing points keep their old labels until you re-run with --regeocode

csrf errors behind a reverse proxy

  • set CSRF_TRUSTED_ORIGINS=https://roamly.yourdomain.com
  • confirm your proxy passes X-Forwarded-Proto

"allowed hosts" error

  • set ALLOWED_HOSTS=roamly.yourdomain.com
  • for local access too: ALLOWED_HOSTS=roamly.yourdomain.com,localhost

faq

is roamly actually private?

yes. it runs on your server, with no telemetry and no data sharing, and geocoding is fully offline. your browser still fetches map tiles from a tile provider, which is unavoidable for a map; everything else that reaches the internet is either admin-controlled or off until you turn it on. the full list is on this instance's privacy page.

can multiple people share one instance?

yes. each user gets their own account with separate data, devices, adventures, and api keys. no cross-user data access.

how much storage does it use?

a phone sending every 5 minutes generates ~100k points/year, which is roughly 50–100 mb in postgresql. postgis handles millions of points fine.

can i run this on a raspberry pi?

yes. roamly runs on arm64. at least 2 gb ram recommended. the postgis docker image supports arm64.

do i need a domain name?

for remote access over the internet, yes — or use tailscale/vpn for your phone to reach the server. for local network only, an ip address works fine.

what if my server goes down?

gpslogger and owntracks both buffer locations on-device and send them when the connection restores. your existing data is safe on disk in the postgres volume.

can i use it without gpslogger or owntracks?

absolutely. anything that can post json over http works — curl, python, tasker, shortcuts, home assistant, custom apps.

how do i migrate to a new server?

  1. export a full backup: settings → export backup (json)
  2. or dump postgres: docker compose exec db pg_dump -U roamly roamly > backup.sql
  3. set up roamly on the new server
  4. import the backup or restore the sql dump

does roamly support real-time tracking?

locations are stored as they arrive but there's no live-push to the browser. refresh the map to see the latest. gpslogger and owntracks can both send updates every few seconds for near-real-time.