From 8bf17866434eba9c19b0b78de6ab3802833dbb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Mon, 27 Jul 2026 13:40:35 +0200 Subject: [PATCH 01/10] fix: osx patch --- .gitignore | 27 +++++ Dockerfile | 11 +- README.md | 14 ++- add-marker.sh | 126 ++++++++++++++++++++++ docker-compose.yml | 30 ++++-- ots/.gitignore | 8 +- ots_config.env => ots_config.env.template | 5 +- patches/fix_eud_handler.py | 74 +++++++++++++ 8 files changed, 279 insertions(+), 16 deletions(-) create mode 100644 .gitignore create mode 100755 add-marker.sh rename ots_config.env => ots_config.env.template (85%) create mode 100644 patches/fix_eud_handler.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bae36c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Runtime state and secrets generated by OpenTAKServer on first boot. +# None of this belongs in version control — the tracked content under ots/ is +# limited to configs/ and mediamtx/ templates. + +# PKI: CA signing key, issued client keys, .p12 bundles, CRL state. +# ca-do-not-share.key signs every cert the server trusts. +# Note the trailing /* rather than /: excluding the directory itself would make +# the !negation below impossible to honour. +/ots/ca/* +!/ots/ca/.gitignore + +# Postgres data directory +/ots/pgdata/ + +# Logs, uploads, and generated runtime config (holds broker credentials) +/ots/logs/ +/ots/uploads/ +/ots/config.yml +/ots/icons.sqlite + +# Certbot / Let's Encrypt material +/ots/configs/nginx/letsencrypt/ + +# macOS +.DS_Store + +ots_config.env diff --git a/Dockerfile b/Dockerfile index 431f09a..e32c59f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,15 @@ RUN chown -R ots:ots /app RUN python -m venv /app/venv ENV PATH="/app/venv/bin:$PATH" -# TODO: Install from PyPI -RUN pip install git+https://github.com/brian7704/OpenTAKServer.git +# Pinned to a tagged release. Unpinned git HEAD ships mid-development +# snapshots; 1.7.11 is the highest version with matching ghcr handler images. +RUN pip install opentakserver==1.7.11 + +# Fix two upstream 1.7.11 bugs that stop CoT reaching clients. See the script +# for details; it fails the build if either anchor is missing, so a version bump +# cannot silently drop a patch. +COPY --chown=ots:ots patches/fix_eud_handler.py /tmp/fix_eud_handler.py +RUN python3 /tmp/fix_eud_handler.py RUN /app/venv/bin/flask --app /app/venv/lib/python3.13/site-packages/opentakserver/app.py ots create-ca #RUN /app/venv/bin/flask --app /app/venv/lib/python3.13/site-packages/opentakserver/app.py db upgrade diff --git a/README.md b/README.md index 69dc0f7..1fac86e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ # OpenTAKServer-Docker +``` +cp ots_config.env.template ots_config.env +``` + +`ots_config.env`: +```diff ++ OTS_FQDN= +- OTS_FQDN= + ++ OTS_MEDIAMTX_TOKEN= +``` + This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` depending on your platform. This repo is new and not quite complete. Full documentation will be available on https://docs.opentakserver.io when it's ready -for use. CloudTAK will also be incorporated into docker-compose.yml. \ No newline at end of file +for use. CloudTAK will also be incorporated into docker-compose.yml. diff --git a/add-marker.sh b/add-marker.sh new file mode 100755 index 0000000..2de1c81 --- /dev/null +++ b/add-marker.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Create a marker on the OpenTAKServer map via POST /api/markers. +# The marker is broadcast to connected TAK clients and stored in the DB. +set -euo pipefail + +OTS_HOST="${OTS_HOST:-192.168.1.29}" +OTS_USER="${OTS_USER:-administrator}" +OTS_PASS="${OTS_PASS:-password}" + +usage() { + cat <&2; usage ;; + \?) echo "Error: unknown option -$OPTARG" >&2; usage ;; + esac +done + +# Log in and grab a bearer token. Flask-Security returns it under +# response.user.authentication_token when include_auth_token is set. +login() { + local body + body=$(curl -sk --fail-with-body \ + -X POST "https://${OTS_HOST}/api/login?include_auth_token" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${OTS_USER}\",\"password\":\"${OTS_PASS}\"}") || { + echo "Error: login failed for ${OTS_USER}@${OTS_HOST}" >&2 + exit 1 + } + python3 -c " +import json, sys +try: + print(json.load(sys.stdin)['response']['user']['authentication_token']) +except (KeyError, ValueError): + sys.exit('Error: no auth token in login response') +" <<<"$body" +} + +token=$(login) + +if [[ "$action" == "list" ]]; then + curl -sk "https://${OTS_HOST}/api/markers" \ + -H "Authentication-Token: ${token}" | python3 -m json.tool + exit 0 +fi + +if [[ "$action" == "delete" ]]; then + curl -sk -X DELETE "https://${OTS_HOST}/api/markers?uid=${del_uid}" \ + -H "Authentication-Token: ${token}" + echo + exit 0 +fi + +[[ -n "$name" && -n "$lat" && -n "$lon" ]] || { + echo "Error: -n, -a and -o are required" >&2 + usage +} + +# The API rejects anything that is not UUID4, so generate rather than improvise. +if [[ -z "$muid" ]]; then + muid=$(python3 -c "import uuid; print(uuid.uuid4())") +fi + +payload=$(python3 -c " +import json, sys +print(json.dumps({ + 'uid': sys.argv[1], + 'name': sys.argv[2], + 'latitude': float(sys.argv[3]), + 'longitude': float(sys.argv[4]), + 'type': sys.argv[5], +})) +" "$muid" "$name" "$lat" "$lon" "$cot_type") + +response=$(curl -sk -X POST "https://${OTS_HOST}/api/markers" \ + -H "Content-Type: application/json" \ + -H "Authentication-Token: ${token}" \ + -d "$payload") + +if [[ "$response" == *'"success":true'* ]]; then + echo "Created '${name}' (${cot_type}) at ${lat},${lon}" + echo " uid: ${muid}" +else + echo "Failed: ${response}" >&2 + exit 1 +fi diff --git a/docker-compose.yml b/docker-compose.yml index c55351e..ccaf17e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: build: context: . dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: opentakserver hostname: opentakserver restart: unless-stopped @@ -26,12 +27,18 @@ services: env_file: ots_config.env ots_cot_parser: - image: ghcr.io/brian7704/ots_cot_parser:master + # The ghcr ots_* images are stale 1.5.14 builds regardless of their tag, and + # ship the old client_controller.py that binds a 'cot' exchange the 1.7.x core + # no longer creates. Use the locally-built image so handlers match the core. + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_cot_parser hostname: ots-cot_parser restart: unless-stopped tty: true - command: python3 /app/venv/bin/cot_parser + entrypoint: ["python3", "/app/venv/bin/cot_parser"] healthcheck: disable: true volumes: @@ -42,12 +49,15 @@ services: env_file: ots_config.env ots_eud_handler: - image: ghcr.io/brian7704/ots_eud_handler:master + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_eud_handler hostname: ots_eud_handler restart: unless-stopped tty: true - command: python3 /app/venv/bin/eud_handler + entrypoint: ["python3", "/app/venv/bin/eud_handler"] ports: - "0.0.0.0:8088:8088" # TCP CoT streaming port healthcheck: @@ -57,12 +67,15 @@ services: env_file: ots_config.env ots_eud_handler_ssl: - image: ghcr.io/brian7704/ots_eud_handler_ssl:master + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_eud_handler_ssl hostname: ots_eud_handler_ssl restart: unless-stopped tty: true - command: python3 /app/venv/bin/eud_handler --ssl + entrypoint: ["python3", "/app/venv/bin/eud_handler", "--ssl"] ports: - "0.0.0.0:8089:8089" # SSL CoT streaming port healthcheck: @@ -76,6 +89,7 @@ services: ots-webui: image: ghcr.io/brian7704/opentakserver-ui:master + platform: linux/amd64 # published for amd64 only, runs emulated container_name: ots-webui hostname: opentakserver-webui @@ -146,7 +160,9 @@ services: - "./ots/mediamtx/mediamtx.yml:/mediamtx.yml" ots-db: - image: postgis/postgis:18-3.6 + # postgis/postgis publishes amd64 only; imresamu/postgis is the PostGIS + # project's multi-arch companion build of the same recipe. + image: imresamu/postgis:18-3.6 container_name: ots-db hostname: ots-db restart: unless-stopped diff --git a/ots/.gitignore b/ots/.gitignore index 30eb3da..369e50c 100644 --- a/ots/.gitignore +++ b/ots/.gitignore @@ -1,7 +1,7 @@ -# Ignore everything in this directory -ca -# But do not ignore this .gitignore file -!ca +# "ca" was listed here and then immediately re-included with "!ca", which left +# the CA private key unignored. Certificate material is handled by the root +# .gitignore now; a negation here would override it, since deeper .gitignore +# files win over shallower ones. !mediamtx !.gitignore !configs \ No newline at end of file diff --git a/ots_config.env b/ots_config.env.template similarity index 85% rename from ots_config.env rename to ots_config.env.template index e89aa31..897136a 100644 --- a/ots_config.env +++ b/ots_config.env.template @@ -1,9 +1,10 @@ SQLALCHEMY_DATABASE_URI=postgresql+psycopg://ots:password@ots-db/ots -OTS_FQDN=_ +OTS_FQDN= OTS_RABBITMQ_SERVER_ADDRESS=rabbitmq OTS_LISTENER_ADDRESS=0.0.0.0 OTS_MEDIAMTX_API_ADDRESS=http://mediamtx:9997 POSTGRES_PASSWORD=password POSTGRES_USER=ots POSTGRES_DB=ots -PGUSER=ots \ No newline at end of file +PGUSER=ots +OTS_MEDIAMTX_TOKEN= diff --git a/patches/fix_eud_handler.py b/patches/fix_eud_handler.py new file mode 100644 index 0000000..22160d8 --- /dev/null +++ b/patches/fix_eud_handler.py @@ -0,0 +1,74 @@ +"""Patch upstream OpenTAKServer 1.7.11 bugs in the installed package. + +Both fixes are idempotent and abort the build if their anchor is missing, so a +version bump can never silently drop them. Upstream sources ship with CRLF line +endings, so anchors are matched with \r?\n tolerance. + +Run as: python3 fix_eud_handler.py [site-packages/opentakserver] +""" + +import re +import sys +from pathlib import Path + +PKG = Path( + sys.argv[1] + if len(sys.argv) > 1 + else "/app/venv/lib/python3.13/site-packages/opentakserver" +) + + +def patch(path: Path, pattern: str, replacement: str, applied: str, label: str) -> None: + # read_text() applies universal newlines, so the upstream CRLF is normalised + # to LF here and written back as LF. Harmless for Python sources. + text = path.read_text() + if re.search(applied, text, flags=re.M): + print(f"[skip] {label}: already applied") + return + new_text, count = re.subn(pattern, replacement, text, count=1, flags=re.M) + if count != 1: + sys.exit(f"[FAIL] {label}: anchor not found in {path}") + path.write_text(new_text) + print(f"[ok] {label}") + + +# 1. eud_handler.py imports the EudHandler *module* where the class is required, +# so it gets passed as RequestHandlerClass and every plain-TCP (8088) +# connection dies with "TypeError: 'module' object is not callable". +# EudHandlerSSL.py already uses the correct form, which is exactly why only +# the TCP listener was affected. +patch( + PKG / "eud_handler" / "eud_handler.py", + r"^from opentakserver\.eud_handler import EudHandler[ \t]*$", + "from opentakserver.eud_handler.EudHandler import EudHandler", + r"^from opentakserver\.eud_handler\.EudHandler import EudHandler[ \t]*$", + "eud_handler.py: import the class, not the module", +) + +# 2. setup() opens the RabbitMQ channel asynchronously via pika.SelectConnection, +# but handle_cot() registers the client only on the FIRST CoT +# ("if event and not self.uid"). parse_device_info() then guards its +# queue_bind/basic_consume block with "if self.rabbit_channel and ...", so a +# client whose self-report arrives before the channel opens is silently +# skipped and stays unbound for the entire connection -- no crash, no retry. +# ATAK sends its self-report immediately on connect, so it loses this race +# routinely. Block in setup() (which runs before handle()) until the channel +# is ready, capped at 5s so a dead broker can't wedge the connection. +patch( + PKG / "eud_handler" / "EudHandler.py", + r"^([ \t]*)self\.iothread\.start\(\)([ \t]*)$", + ( + r"\1self.iothread.start()\2\n" + r"\1# Wait for the async channel before handle() reads the first CoT.\n" + r"\1# Registration runs once, so losing the race leaves us unbound.\n" + r"\1import time as _ots_time\n" + r"\1for _ in range(100):\n" + r"\1 if self.rabbit_channel and self.rabbit_channel.is_open:\n" + r"\1 break\n" + r"\1 _ots_time.sleep(0.05)" + ), + r"_ots_time", + "EudHandler.py: await RabbitMQ channel in setup()", +) + +print("patches complete") From 8823a75c8af4ff70fbdb27464081a0ddf72d737f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Mon, 27 Jul 2026 13:58:14 +0200 Subject: [PATCH 02/10] feat: add add-marker.sh docs --- add-marker.sh | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/add-marker.sh b/add-marker.sh index 2de1c81..489fdf0 100755 --- a/add-marker.sh +++ b/add-marker.sh @@ -1,6 +1,54 @@ #!/usr/bin/env bash +# # Create a marker on the OpenTAKServer map via POST /api/markers. -# The marker is broadcast to connected TAK clients and stored in the DB. +# The marker is stored in the DB and broadcast to connected TAK clients. +# +# ------------------------------------------------------------------------------ +# EXAMPLES +# ------------------------------------------------------------------------------ +# +# Create an unknown-ground marker (type defaults to a-u-G): +# ./add-marker.sh -n "Waypoint 1" -a 52.2297 -o 21.0122 +# +# Create a friendly marker (blue icon on the client): +# ./add-marker.sh -n "OP North" -a 52.2297 -o 21.0122 -t a-f-G-U-C +# +# Create a hostile marker (red icon): +# ./add-marker.sh -n "Contact" -a 52.3100 -o 21.1100 -t a-h-G +# +# List every marker currently on the map (paginated, 10 per page): +# ./add-marker.sh -l +# +# Move or rename an existing marker by reusing its UID. This updates the +# marker in place rather than creating a duplicate: +# ./add-marker.sh -n "OP North" -a 52.4000 -o 21.0122 -t a-f-G-U-C \ +# -u 10e99852-26f2-454f-9acb-86da78f46cf9 +# +# Delete a marker: +# ./add-marker.sh -d 10e99852-26f2-454f-9acb-86da78f46cf9 +# +# Target a different server, or use different credentials, without editing +# this file: +# OTS_HOST=10.0.0.5 ./add-marker.sh -n Rally -a 52.1 -o 21.0 +# OTS_USER=alice OTS_PASS=hunter2 ./add-marker.sh -n Rally -a 52.1 -o 21.0 +# +# Drop a row of markers from a loop: +# for i in 1 2 3; do +# ./add-marker.sh -n "Checkpoint $i" -a "52.2$i" -o 21.01 +# done +# +# Print the full option reference: +# ./add-marker.sh -h +# +# ------------------------------------------------------------------------------ +# NOTES +# ------------------------------------------------------------------------------ +# +# - UIDs must be UUID4; the script generates one unless you pass -u. +# - Markers go stale on clients 24h after creation (server-side default). +# - A client must be connected when the marker is created to receive it; +# replay-on-connect for pre-existing markers is untested. +# set -euo pipefail OTS_HOST="${OTS_HOST:-192.168.1.29}" From a0f668665c98d7b32f41a3977f0c8b6bd5eaf229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Mon, 27 Jul 2026 14:14:39 +0200 Subject: [PATCH 03/10] feat: update reamde --- README.md | 5 ++++- add-marker.sh => markers.sh | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) rename add-marker.sh => markers.sh (89%) diff --git a/README.md b/README.md index 1fac86e..fea524b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ cp ots_config.env.template ots_config.env ``` -`ots_config.env`: +In `ots_config.env`: + ```diff + OTS_FQDN= - OTS_FQDN= @@ -12,6 +13,8 @@ cp ots_config.env.template ots_config.env + OTS_MEDIAMTX_TOKEN= ``` +To manage markers use `marker.sh` script. See docs in script comment. + This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` depending on your platform. diff --git a/add-marker.sh b/markers.sh similarity index 89% rename from add-marker.sh rename to markers.sh index 489fdf0..01d7ee0 100755 --- a/add-marker.sh +++ b/markers.sh @@ -8,37 +8,37 @@ # ------------------------------------------------------------------------------ # # Create an unknown-ground marker (type defaults to a-u-G): -# ./add-marker.sh -n "Waypoint 1" -a 52.2297 -o 21.0122 +# ./markers.sh -n "Waypoint 1" -a 52.2297 -o 21.0122 # # Create a friendly marker (blue icon on the client): -# ./add-marker.sh -n "OP North" -a 52.2297 -o 21.0122 -t a-f-G-U-C +# ./markers.sh -n "OP North" -a 52.2297 -o 21.0122 -t a-f-G-U-C # # Create a hostile marker (red icon): -# ./add-marker.sh -n "Contact" -a 52.3100 -o 21.1100 -t a-h-G +# ./markers.sh -n "Contact" -a 52.3100 -o 21.1100 -t a-h-G # # List every marker currently on the map (paginated, 10 per page): -# ./add-marker.sh -l +# ./markers.sh -l # # Move or rename an existing marker by reusing its UID. This updates the # marker in place rather than creating a duplicate: -# ./add-marker.sh -n "OP North" -a 52.4000 -o 21.0122 -t a-f-G-U-C \ +# ./markers.sh -n "OP North" -a 52.4000 -o 21.0122 -t a-f-G-U-C \ # -u 10e99852-26f2-454f-9acb-86da78f46cf9 # # Delete a marker: -# ./add-marker.sh -d 10e99852-26f2-454f-9acb-86da78f46cf9 +# ./markers.sh -d 10e99852-26f2-454f-9acb-86da78f46cf9 # # Target a different server, or use different credentials, without editing # this file: -# OTS_HOST=10.0.0.5 ./add-marker.sh -n Rally -a 52.1 -o 21.0 -# OTS_USER=alice OTS_PASS=hunter2 ./add-marker.sh -n Rally -a 52.1 -o 21.0 +# OTS_HOST=10.0.0.5 ./markers.sh -n Rally -a 52.1 -o 21.0 +# OTS_USER=alice OTS_PASS=hunter2 ./markers.sh -n Rally -a 52.1 -o 21.0 # # Drop a row of markers from a loop: # for i in 1 2 3; do -# ./add-marker.sh -n "Checkpoint $i" -a "52.2$i" -o 21.01 +# ./markers.sh -n "Checkpoint $i" -a "52.2$i" -o 21.01 # done # # Print the full option reference: -# ./add-marker.sh -h +# ./markers.sh -h # # ------------------------------------------------------------------------------ # NOTES From 72f6d4d49c4451e4e97272e358cfcba158dc34d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Wed, 29 Jul 2026 15:15:41 +0200 Subject: [PATCH 04/10] feat: configurable ports --- .gitignore | 3 +++ README.md | 39 ++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 23 ++++++++++++++++------- ots_config.env.template | 19 +++++++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index bae36c0..301f34b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ .DS_Store ots_config.env + +# Symlink to ots_config.env so compose can interpolate ${OTS_DB_PORT}. +.env diff --git a/README.md b/README.md index fea524b..7224e06 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,15 @@ ``` cp ots_config.env.template ots_config.env +ln -s ots_config.env .env ``` +The `.env` symlink is required, not optional. `env_file:` in `docker-compose.yml` passes +`ots_config.env` *into the containers*, but Compose resolves `${...}` placeholders in the +compose file itself only from the shell environment or a `.env` file. Without the symlink +the port variables below are silently ignored and those host ports fall back to their +defaults. + In `ots_config.env`: ```diff @@ -13,7 +20,37 @@ In `ots_config.env`: + OTS_MEDIAMTX_TOKEN= ``` -To manage markers use `marker.sh` script. See docs in script comment. +## Host ports + +Every published host port is configurable, so this stack can coexist with other local +projects. In each case only the **host** side moves; the container side is fixed. + +| Variable | Default | Published for | +| --- | --- | --- | +| `OTS_DB_PORT` | `5433` | Postgres | +| `OTS_WEB_HTTP_PORT` | `80` | HTTP Web UI | +| `OTS_WEB_HTTPS_PORT` | `443` | HTTPS Web UI | +| `OTS_API_HTTP_PORT` | `8080` | HTTP API | +| `OTS_API_HTTPS_PORT` | `8443` | HTTPS API | +| `OTS_CERT_ENROLLMENT_PORT` | `8446` | Certificate enrollment | +| `OTS_MQTT_PORT` | `8883` | MQTT / Meshtastic | + +`OTS_DB_PORT`'s container side stays 5432 because OTS reaches its database via +`SQLALCHEMY_DATABASE_URI=...@ots-db/ots`, which carries no port and resolves over the +compose network. + +The `nginx-proxy` container sides are fixed because each is a hardcoded `listen` directive +in `ots/configs/nginx/templates` — remap the host side and nginx still listens where it +always did, but change the container side and nothing is listening there at all. + +Two caveats when moving the web ports: + +- Clients must then include the port explicitly. `markers.sh` and anything else calling + `https:///api/...` assumes `443`. +- `OTS_FQDN` carries no port, so certificate enrollment and any TAK client profile deriving + URLs from it still expect the defaults. + +To manage markers use the `markers.sh` script. See docs in script comment. This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` diff --git a/docker-compose.yml b/docker-compose.yml index ccaf17e..582953f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,12 +101,17 @@ services: hostname: nginx-proxy restart: unless-stopped ports: - - "0.0.0.0:80:80" # HTTP Web UI - - "0.0.0.0:443:443" # HTTPS Web UI - - "0.0.0.0:8080:8080" # HTTP API requests to OpenTAKServer port 8081 - - "0.0.0.0:8443:8443" # HTTPS API requests to OpenTAKServer port 8081 - - "0.0.0.0:8446:8446" # Proxy for certificate enrollment to OpenTAKServer port 8081 - - "0.0.0.0:8883:8883" # Proxy for MQTT / Meshtastic to Rabbitmq port 1883 + # Host sides are configurable to avoid colliding with other local projects; + # set the OTS_*_PORT vars in ots_config.env. Container sides must stay as + # they are: each is hardcoded as a `listen` directive in + # ots/configs/nginx/templates, so nginx would not be listening on a + # changed port. + - "0.0.0.0:${OTS_WEB_HTTP_PORT:-80}:80" # HTTP Web UI + - "0.0.0.0:${OTS_WEB_HTTPS_PORT:-443}:443" # HTTPS Web UI + - "0.0.0.0:${OTS_API_HTTP_PORT:-8080}:8080" # HTTP API requests to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_API_HTTPS_PORT:-8443}:8443" # HTTPS API requests to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_CERT_ENROLLMENT_PORT:-8446}:8446" # Proxy for certificate enrollment to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_MQTT_PORT:-8883}:8883" # Proxy for MQTT / Meshtastic to Rabbitmq port 1883 volumes: - "./ots/ca:/app/ots/ca:ro" - "./ots/configs/nginx/templates:/etc/nginx/templates:ro" @@ -175,4 +180,8 @@ services: timeout: 5s retries: 10 ports: - - "5432:5432" \ No newline at end of file + # Host side is configurable to avoid colliding with other local projects' + # Postgres; set OTS_DB_PORT in ots_config.env. Container side must stay + # 5432: OTS connects with SQLALCHEMY_DATABASE_URI=...@ots-db/ots, which + # has no explicit port and resolves over the compose network. + - "${OTS_DB_PORT:-5433}:5432" \ No newline at end of file diff --git a/ots_config.env.template b/ots_config.env.template index 897136a..8913b58 100644 --- a/ots_config.env.template +++ b/ots_config.env.template @@ -8,3 +8,22 @@ POSTGRES_USER=ots POSTGRES_DB=ots PGUSER=ots OTS_MEDIAMTX_TOKEN= + +# Host port for the Postgres container. The container side stays 5432. +OTS_DB_PORT=5433 + +# Host ports published by nginx-proxy. Container sides are fixed by the `listen` +# directives in ots/configs/nginx/templates, so only these host mappings are +# configurable. Changing OTS_WEB_HTTPS_PORT away from 443 means clients must +# include the port explicitly (markers.sh and any https:///api/... caller +# assume the default). +OTS_WEB_HTTP_PORT=80 +OTS_WEB_HTTPS_PORT=443 + +# Host port for the OTS HTTP API. Moved off default 8080 so the drone-detection-api +# `api` service can bind it. Container side stays 8080. +OTS_API_HTTP_PORT=18080 + +OTS_API_HTTPS_PORT=8443 +OTS_CERT_ENROLLMENT_PORT=8446 +OTS_MQTT_PORT=8883 From f22fd4bd960eeb9fc33ea0051bce3dda03b67a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Thu, 30 Jul 2026 10:55:49 +0200 Subject: [PATCH 05/10] feat: update README.md --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7224e06..80daf15 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,31 @@ # OpenTAKServer-Docker -``` +This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. +To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` +depending on your platform. + +This repo is new and not quite complete. Full documentation will be available on https://docs.opentakserver.io when it's ready +for use. CloudTAK will also be incorporated into docker-compose.yml. + +```sh cp ots_config.env.template ots_config.env -ln -s ots_config.env .env ``` -The `.env` symlink is required, not optional. `env_file:` in `docker-compose.yml` passes +In `ots_config.env`: + +```diff ++ OTS_FQDN= ++ OTS_MEDIAMTX_TOKEN= +``` + +The `.env` symlink is required. `env_file:` in `docker-compose.yml` passes `ots_config.env` *into the containers*, but Compose resolves `${...}` placeholders in the compose file itself only from the shell environment or a `.env` file. Without the symlink the port variables below are silently ignored and those host ports fall back to their defaults. -In `ots_config.env`: - -```diff -+ OTS_FQDN= -- OTS_FQDN= - -+ OTS_MEDIAMTX_TOKEN= +```sh +ln -s ots_config.env .env ``` ## Host ports @@ -51,10 +59,3 @@ Two caveats when moving the web ports: URLs from it still expect the defaults. To manage markers use the `markers.sh` script. See docs in script comment. - -This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. -To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` -depending on your platform. - -This repo is new and not quite complete. Full documentation will be available on https://docs.opentakserver.io when it's ready -for use. CloudTAK will also be incorporated into docker-compose.yml. From aa77799816f09512199313fc7209bff97c716218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Thu, 30 Jul 2026 10:58:45 +0200 Subject: [PATCH 06/10] feat: update .env.template --- ots_config.env.template | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ots_config.env.template b/ots_config.env.template index 8913b58..4b8dc61 100644 --- a/ots_config.env.template +++ b/ots_config.env.template @@ -1,5 +1,10 @@ -SQLALCHEMY_DATABASE_URI=postgresql+psycopg://ots:password@ots-db/ots +# Required, your MacOS local network IP: OTS_FQDN= + +# Required, random, alphanumeric string: +OTS_MEDIAMTX_TOKEN= + +SQLALCHEMY_DATABASE_URI=postgresql+psycopg://ots:password@ots-db/ots OTS_RABBITMQ_SERVER_ADDRESS=rabbitmq OTS_LISTENER_ADDRESS=0.0.0.0 OTS_MEDIAMTX_API_ADDRESS=http://mediamtx:9997 @@ -7,7 +12,6 @@ POSTGRES_PASSWORD=password POSTGRES_USER=ots POSTGRES_DB=ots PGUSER=ots -OTS_MEDIAMTX_TOKEN= # Host port for the Postgres container. The container side stays 5432. OTS_DB_PORT=5433 From 780fdd7ade0de8302c6bfdb870416c4eae70c70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Mon, 3 Aug 2026 13:12:56 +0200 Subject: [PATCH 07/10] feat: cot script + port config extend --- README.md | 5 +- docker-compose.yml | 2 +- markers.sh | 1 + ots_config.env.template | 4 ++ send_cot.py | 110 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 3 deletions(-) create mode 100755 send_cot.py diff --git a/README.md b/README.md index 80daf15..2c46804 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ projects. In each case only the **host** side moves; the container side is fixed | `OTS_DB_PORT` | `5433` | Postgres | | `OTS_WEB_HTTP_PORT` | `80` | HTTP Web UI | | `OTS_WEB_HTTPS_PORT` | `443` | HTTPS Web UI | -| `OTS_API_HTTP_PORT` | `8080` | HTTP API | -| `OTS_API_HTTPS_PORT` | `8443` | HTTPS API | +| `OTS_API_HTTP_PORT` | `18080` | HTTP API (nginx-proxy) | +| `OTS_API_HTTPS_PORT` | `8443` | HTTPS API (nginx-proxy) | +| `OTS_API_INTERNAL_PORT` | `8081` | Internal Flask API server | | `OTS_CERT_ENROLLMENT_PORT` | `8446` | Certificate enrollment | | `OTS_MQTT_PORT` | `8883` | MQTT / Meshtastic | diff --git a/docker-compose.yml b/docker-compose.yml index 582953f..867c133 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: restart: unless-stopped tty: true ports: - - "8081:8081" # OTS listens on this port on the loopback interface for HTTP(S) requests + - "${OTS_API_INTERNAL_PORT:-8081}:8081" # OTS listens on this port for HTTP(S) requests volumes: - "./ots:/app/ots:rw" #- "./.opentakserver_venv:/app/venv/:rw" diff --git a/markers.sh b/markers.sh index 01d7ee0..b65b360 100755 --- a/markers.sh +++ b/markers.sh @@ -157,6 +157,7 @@ print(json.dumps({ 'latitude': float(sys.argv[3]), 'longitude': float(sys.argv[4]), 'type': sys.argv[5], + 'fov': 90 })) " "$muid" "$name" "$lat" "$lon" "$cot_type") diff --git a/ots_config.env.template b/ots_config.env.template index 4b8dc61..83da14a 100644 --- a/ots_config.env.template +++ b/ots_config.env.template @@ -31,3 +31,7 @@ OTS_API_HTTP_PORT=18080 OTS_API_HTTPS_PORT=8443 OTS_CERT_ENROLLMENT_PORT=8446 OTS_MQTT_PORT=8883 + +# Host port for the OTS internal API server (Flask/gunicorn). Used by clients connecting +# to the OpenTAKServer for REST API calls and WebSocket connections. Container side stays 8081. +OTS_API_INTERNAL_PORT=8081 diff --git a/send_cot.py b/send_cot.py new file mode 100755 index 0000000..512a6c9 --- /dev/null +++ b/send_cot.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Send XML CoT events to OpenTAKServer via TCP port 8088. + +This script properly handles the OpenTAKServer protocol: +1. Sends initial device identification CoT +2. Sends actual data CoT events +3. Publishes via the cot_parser to be stored in the database + +Usage: + ./send_cot.py --callsign "Test Device" --lat 52.2297 --lon 21.0122 +""" + +import uuid +import socket +import time +from datetime import datetime, timedelta +from xml.etree.ElementTree import Element, SubElement, tostring + +def create_device_cot(device_uid, callsign): + """Create device identification CoT (first event after connecting).""" + now = datetime.utcnow() + stale = now + timedelta(hours=24) + now_str = now.isoformat() + 'Z' + stale_str = stale.isoformat() + 'Z' + + xml = f""" + + + + +""" + + return xml + + +def create_marker_cot(uid, callsign, latitude, longitude, cot_type='a-f-G', seconds=2): + """Create a marker CoT event.""" + now = datetime.utcnow() + stale = now + timedelta(seconds=seconds) + now_str = now.isoformat() + 'Z' + stale_str = stale.isoformat() + 'Z' + + xml = f""" + + + + +""" + + return xml + + +def send_cot_via_tcp(host, port, cot_xml): + """Send CoT XML via TCP.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.sendall(cot_xml.encode('utf-8')) + time.sleep(0.1) + sock.close() + return True + except Exception as e: + print(f"✗ Error sending CoT: {e}") + return False + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='Send CoT events to OpenTAKServer') + parser.add_argument('--host', default='localhost', help='OpenTAKServer host') + parser.add_argument('--port', type=int, default=8088, help='OpenTAKServer TCP CoT port') + parser.add_argument('--callsign', required=True, help='Callsign for the device and marker') + parser.add_argument('--lat', type=float, required=True, help='Latitude') + parser.add_argument('--lon', type=float, required=True, help='Longitude') + parser.add_argument('--type', default='a-f-G', help='CoT type (default: a-f-G friendly ground)') + parser.add_argument('--ttl', type=int, default=10, help='Time-to-live in hours') + parser.add_argument('--device-uid', help='Device UID (generated if not provided)') + + args = parser.parse_args() + + # Generate a device UID (should be consistent for this device) + device_uid = args.device_uid or str(uuid.uuid4()) + + print(f"Connecting to {args.host}:{args.port}...") + + # Step 1: Send device identification + print(f"1. Registering device '{args.callsign}'...") + # device_cot = create_device_cot(device_uid, args.callsign) + # if send_cot_via_tcp(args.host, args.port, device_cot): + # print(" ✓ Device registered") + time.sleep(1.0) # Wait for device to be registered in DB + + # Step 2: Send marker CoT (use device UID so it's associated with the registered device) + print(f"2. Sending marker '{args.callsign}' at {args.lat}, {args.lon}...") + marker_cot = create_marker_cot(device_uid, args.callsign, args.lat, args.lon, args.type, args.ttl) + if send_cot_via_tcp(args.host, args.port, marker_cot): + print(" ✓ Marker CoT sent") + + print(f"\n✓ Complete!") + print(f"Device UID: {device_uid}") + print(f"Type: {args.type}") + print(f"Expires in: {args.ttl} hours") + print(f"\nTo send more markers from this device:") + print(f" python3 send_cot.py --device-uid {device_uid} --callsign 'New Marker' --lat 52.5 --lon 21.5") + + +if __name__ == '__main__': + main() From aab08eef7dac3b950e61f292865f83c88e19c06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Wed, 5 Aug 2026 11:50:53 +0200 Subject: [PATCH 08/10] feat: mv scrips to bin dir/ --- markers.sh => bin/markers.sh | 0 bin/remove_all_markers.sh | 55 ++++++++++++++++++++++++++++++++++ send_cot.py => bin/send_cot.py | 0 3 files changed, 55 insertions(+) rename markers.sh => bin/markers.sh (100%) create mode 100755 bin/remove_all_markers.sh rename send_cot.py => bin/send_cot.py (100%) diff --git a/markers.sh b/bin/markers.sh similarity index 100% rename from markers.sh rename to bin/markers.sh diff --git a/bin/remove_all_markers.sh b/bin/remove_all_markers.sh new file mode 100755 index 0000000..8eb636c --- /dev/null +++ b/bin/remove_all_markers.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Remove all markers from the OpenTAKServer map +# + +set -eu + +OTS_HOST="${OTS_HOST:-localhost}" +OTS_USER="${OTS_USER:-administrator}" +OTS_PASS="${OTS_PASS:-password}" + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# List all markers and extract UIDs, then delete each one +echo "Fetching markers from ${OTS_HOST}..." + +marker_uids=$(OTS_HOST="${OTS_HOST}" OTS_USER="${OTS_USER}" OTS_PASS="${OTS_PASS}" "$SCRIPT_DIR/markers.sh" -l 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + # Try both possible response formats + markers = data.get('response', {}).get('markers', []) or data.get('results', []) + for m in markers: + print(m.get('uid')) +except Exception as e: + print(f'Error: {e}', file=sys.stderr) + sys.exit(1) +") + +if [ -z "$marker_uids" ]; then + echo "No markers found on the map." + exit 0 +fi + +count=$(echo "$marker_uids" | wc -l) +echo "Found $count item(s). Attempting deletion..." + +deleted=0 +failed=0 + +echo "$marker_uids" | while IFS= read -r uid; do + if [ -n "$uid" ]; then + result=$(OTS_HOST="${OTS_HOST}" OTS_USER="${OTS_USER}" OTS_PASS="${OTS_PASS}" "$SCRIPT_DIR/markers.sh" -d "$uid" 2>&1) + if echo "$result" | grep -q '"success":true'; then + echo "✓ Deleted: $uid" + deleted=$((deleted + 1)) + else + failed=$((failed + 1)) + fi + fi +done + +echo "" +echo "Done! Markers have been deleted." diff --git a/send_cot.py b/bin/send_cot.py similarity index 100% rename from send_cot.py rename to bin/send_cot.py From 3ea8fdcef61a429f9ec4f8f2b1470bed87263b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Thu, 6 Aug 2026 08:59:15 +0200 Subject: [PATCH 09/10] feat: remove hardcoded ips --- bin/markers.sh | 2 +- bin/remove_all_markers.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/markers.sh b/bin/markers.sh index b65b360..79652eb 100755 --- a/bin/markers.sh +++ b/bin/markers.sh @@ -51,7 +51,7 @@ # set -euo pipefail -OTS_HOST="${OTS_HOST:-192.168.1.29}" +OTS_HOST="${OTS_HOST:-$(ipconfig getifaddr en0)}" OTS_USER="${OTS_USER:-administrator}" OTS_PASS="${OTS_PASS:-password}" diff --git a/bin/remove_all_markers.sh b/bin/remove_all_markers.sh index 8eb636c..7babed6 100755 --- a/bin/remove_all_markers.sh +++ b/bin/remove_all_markers.sh @@ -5,7 +5,7 @@ set -eu -OTS_HOST="${OTS_HOST:-localhost}" +OTS_HOST="${OTS_HOST:-$(ipconfig getifaddr en0)}" OTS_USER="${OTS_USER:-administrator}" OTS_PASS="${OTS_PASS:-password}" From 2258d9502ef7b1edd58b966afaae9d3fb3d89ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kabaci=C5=84ski?= Date: Wed, 12 Aug 2026 13:04:51 +0200 Subject: [PATCH 10/10] feat: clean up README.md --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2c46804..93bbc0a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,25 @@ # OpenTAKServer-Docker -This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. -To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` -depending on your platform. +This repo contains the `docker-compose.yml` and config files needed to run OpenTAKServer in a +docker container. -This repo is new and not quite complete. Full documentation will be available on https://docs.opentakserver.io when it's ready -for use. CloudTAK will also be incorporated into docker-compose.yml. +This repo is new and not quite complete. Full documentation will be available at +https://docs.opentakserver.io when it's ready for use. CloudTAK will also be incorporated into +`docker-compose.yml`. + +## Table of contents + +- [Getting started](#getting-started) +- [Configuration](#configuration) + - [Host ports](#host-ports) +- [Scripts](#scripts) +- [Usage with Colata API](#usage-with-colata-api) + +## Getting started + +To use it, install docker on your system, clone this repo. + +Create config file: ```sh cp ots_config.env.template ots_config.env @@ -18,6 +32,12 @@ In `ots_config.env`: + OTS_MEDIAMTX_TOKEN= ``` +For `OTS_FQDN` set server's host IP: + +``` +ipconfig getifaddr en0 +``` + The `.env` symlink is required. `env_file:` in `docker-compose.yml` passes `ots_config.env` *into the containers*, but Compose resolves `${...}` placeholders in the compose file itself only from the shell environment or a `.env` file. Without the symlink @@ -28,7 +48,13 @@ defaults. ln -s ots_config.env .env ``` -## Host ports +Run `docker compose up -d` (or `sudo docker compose up -d`, depending on your platform). + +A successful run opens the TAK admin panel at `http://localhost:80`. + +## Configuration + +### Host ports Every published host port is configurable, so this stack can coexist with other local projects. In each case only the **host** side moves; the container side is fixed. @@ -59,4 +85,17 @@ Two caveats when moving the web ports: - `OTS_FQDN` carries no port, so certificate enrollment and any TAK client profile deriving URLs from it still expect the defaults. -To manage markers use the `markers.sh` script. See docs in script comment. +## Scripts + +To manage markers manually use the `bin/markers.sh` or `bin/remove_all_markers.sh` script. See docs in +script comments. + +## Usage with Colata API + +This setup was created to work specially with [Colata API](https://github.com/codequest-eu/drone-detection-api). + +1. Run TAK server before the API to receive initial detector events. +2. Make sure the TAK client (like Android's `ATAK`) is properly connected to the TAK server via + TCP (`8080` port) using the IP provided in `ots_config.env`. ⚠️ The TAK server and client have + to be on the same network for this to work. +3. Run the API with the `atak-worker` service.