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_URLis 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
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
| service | image | port | description |
|---|---|---|---|
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.
create an account
open http://localhost:8001 and click get started. enter a username and password — no email required.
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.
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.
verify it's working
after gpslogger sends its first location, check the map page. if nothing appears, see troubleshooting.
run geocoding
go to settings → start geocoding to convert coordinates to city/country names. runs in the background.
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
| variable | description | default |
|---|---|---|
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
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.
./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:
enter your server url
point the app at your instance, e.g. https://roamly.yourdomain.com.
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.
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:
| setting | what it does |
|---|---|
| interval | how often to request a new fix |
| gps priority | auto, high accuracy, balanced, or low power |
| min movement distance | skip points you haven't moved far enough from |
| max accuracy | discard 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.
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
- android: f-droid or google play
configuration
get your api key
settings → api keys → create api key. name it after your device (e.g., "pixel 9"). copy the key.
enable custom url logging
in gpslogger, tap the three-dot menu → logging details, then enable log to custom url.
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
set logging interval
go back, tap performance, and set your preferred interval (e.g., 60 seconds).
start logging
tap the start button. your points should appear on roamly's map within seconds.
%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/jsonis 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.
- ios: app store
- android: google play or f-droid
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
- authorization header:
Authorization: Bearer YOUR_KEY - url parameter:
?api_key=YOUR_KEY - 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
| field | type | required | description |
|---|---|---|---|
device_id | string | yes | unique identifier for the device |
latitude | number | yes | decimal degrees, -90 to 90 |
longitude | number | yes | decimal degrees, -180 to 180 |
timestamp | string / number | no | iso 8601 or unix timestamp. defaults to now. |
altitude | number | no | meters |
accuracy | number | no | gps accuracy in meters |
speed | number | no | meters per second |
battery | number | no | 0–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
go to adventures
click adventures in the navigation bar.
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.
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.
- open an adventure → sidebar → places → click + add place
- click on the map to drop a pin
- set a radius in meters — gps points within the radius count as "at this place"
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 municipalitystate— state, province, or regioncountry— full country namecountry_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
| provider | endpoint url |
|---|---|
| aws s3 | https://s3.amazonaws.com |
| backblaze b2 | https://s3.us-west-000.backblazeb2.com |
| cloudflare r2 | https://<account-id>.r2.cloudflarestorage.com |
| minio | http://your-minio-server:9000 |
| wasabi | https://s3.wasabisys.com |
setup
create a bucket
create a bucket in your provider. note the name, endpoint url, and region.
get credentials
create an access key and secret key with read/write access to the bucket.
configure in roamly
settings → automatic backups → fill in endpoint url, bucket name, access key, secret key, path prefix, region.
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.
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
.env when using a reverse proxy:ALLOWED_HOSTS=roamly.yourdomain.comCSRF_TRUSTED_ORIGINS=https://roamly.yourdomain.comcaddy (simpler)
caddy handles ssl automatically with zero configuration:
# Caddyfile
roamly.yourdomain.com {
reverse_proxy localhost:8001
}
security
production checklist
- set
DEBUG=False - change
SECRET_KEYto 50+ random characters - change
POSTGRES_PASSWORDto something strong - set
ALLOWED_HOSTSto your domain only - set
CSRF_TRUSTED_ORIGINSto 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.
docker compose exec db pg_dump -U roamly roamly > backup_$(date +%Y%m%d).sqlapi 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
| method | endpoint | auth | description |
|---|---|---|---|
| POST | /api/push/ | api key | push a location point |
| GET | /api/locations/ | session | list locations with filtering |
| GET | /api/locations/geojson/ | session | locations as geojson |
| GET | /api/tiles/<z>/<x>/<y>.pbf | session | mapbox vector tiles (postgis) |
| DELETE | /api/locations/<id>/delete/ | session | delete a location point |
statistics
| method | endpoint | auth | description |
|---|---|---|---|
| GET | /api/stats/ | session | summary: total points, countries, cities, date range |
| GET | /api/stats/yearly/ | session | week/month/year overview with period-over-period comparisons and top places |
| GET | /api/visits/ | session | city/state/country visit counts with time spent |
| GET | /api/distance/ | session | daily distance in km |
| GET | /api/search/ | session | search 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.
| method | endpoint | auth | description |
|---|---|---|---|
| GET | /api/trips/ | session | list all adventures |
| POST | /api/trips/create/ | session | create an adventure |
| GET | /api/trips/<id>/ | session | adventure details with locations, places, and day notes |
| POST | /api/trips/<id>/update/ | session | update adventure name/description |
| DELETE | /api/trips/<id>/delete/ | session | delete adventure (locations preserved) |
| POST | /api/trips/<id>/places/create/ | session | add a place (with optional rating and category) |
| POST | /api/trips/<id>/places/<pid>/update/ | session | update a place |
| DELETE | /api/trips/<id>/places/<pid>/delete/ | session | delete a place |
geocoding
| method | endpoint | auth | description |
|---|---|---|---|
| POST | /api/geocode/ | session | start background geocoding job |
| GET | /api/geocode/status/ | session | check geocoding progress |
| POST | /api/geocode/stop/ | session | stop the geocoding job |
import & export
| method | endpoint | auth | description |
|---|---|---|---|
| GET | /api/export/csv/ | session | download all locations as csv |
| GET | /api/export/gpx/ | session | download all locations as gpx 1.1 |
| POST | /api/export/backup/start/ | session | kick off a full json backup (runs as a background job) |
| GET | /api/export/backup/status/<job>/ | session | poll backup job progress |
| GET | /api/export/backup/download/<job>/ | session | download the finished backup file |
| POST | /api/import/csv/ | session | upload and import csv |
| POST | /api/import/gpx/ | session | upload and import gpx |
| POST | /api/import/backup/ | session | restore from json backup |
backups
| method | endpoint | auth | description |
|---|---|---|---|
| GET | /api/backup/config/ | session | get s3 backup config |
| POST | /api/backup/config/ | session | save s3 config and schedule |
| POST | /api/backup/test/ | session | test s3 connection |
| POST | /api/backup/now/ | session | trigger immediate backup |
| GET | /api/backup/status/ | session | last backup status, time, size |
query parameters
most GET endpoints accept:
| parameter | description | example |
|---|---|---|
hours | last n hours | ?hours=168 |
all | all-time data | ?all=1 |
device_id | filter by device | ?device_id=my-phone |
start_date | date range start (iso) | ?start_date=2026-01-01 |
end_date | date 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?
- export a full backup: settings → export backup (json)
- or dump postgres:
docker compose exec db pg_dump -U roamly roamly > backup.sql - set up roamly on the new server
- 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.