Add NAS Compose deployment with outbound-only named tunnel

This commit is contained in:
liu weikai committed 2026-09-28 19:42:26 +08:00
1 parent c3036a0ffe
commit 7b27e461d9
7 files changed
+222

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Copy to .env. Keep the real token local; never commit it or paste it into chat.
TUNNEL_TOKEN=
DIRECTORY_NAME=Trail Mate public directory
SITE_ORIGIN=https://vicliu624.github.io
+2
View File
@@ -0,0 +1,2 @@
.env
*.token
+91
View File
@@ -0,0 +1,91 @@
# NAS deployment without inbound ports
This Compose project runs a persistent geocaching directory, a raw Reticulum WebSocket bridge, and a named Cloudflare Tunnel. It publishes no NAS ports. There is no host networking, Docker socket mount, certificate server or static cache export.
The directory owns a private container network namespace. Bridge and tunnel share it, so `127.0.0.1:44242` and `127.0.0.1:8787` are internal to these containers, not the NAS host. Cloudflare terminates public TLS. Browser queries and responses pass through the tunnel in real time.
## Prerequisites
- Linux Docker Engine with Docker Compose v2.17 or later (for dependency restart propagation). Use the complete project; deploying the services separately loses their shared-network relationship.
- An amd64 or arm64 NAS able to pull the Python and Cloudflare images and install the pinned Python dependencies.
- A Cloudflare account and a domain that can be configured for a published application hostname. A DDNS hostname you cannot manage in Cloudflare is not automatically sufficient.
- Outbound DNS, HTTPS for image/dependency installation, TCP 7844 to Cloudflare, and TCP 4242 to the configured Reticulum peers. No inbound 80, 443 or other router forwarding is required.
The tunnel uses HTTP/2 over outbound TCP 7844, so UDP is not required. This is the connector transport; browser-facing WebSocket traffic still passes through it. Availability and latency must be checked from the NAS and actual visitors.
## 1. Get the files
For a new checkout on your NAS:
```sh
git clone --branch codex/geocaching --single-branch https://github.com/vicliu624/trail-mate.git
cd trail-mate/tools/geocaching/deploy
cp .env.example .env
```
For an existing checkout, update `codex/geocaching` and enter the same deployment directory. The build context is the repository root; copying only `compose.yaml` is insufficient. The Dockerfile-specific ignore file sends only the service files and requirement files to the builder, excluding local keys, databases and firmware output.
## 2. Create a named tunnel
In Cloudflare's tunnel dashboard, create a remotely managed Cloudflared tunnel. Obtain its tunnel token and put it in `.env` as `TUNNEL_TOKEN=...`. This is the tunnel token, not an API key. Keep it local.
Add a published application route:
| Setting | Value |
|---|---|
| Public hostname | A hostname you control, such as `geocaching.your-domain.example` |
| Path | Leave empty |
| Service type | **HTTP** |
| Service URL | **127.0.0.1:8787** |
Use HTTP for the internal service, not Cloudflare's arbitrary TCP application mode. The bridge already speaks WebSocket over HTTP. The resulting browser endpoint is `wss://geocaching.your-domain.example/`. Do not put an interactive Access login in front of this public endpoint: the map worker cannot complete that login flow.
Leave `SITE_ORIGIN=https://vicliu624.github.io` for the current website. It is the origin, not the full `/trail-mate/geocaching/` URL. `DIRECTORY_NAME` must fit in 40 UTF-8 bytes. The existing `reticulum/config` provides three editable upstream TCP entries and one loopback-only listener.
## 3. Build and start
```sh
docker compose config --quiet
docker compose build directory
docker compose up -d --no-build
docker compose ps
docker compose logs --tail=60 directory bridge tunnel
```
The first build installs the service dependencies; it does not compile Trail Mate firmware. Both Python services use the same local image. Container logs rotate at 5 MB, retaining at most three files per service.
Expected milestones:
- Directory emits JSON with `event: "ready"`, `delivery`, and **`discovery`** addresses.
- Bridge emits `event: "bridge_ready"`.
- Cloudflared reports a registered tunnel connection, and the dashboard shows the connector online.
The directory health check establishes a local TCP connection. The bridge check performs a local WebSocket handshake with the allowed Origin. These checks establish local readiness only; they do not prove public directory discovery, author verification or successful spatial queries. Docker restarts exited services, but does not restart a running process merely because its health status changes.
Send the operator the public **WSS URL** and the **discovery** value from the directory's ready log. Do not send the tunnel token, private identity file or database. The operator then updates `site/geocaching/network.json` and, where needed, device discovery seeds. That initial configuration deployment is separate from publishing cache data. Later cache publications and updates require no website deployment.
## Persistence and upgrades
The named volumes `trail-mate-geocaching_directory-data` and `trail-mate-geocaching_reticulum-data` preserve the directory identity, database, LXMF state and Reticulum state. A new empty deployment creates a new directory identity and an empty catalogue. It does not automatically restore previously published records whose only source is offline.
The database, requests, snapshots and records are not shared with a browser build. To retain an existing directory identity and content, migrate its complete stopped service state deliberately before starting this deployment; do not overwrite a running volume.
Upgrade all three services together because bridge and tunnel share the directory container's network namespace:
```sh
docker compose down
git pull --ff-only
docker compose build directory
docker compose pull tunnel
docker compose up -d --no-build
```
`down` retains named volumes. **Do not add `--volumes` or `-v`** unless intentionally deleting all saved directory data and identity. Use NAS volume backup tooling while services are stopped for a consistent backup; identity material is private. The Python service user is UID/GID 10001. Replacing named volumes with NAS bind mounts requires granting that user write access.
## Verification and limits
Compose configuration is validated before release. A live Docker build/run and end-to-end public query still need validation on the NAS; the development machine's Linux Docker engine was unavailable when this deployment was prepared.
After startup, verify a live query, exact signed detail and GPX download. Then publish a new revision and query again without rebuilding or deploying the website. Restart the NAS containers and verify the directory identity and data survive. The bridge admits 32 concurrent clients; this is a connection limit, not a tested NAS capacity guarantee.
Reference: [Cloudflare setup](https://developers.cloudflare.com/tunnel/get-started/), [outbound firewall requirements](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/tunnel-with-firewall/), [tunnel run parameters](https://developers.cloudflare.com/tunnel/reference/run-parameters/).
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.12-slim-bookworm
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 HOME=/var/lib/trail-mate
WORKDIR /app
COPY tools/geocaching/interop-requirements.txt tools/geocaching/bridge-requirements.txt /app/
RUN pip install --no-cache-dir -r interop-requirements.txt -r bridge-requirements.txt \
&& groupadd --gid 10001 trailmate \
&& useradd --uid 10001 --gid 10001 --no-create-home --home-dir /var/lib/trail-mate trailmate \
&& mkdir -p /var/lib/trail-mate/directory /var/lib/trail-mate/reticulum \
&& chown -R 10001:10001 /var/lib/trail-mate
COPY tools/geocaching/directory_service.py tools/geocaching/directory_store.py tools/geocaching/directory_sync.py tools/geocaching/packet_bridge.py /app/
USER 10001:10001
CMD ["python", "directory_service.py", "--help"]
@@ -0,0 +1,9 @@
**
!tools/
!tools/geocaching/
!tools/geocaching/interop-requirements.txt
!tools/geocaching/bridge-requirements.txt
!tools/geocaching/directory_service.py
!tools/geocaching/directory_store.py
!tools/geocaching/directory_sync.py
!tools/geocaching/packet_bridge.py
+2
View File
@@ -1,5 +1,7 @@
# 多入口部署
NAS 无需开放入站端口的 Docker Compose 部署见 [COMPOSE.md](COMPOSE.md),配置文件为 [compose.yaml](compose.yaml)。
网页自动连接我们维护的 WSS 服务。服务器上的一个 Reticulum 实例同时连接多个
公共 TCP 入口,并承载持久寻宝目录。普通访客没有网络设置界面。
+101
View File
@@ -0,0 +1,101 @@
name: trail-mate-geocaching
x-runtime: &runtime
image: trail-mate-geocaching:local
build:
context: ../../..
dockerfile: tools/geocaching/deploy/Dockerfile
restart: unless-stopped
init: true
read_only: true
tmpfs:
- /tmp:size=32m,mode=1777
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
services:
directory:
<<: *runtime
command:
- python
- directory_service.py
- --state
- /var/lib/trail-mate/directory
- --rns-config
- /var/lib/trail-mate/reticulum
- --name
- ${DIRECTORY_NAME:-Trail Mate public directory}
volumes:
- directory-data:/var/lib/trail-mate/directory
- reticulum-data:/var/lib/trail-mate/reticulum
- ./reticulum/config:/var/lib/trail-mate/reticulum/config:ro
healthcheck:
test: [CMD, python, -c, "import socket; socket.create_connection(('127.0.0.1',44242),3).close()"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
stop_grace_period: 40s
bridge:
<<: *runtime
network_mode: service:directory
depends_on:
directory:
condition: service_healthy
restart: true
command:
- python
- packet_bridge.py
- --listen
- 127.0.0.1
- --port
- "8787"
- --tcp-host
- 127.0.0.1
- --tcp-port
- "44242"
- --origin
- ${SITE_ORIGIN:-https://vicliu624.github.io}
- --max-clients
- "32"
healthcheck:
test: [CMD, python, -c, "import os; from websockets.sync.client import connect; c=connect('ws://127.0.0.1:8787',origin=os.environ['SITE_ORIGIN'],open_timeout=3); c.close()"]
interval: 60s
timeout: 5s
retries: 3
start_period: 10s
environment:
SITE_ORIGIN: ${SITE_ORIGIN:-https://vicliu624.github.io}
tunnel:
image: cloudflare/cloudflared:2026.9.3
restart: unless-stopped
network_mode: service:directory
depends_on:
directory:
condition: service_healthy
restart: true
bridge:
condition: service_healthy
restart: true
command: [tunnel, --no-autoupdate, --protocol, http2, run]
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN:?Set the named Cloudflare tunnel token in .env}
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
volumes:
directory-data:
reticulum-data: