Add docker install script

This commit is contained in:
Jack Kingsman
2026-03-30 17:09:25 -07:00
parent 7460c3ea9d
commit 14ba342160
4 changed files with 411 additions and 27 deletions

4
.gitignore vendored
View File

@@ -25,3 +25,7 @@ references/
# ancillary LLM files
.claude/
# local Docker compose files
docker-compose.yml
docker-compose.yaml

View File

@@ -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:

View File

@@ -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

View File

@@ -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}"