From 14ba342160d43e06ea17f33283cf4d905f65c3e7 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 30 Mar 2026 17:09:25 -0700 Subject: [PATCH] Add docker install script --- .gitignore | 4 + README.md | 57 +-- ...compose.yaml => docker-compose.example.yml | 24 +- scripts/setup/install_docker.sh | 353 ++++++++++++++++++ 4 files changed, 411 insertions(+), 27 deletions(-) rename docker-compose.yaml => docker-compose.example.yml (51%) create mode 100644 scripts/setup/install_docker.sh diff --git a/.gitignore b/.gitignore index 16f3c0c..8935eab 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ references/ # ancillary LLM files .claude/ + +# local Docker compose files +docker-compose.yml +docker-compose.yaml diff --git a/README.md b/README.md index b6e3828..e503b0f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Access the app at http://localhost:8000. Source checkouts expect a normal frontend build in `frontend/dist`. > [!NOTE] -> Running on lightweight hardware/ don't want to build the frontend locally? From a cloned checkout, run `python3 scripts/setup/fetch_prebuilt_frontend.py` to fetch and unpack a prebuilt frontend into `frontend/prebuilt`, then start the app normally with `uv run uvicorn app.main:app --host 0.0.0.0 --port 8000`. +> Running on lightweight hardware, or just do not want to build the frontend locally? From a cloned checkout, run `python3 scripts/setup/fetch_prebuilt_frontend.py` to fetch and unpack a prebuilt frontend into `frontend/prebuilt`, then start the app normally with `uv run uvicorn app.main:app --host 0.0.0.0 --port 8000`. > [!TIP] > On Linux, you can also install RemoteTerm as a persistent `systemd` service that starts on boot and restarts automatically on failure: @@ -103,47 +103,62 @@ Source checkouts expect a normal frontend build in `frontend/dist`. ## Path 2: Docker -Edit `docker-compose.yaml` to set a serial device for passthrough, or uncomment your transport (serial or TCP). Then: +> **Warning:** Docker has had reports intermittent issues with serial event subscriptions. The native method above is more reliable. + +Local Docker builds are architecture-native by default. On Apple Silicon Macs and ARM64 Linux hosts such as Raspberry Pi, `docker compose build` / `docker compose up --build` will produce an ARM64 image unless you override the platform. + +Create a local `docker-compose.yml` in one of two ways: + +1. Copy the example file and edit it by hand: ```bash -docker compose up -d +cp docker-compose.example.yml docker-compose.yml ``` -The database is stored in `./data/` (bind-mounted), so the container shares the same database as the native app. To rebuild after pulling updates: +2. Or generate one interactively: ```bash -docker compose up -d --build +bash scripts/setup/install_docker.sh ``` -To use the prebuilt Docker Hub image instead of building locally, replace: +Your local `docker-compose.yml` is gitignored so future pulls do not overwrite your Docker settings. -```yaml -build: . +The guided Docker flow can collect BLE settings, but BLE access from Docker still needs manual compose customization such as Bluetooth passthrough and possibly privileged mode or host networking. If you want the simpler path for BLE, use the regular Python launch flow instead. + +Then customize the local compose file for your transport and launch: + +```bash +docker compose up # -d for background once you validate it's working ``` -with: +The database is stored in `./data/` (bind-mounted), so the container shares the same database as the native app. -```yaml -image: jkingsman/remoteterm-meshcore:latest -``` - -Then run: +To rebuild after pulling updates: ```bash docker compose pull docker compose up -d ``` -Published Docker tags are intended to be multi-arch (`linux/amd64` and `linux/arm64`). If you are building and publishing manually, use Docker Buildx: +The example file and setup script default to the published Docker Hub image. To build locally from your checkout instead, replace: -```bash -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t jkingsman/remoteterm-meshcore:latest \ - --push . +```yaml +image: jkingsman/remoteterm-meshcore:latest ``` -The container runs as root by default for maximum serial passthrough compatibility across host setups. On Linux, if you switch between native and Docker runs, `./data` can end up root-owned. If you do not need that serial compatibility behavior, you can enable the optional `user: "${UID:-1000}:${GID:-1000}"` line in `docker-compose.yaml` to keep ownership aligned with your host user. +with: + +```yaml +build: . +``` + +Then run: + +```bash +docker compose up -d --build +``` + +The container runs as root by default for maximum serial passthrough compatibility across host setups. On Linux, if you switch between native and Docker runs, `./data` can end up root-owned. If you do not need that serial compatibility behavior, you can enable the optional `user: "${UID:-1000}:${GID:-1000}"` line in `docker-compose.yml` to keep ownership aligned with your host user. To stop: diff --git a/docker-compose.yaml b/docker-compose.example.yml similarity index 51% rename from docker-compose.yaml rename to docker-compose.example.yml index 4ed71c0..5b22a90 100644 --- a/docker-compose.yaml +++ b/docker-compose.example.yml @@ -1,30 +1,42 @@ services: remoteterm: - build: . - # image: jkingsman/remoteterm-meshcore:latest + # build: . + image: jkingsman/remoteterm-meshcore:latest # Optional on Linux: run container as your host user to avoid root-owned files in ./data + # This is less reliable for serial-device access than running as root and may require + # extra group setup (for example dialout) or other manual customization. # user: "${UID:-1000}:${GID:-1000}" ports: - "8000:8000" volumes: - ./data:/app/data + ################################################ - # Set your serial device for passthrough here! # + # Map your radio by stable device ID if available. # ################################################ devices: - - /dev/ttyACM0:/dev/ttyUSB0 + - /dev/serial/by-id/your-meshcore-radio:/dev/meshcore-radio environment: MESHCORE_DATABASE_PATH: data/meshcore.db - # Radio connection -- optional if you map just a single serial device above, as the app will autodetect + + # Radio connection # Serial (USB) - # MESHCORE_SERIAL_PORT: /dev/ttyUSB0 + MESHCORE_SERIAL_PORT: /dev/meshcore-radio # MESHCORE_SERIAL_BAUDRATE: 115200 + # TCP # MESHCORE_TCP_HOST: 192.168.1.100 # MESHCORE_TCP_PORT: 4000 + # BLE + # BLE in Docker usually needs additional manual compose changes such as + # Bluetooth device passthrough, privileged mode, host networking, or + # other host-specific tweaks before it will actually work. + # MESHCORE_BLE_ADDRESS: AA:BB:CC:DD:EE:FF + # MESHCORE_BLE_PIN: 123456 + # Security # MESHCORE_DISABLE_BOTS: "true" # MESHCORE_BASIC_AUTH_USERNAME: changeme diff --git a/scripts/setup/install_docker.sh b/scripts/setup/install_docker.sh new file mode 100644 index 0000000..00981be --- /dev/null +++ b/scripts/setup/install_docker.sh @@ -0,0 +1,353 @@ +#!/usr/bin/env bash +# install_docker.sh +# +# Generates a local docker-compose.yml for RemoteTerm from a guided prompt flow. +# The generated compose file is intentionally gitignored so local customization +# does not create merge churn on future pulls. +# +# Run from anywhere inside the repo: +# bash scripts/setup/install_docker.sh + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +PURPLE='\033[0;35m' +BOLD='\033[1m' +NC='\033[0m' + +REPO_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +COMPOSE_FILE="$REPO_DIR/docker-compose.yml" +EXAMPLE_FILE="$REPO_DIR/docker-compose.example.yml" + +IMAGE_MODE="image" +TRANSPORT_MODE="serial" +SERIAL_HOST_PATH="/dev/ttyACM0" +SERIAL_CONTAINER_PATH="/dev/meshcore-radio" +TCP_HOST="" +TCP_PORT="4000" +BLE_ADDRESS="" +BLE_PIN="" +ENABLE_BOTS="N" +ENABLE_AUTH="N" +AUTH_USERNAME="" +AUTH_PASSWORD="" +RUN_AS_HOST_USER="N" +BLE_MANUAL_WARNING=false + +find_serial_devices() { + local -n out_host_paths_ref=$1 + local -n out_labels_ref=$2 + local -n out_display_ref=$3 + local path + local resolved + local label + + out_host_paths_ref=() + out_labels_ref=() + out_display_ref=() + + if [ -d /dev/serial/by-id ]; then + while IFS= read -r path; do + [ -n "$path" ] || continue + resolved="$(readlink -f "$path" 2>/dev/null || true)" + [ -n "$resolved" ] || resolved="$path" + label="$(basename "$path")" + out_host_paths_ref+=("$path") + out_labels_ref+=("$label") + out_display_ref+=("$path -> $resolved") + done < <(find /dev/serial/by-id -maxdepth 1 -type l | sort) + fi + + for path in /dev/ttyACM* /dev/ttyUSB* /dev/cu.usbmodem* /dev/cu.usbserial*; do + [ -e "$path" ] || continue + resolved="$(readlink -f "$path" 2>/dev/null || true)" + [ -n "$resolved" ] || resolved="$path" + + if ((${#out_host_paths_ref[@]} > 0)); then + local existing + for existing in "${out_display_ref[@]}"; do + if [[ "$existing" = *"-> $resolved" ]]; then + resolved="" + break + fi + done + [ -n "$resolved" ] || continue + fi + + out_host_paths_ref+=("$path") + out_labels_ref+=("$(basename "$path")") + out_display_ref+=("$path") + done +} + +echo -e "${BOLD}=== RemoteTerm for MeshCore — Docker Setup ===${NC}" +echo +echo -e " Repo directory : ${CYAN}${REPO_DIR}${NC}" +echo -e " Example compose : ${CYAN}${EXAMPLE_FILE}${NC}" +echo -e " Output compose : ${CYAN}${COMPOSE_FILE}${NC}" +echo + +if ! command -v docker &>/dev/null; then + echo -e "${RED}Error: docker was not found in PATH.${NC}" + exit 1 +fi + +if ! docker compose version &>/dev/null; then + echo -e "${RED}Error: docker compose is required but was not available.${NC}" + exit 1 +fi + +if [ -f "$COMPOSE_FILE" ]; then + echo -e "${YELLOW}A local docker-compose.yml already exists.${NC}" + read -rp "Overwrite it? [y/N]: " OVERWRITE + OVERWRITE="${OVERWRITE:-N}" + if ! [[ "$OVERWRITE" =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Leaving the existing compose file untouched.${NC}" + exit 0 + fi +fi + +echo -e "${BOLD}─── Image Source ────────────────────────────────────────────────────${NC}" +echo "How should Docker run RemoteTerm?" +echo " 1) Use the published Docker Hub image (default)" +echo " 2) Build locally from this checkout" +echo +read -rp "Select image mode [1-2] (default: 1): " IMAGE_CHOICE +IMAGE_CHOICE="${IMAGE_CHOICE:-1}" +echo + +case "$IMAGE_CHOICE" in + 1) + IMAGE_MODE="image" + echo -e "${GREEN}Using published Docker image.${NC}" + ;; + 2) + IMAGE_MODE="build" + echo -e "${GREEN}Using local Docker build.${NC}" + ;; + *) + IMAGE_MODE="image" + echo -e "${YELLOW}Invalid selection; defaulting to published Docker image.${NC}" + ;; +esac +echo + +echo -e "${BOLD}─── Transport ───────────────────────────────────────────────────────${NC}" +echo "How will the container reach your MeshCore radio?" +echo " 1) Serial device passthrough (default)" +echo " 2) TCP" +echo " 3) BLE" +echo +echo "BLE can be configured here, but Docker Bluetooth access still requires manual compose customization." +echo +read -rp "Select transport [1-3] (default: 1): " TRANSPORT_CHOICE +TRANSPORT_CHOICE="${TRANSPORT_CHOICE:-1}" +echo + +case "$TRANSPORT_CHOICE" in + 1) + TRANSPORT_MODE="serial" + SERIAL_HOST_PATHS=() + SERIAL_LABELS=() + SERIAL_DISPLAYS=() + find_serial_devices SERIAL_HOST_PATHS SERIAL_LABELS SERIAL_DISPLAYS + + if ((${#SERIAL_HOST_PATHS[@]} == 0)); then + echo -e "${YELLOW}No serial devices were auto-detected.${NC}" + read -rp "Serial device path on the host (default: /dev/ttyACM0): " SERIAL_HOST_PATH + SERIAL_HOST_PATH="${SERIAL_HOST_PATH:-/dev/ttyACM0}" + else + echo "Detected serial devices:" + for i in "${!SERIAL_HOST_PATHS[@]}"; do + printf ' %d) %s (%s)\n' "$((i + 1))" "${SERIAL_LABELS[$i]}" "${SERIAL_DISPLAYS[$i]}" + done + echo " m) Enter a path manually" + echo + read -rp "Select serial device [1-${#SERIAL_HOST_PATHS[@]} or m] (default: 1): " SERIAL_CHOICE + SERIAL_CHOICE="${SERIAL_CHOICE:-1}" + + if [[ "$SERIAL_CHOICE" =~ ^[Mm]$ ]]; then + read -rp "Serial device path on the host (default: ${SERIAL_HOST_PATHS[0]}): " SERIAL_HOST_PATH + SERIAL_HOST_PATH="${SERIAL_HOST_PATH:-${SERIAL_HOST_PATHS[0]}}" + elif [[ "$SERIAL_CHOICE" =~ ^[0-9]+$ ]] && [ "$SERIAL_CHOICE" -ge 1 ] && [ "$SERIAL_CHOICE" -le "${#SERIAL_HOST_PATHS[@]}" ]; then + SERIAL_HOST_PATH="${SERIAL_HOST_PATHS[$((SERIAL_CHOICE - 1))]}" + else + SERIAL_HOST_PATH="${SERIAL_HOST_PATHS[0]}" + echo -e "${YELLOW}Invalid selection; defaulting to ${SERIAL_HOST_PATH}.${NC}" + fi + fi + + echo -e "${GREEN}Serial passthrough: ${SERIAL_HOST_PATH} -> ${SERIAL_CONTAINER_PATH}${NC}" + ;; + 2) + TRANSPORT_MODE="tcp" + read -rp "TCP host (IP address or hostname): " TCP_HOST + while [ -z "$TCP_HOST" ]; do + echo -e "${RED}TCP host is required.${NC}" + read -rp "TCP host: " TCP_HOST + done + read -rp "TCP port (default: 4000): " TCP_PORT + TCP_PORT="${TCP_PORT:-4000}" + echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}" + ;; + 3) + TRANSPORT_MODE="ble" + read -rp "BLE device address (e.g. AA:BB:CC:DD:EE:FF): " BLE_ADDRESS + while [ -z "$BLE_ADDRESS" ]; do + echo -e "${RED}BLE address is required.${NC}" + read -rp "BLE device address: " BLE_ADDRESS + done + read -rsp "BLE PIN: " BLE_PIN + echo + while [ -z "$BLE_PIN" ]; do + echo -e "${RED}BLE PIN is required.${NC}" + read -rsp "BLE PIN: " BLE_PIN + echo + done + echo -e "${GREEN}BLE: ${BLE_ADDRESS}${NC}" + echo + echo -e "${RED}BLE Docker warning:${NC} Bluetooth access is not fully automated here." + echo -e "${RED}You will need to customize docker-compose.yml manually before BLE works.${NC}" + echo "That may include passing through Bluetooth devices, enabling privileged mode," + echo "using host networking, or other host-specific Docker changes." + echo "If you want the easier path, use the regular Python launch flow for BLE instead." + BLE_MANUAL_WARNING=true + ;; + *) + TRANSPORT_MODE="serial" + SERIAL_HOST_PATH="/dev/ttyACM0" + echo -e "${YELLOW}Invalid selection; defaulting to serial passthrough at ${SERIAL_HOST_PATH}.${NC}" + ;; +esac +echo + +echo -e "${BOLD}─── Bot System ──────────────────────────────────────────────────────${NC}" +echo -e "${YELLOW}Warning:${NC} The bot system executes arbitrary Python code on the server." +echo "It is not recommended on untrusted networks." +echo +read -rp "Enable bots? [y/N]: " ENABLE_BOTS +ENABLE_BOTS="${ENABLE_BOTS:-N}" +echo + +if [[ "$ENABLE_BOTS" =~ ^[Yy]$ ]]; then + echo -e "${GREEN}Bots enabled.${NC}" + echo + echo -e "${BOLD}─── HTTP Basic Auth ─────────────────────────────────────────────────${NC}" + echo "With bots enabled, HTTP Basic Auth is strongly recommended if this" + echo "service will be reachable beyond your local machine." + echo + read -rp "Set up HTTP Basic Auth? [Y/n]: " ENABLE_AUTH + ENABLE_AUTH="${ENABLE_AUTH:-Y}" + echo + + if [[ "$ENABLE_AUTH" =~ ^[Yy]$ ]]; then + read -rp "Username: " AUTH_USERNAME + while [ -z "$AUTH_USERNAME" ]; do + echo -e "${RED}Username cannot be empty.${NC}" + read -rp "Username: " AUTH_USERNAME + done + read -rsp "Password: " AUTH_PASSWORD + echo + while [ -z "$AUTH_PASSWORD" ]; do + echo -e "${RED}Password cannot be empty.${NC}" + read -rsp "Password: " AUTH_PASSWORD + echo + done + echo -e "${GREEN}Basic Auth configured for user '${AUTH_USERNAME}'.${NC}" + fi +else + echo -e "${GREEN}Bots disabled.${NC}" +fi +echo + +if [ "$(uname -s)" = "Linux" ]; then + echo -e "${BOLD}─── Container User ──────────────────────────────────────────────────${NC}" + echo "The container runs as root by default for maximum serial compatibility." + echo "You can override that and run as your host UID/GID instead to avoid" + echo "root-owned files in ./data." + echo + read -rp "Run as your current UID/GID instead of the default root user? [y/N]: " RUN_AS_HOST_USER + RUN_AS_HOST_USER="${RUN_AS_HOST_USER:-N}" + if [[ "$RUN_AS_HOST_USER" =~ ^[Yy]$ ]] && [ "$TRANSPORT_MODE" = "serial" ]; then + echo + echo -e "${YELLOW}Note:${NC} host-user mode can be less reliable for serial device access than running as root." + echo "It may require extra group setup such as dialout, or other manual" + echo "container customization, depending on your host." + echo "If serial access becomes unreliable, rerun this setup and keep the" + echo "default root user instead." + fi + echo +fi + +mkdir -p "$REPO_DIR/data" + +{ + echo "# Generated by scripts/setup/install_docker.sh" + echo "# This file is gitignored. Re-run the setup script to regenerate it." + echo "services:" + echo " remoteterm:" + if [ "$IMAGE_MODE" = "build" ]; then + echo " build: ." + else + echo " image: jkingsman/remoteterm-meshcore:latest" + fi + if [[ "$RUN_AS_HOST_USER" =~ ^[Yy]$ ]]; then + echo " user: \"$(id -u):$(id -g)\"" + fi + echo " ports:" + echo " - \"8000:8000\"" + echo " volumes:" + echo " - ./data:/app/data" + if [ "$TRANSPORT_MODE" = "serial" ]; then + echo " devices:" + echo " - ${SERIAL_HOST_PATH}:${SERIAL_CONTAINER_PATH}" + fi + echo " environment:" + echo " MESHCORE_DATABASE_PATH: data/meshcore.db" + if [ "$TRANSPORT_MODE" = "serial" ]; then + echo " MESHCORE_SERIAL_PORT: ${SERIAL_CONTAINER_PATH}" + elif [ "$TRANSPORT_MODE" = "tcp" ]; then + echo " MESHCORE_TCP_HOST: ${TCP_HOST}" + echo " MESHCORE_TCP_PORT: ${TCP_PORT}" + else + echo " MESHCORE_BLE_ADDRESS: ${BLE_ADDRESS}" + echo " MESHCORE_BLE_PIN: ${BLE_PIN}" + fi + if ! [[ "$ENABLE_BOTS" =~ ^[Yy]$ ]]; then + echo " MESHCORE_DISABLE_BOTS: \"true\"" + fi + if [[ "$ENABLE_AUTH" =~ ^[Yy]$ ]]; then + echo " MESHCORE_BASIC_AUTH_USERNAME: ${AUTH_USERNAME}" + echo " MESHCORE_BASIC_AUTH_PASSWORD: ${AUTH_PASSWORD}" + fi + echo " restart: unless-stopped" +} >"$COMPOSE_FILE" + +echo -e "${GREEN}Generated ${COMPOSE_FILE}.${NC}" +echo +echo -e "${BOLD}Docker commands${NC}" +if [ "$IMAGE_MODE" = "build" ]; then + echo " docker compose up -d --build # build the local image and start RemoteTerm in the background" +else + echo " docker compose up -d # start RemoteTerm in the background" +fi +echo " docker compose logs -f # follow the container logs live" +echo +echo " docker compose down # stop and remove the running container" +echo " docker compose restart # restart the container without changing the image" +echo " docker compose pull && docker compose up -d # upgrade to the latest published image and restart" +if [ "$TRANSPORT_MODE" = "ble" ] || [ "$BLE_MANUAL_WARNING" = true ]; then + echo + echo -e "${RED}BLE requires more than the generated env vars.${NC}" + echo -e "${RED}Before starting, edit docker-compose.yml for Bluetooth passthrough and any privileged/network settings your host requires.${NC}" +fi +echo +echo -e "${GREEN}Your new docker file is ready at ${COMPOSE_FILE}.${NC}" +echo -e "${GREEN}Feel free to edit it by hand as desired, or:${NC}" +echo +echo -e "${PURPLE}┌──────────────────────────────────────────────┐${NC}" +echo -e "${PURPLE}│ Run ${GREEN}${BOLD}docker compose up -d${NC}${PURPLE} to get started. │${NC}" +echo -e "${PURPLE}└──────────────────────────────────────────────┘${NC}"