diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..33f0cd6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,76 @@ +# Git +.git +.gitignore + +# Documentation +README.md +CHANGELOG.md +*.md + +# Docker files +docker-compose*.yml +.dockerignore + +# Environment files +.env* +!.env.example + +# Logs +*.log +logs/ + +# Runtime data +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +vendor/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Test files +tests/ +spec/ +test_* +*_test.py +*_spec.rb + +# Development files +ai_docs/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2f27ef7 --- /dev/null +++ b/.env.example @@ -0,0 +1,71 @@ +# PotatoMesh Environment Configuration +# Copy this file to .env and customize for your setup + +# ============================================================================= +# REQUIRED SETTINGS +# ============================================================================= + +# API authentication token (required for ingestor communication) +# Generate a secure token: openssl rand -hex 32 +API_TOKEN=your-secure-api-token-here + +# Meshtastic device path (required for ingestor) +# Common paths: +# - Linux: /dev/ttyACM0, /dev/ttyUSB0 +# - macOS: /dev/cu.usbserial-* +# - Windows (WSL): /dev/ttyS* +MESH_SERIAL=/dev/ttyACM0 + +# ============================================================================= +# SITE CUSTOMIZATION +# ============================================================================= + +# Your mesh network name +SITE_NAME=My Meshtastic Network + +# Default Meshtastic channel +DEFAULT_CHANNEL=#MediumFast + +# Default frequency for your region +# Common frequencies: 868MHz (Europe), 915MHz (US), 433MHz (Worldwide) +DEFAULT_FREQUENCY=868MHz + +# Map center coordinates (latitude, longitude) +# Berlin, Germany: 52.502889, 13.404194 +# Denver, Colorado: 39.7392, -104.9903 +# London, UK: 51.5074, -0.1278 +MAP_CENTER_LAT=52.502889 +MAP_CENTER_LON=13.404194 + +# Maximum distance to show nodes (kilometers) +MAX_NODE_DISTANCE_KM=50 + +# ============================================================================= +# OPTIONAL INTEGRATIONS +# ============================================================================= + +# Matrix chat room for your community (optional) +# Format: !roomid:matrix.org +MATRIX_ROOM='#meshtastic-berlin:matrix.org' + + +# ============================================================================= +# ADVANCED SETTINGS +# ============================================================================= + +# Debug mode (0=off, 1=on) +DEBUG=0 + +# Meshtastic snapshot interval (seconds) +MESH_SNAPSHOT_SECS=60 + +# Meshtastic channel index (0=primary, 1=secondary, etc.) +MESH_CHANNEL_INDEX=0 + +# Database settings +DB_BUSY_TIMEOUT_MS=5000 +DB_BUSY_MAX_RETRIES=5 +DB_BUSY_RETRY_DELAY=0.05 + +# Application settings +MAX_JSON_BODY_BYTES=1048576 diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..17d308f --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,18 @@ +# GitHub Actions Workflows + +## Workflows + +- **`docker.yml`** - Build and push Docker images to GHCR +- **`codeql.yml`** - Security scanning +- **`python.yml`** - Python testing +- **`ruby.yml`** - Ruby testing + +## Usage + +```bash +# Build locally +docker-compose build + +# Deploy +docker-compose up -d +``` diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3bcf402 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,167 @@ +name: Build and Push Docker Images + +on: + push: + tags: [ 'v*' ] + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.0.0)' + required: true + default: '1.0.0' + publish_all_variants: + description: 'Publish all Docker image variants (latest tag)' + type: boolean + default: false + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: l5yth/potato-mesh + +jobs: + build-and-push: + runs-on: ubuntu-latest + if: (startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push') || github.event_name == 'workflow_dispatch' + environment: production + permissions: + contents: read + packages: write + + strategy: + matrix: + service: [web, ingestor] + architecture: + - { name: linux-amd64, platform: linux/amd64, label: "Linux x86_64" } + - { name: windows-amd64, platform: windows/amd64, label: "Windows x86_64" } + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag or input + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION=${GITHUB_REF#refs/tags/v} + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Published version: $VERSION" + + - name: Build and push ${{ matrix.service }} for ${{ matrix.architecture.name }} + uses: docker/build-push-action@v5 + with: + context: . + file: ./${{ matrix.service == 'web' && 'web/Dockerfile' || 'data/Dockerfile' }} + target: production + platforms: ${{ matrix.architecture.platform }} + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-${{ matrix.service }}-${{ matrix.architecture.name }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-${{ matrix.service }}-${{ matrix.architecture.name }}:${{ steps.version.outputs.version }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=PotatoMesh ${{ matrix.service == 'web' && 'Web Application' || 'Python Ingestor' }} for ${{ matrix.architecture.label }} + org.opencontainers.image.licenses=Apache-2.0 + org.opencontainers.image.version=${{ steps.version.outputs.version }} + org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.title=PotatoMesh ${{ matrix.service == 'web' && 'Web' || 'Ingestor' }} (${{ matrix.architecture.label }}) + org.opencontainers.image.vendor=PotatoMesh + org.opencontainers.image.architecture=${{ matrix.architecture.name }} + org.opencontainers.image.os=linux + org.opencontainers.image.arch=${{ matrix.architecture.name }} + cache-from: type=gha,scope=${{ matrix.service }}-${{ matrix.architecture.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ matrix.architecture.name }} + + test-images: + runs-on: ubuntu-latest + needs: build-and-push + if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Test web application (Linux AMD64) + run: | + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web-linux-amd64:${{ steps.version.outputs.version }} + docker run --rm -d --name web-test -p 41447:41447 \ + -e API_TOKEN=test-token \ + -e DEBUG=1 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web-linux-amd64:${{ steps.version.outputs.version }} + sleep 10 + curl -f http://localhost:41447/ || exit 1 + docker stop web-test + + - name: Test ingestor (Linux AMD64) + run: | + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-ingestor-linux-amd64:${{ steps.version.outputs.version }} + docker run --rm --name ingestor-test \ + -e POTATOMESH_INSTANCE=http://localhost:41447 \ + -e API_TOKEN=test-token \ + -e DEBUG=1 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-ingestor-linux-amd64:${{ steps.version.outputs.version }} & + sleep 5 + docker stop ingestor-test || true + + publish-summary: + runs-on: ubuntu-latest + needs: [build-and-push, test-images] + if: always() && startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' + + steps: + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Publish release summary + run: | + echo "## 🚀 PotatoMesh Images Published to GHCR" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Published Images:**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Web images + echo "### 🌐 Web Application" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web-linux-amd64:latest\` - Linux x86_64" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web-windows-amd64:latest\` - Windows x86_64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Ingestor images + echo "### 📡 Ingestor Service" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-ingestor-linux-amd64:latest\` - Linux x86_64" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-ingestor-windows-amd64:latest\` - Windows x86_64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 24dc2b0..d50e468 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ /tmp/ # Used by dotenv library to load environment variables. -# .env +.env # Ignore Byebug command history file. .byebug_history @@ -62,3 +62,6 @@ coverage/ coverage.xml htmlcov/ reports/ + +# AI planning and documentation +ai_docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a8b60..92c0b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v0.3.0 + +* Add comprehensive Docker support with multi-architecture builds and automated CI/CD by @trose in + ## v0.2.0 * Update readme for 0.2 by @l5yth in diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..cad3c2b --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,88 @@ +# PotatoMesh Docker Setup + +## Quick Start + +```bash +./configure.sh +docker-compose up -d +docker-compose logs -f +``` + +Access at `http://localhost:41447` + +## Configuration + +Edit `.env` file or run `./configure.sh` to set: + +- `API_TOKEN` - Required for ingestor authentication +- `MESH_SERIAL` - Your Meshtastic device path (e.g., `/dev/ttyACM0`) +- `SITE_NAME` - Your mesh network name +- `MAP_CENTER_LAT/LON` - Map center coordinates + +## Device Setup + +**Find your device:** +```bash +# Linux +ls /dev/ttyACM* /dev/ttyUSB* + +# macOS +ls /dev/cu.usbserial-* + +# Windows +ls /dev/ttyS* +``` + +**Set permissions (Linux/macOS):** +```bash +sudo chmod 666 /dev/ttyACM0 +# Or add user to dialout group +sudo usermod -a -G dialout $USER +``` + +## Common Commands + +```bash +# Start services +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down + +# Stop and remove data +docker-compose down -v + +# Update images +docker-compose pull && docker-compose up -d +``` + +## Troubleshooting + +**Device access issues:** +```bash +# Check device exists and permissions +ls -la /dev/ttyACM0 + +# Fix permissions +sudo chmod 666 /dev/ttyACM0 +``` + +**Port conflicts:** +```bash +# Find what's using port 41447 +sudo lsof -i :41447 +``` + +**Container issues:** +```bash +# Check logs +docker-compose logs + +# Restart services +docker-compose restart +``` + +For more Docker help, see [Docker Compose documentation](https://docs.docker.com/compose/). \ No newline at end of file diff --git a/README.md b/README.md index 730758a..72e7932 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ Live demo for Berlin #MediumFast: [potatomesh.net](https://potatomesh.net) ![screenshot of the second version](./scrot-0.2.png) +## 🐳 Quick Start with Docker + +```bash +./configure.sh # Configure your setup +docker-compose up -d # Start services +docker-compose logs -f # View logs +``` + ## Web App Requires Ruby for the Sinatra web app and SQLite3 for the app's database. diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..2b6756a --- /dev/null +++ b/configure.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +# PotatoMesh Configuration Script +# This script helps you configure your PotatoMesh instance with your local settings + +set -e + +echo "🥔 PotatoMesh Configuration" +echo "==========================" +echo "" + +# Check if .env exists, if not create from .env.example +if [ ! -f .env ]; then + if [ -f .env.example ]; then + echo "📋 Creating .env file from .env.example..." + cp .env.example .env + else + echo "📋 Creating new .env file..." + touch .env + fi +fi + +echo "🔧 Let's configure your PotatoMesh instance!" +echo "" + +# Function to read input with default +read_with_default() { + local prompt="$1" + local default="$2" + local var_name="$3" + + if [ -n "$default" ]; then + read -p "$prompt [$default]: " input + input=${input:-$default} + else + read -p "$prompt: " input + fi + + eval "$var_name='$input'" +} + +# Function to update .env file +update_env() { + local key="$1" + local value="$2" + + if grep -q "^$key=" .env; then + # Update existing value + sed -i.bak "s/^$key=.*/$key=$value/" .env + else + # Add new value + echo "$key=$value" >> .env + fi +} + +# Get current values from .env if they exist +SITE_NAME=$(grep "^SITE_NAME=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "My Meshtastic Network") +DEFAULT_CHANNEL=$(grep "^DEFAULT_CHANNEL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "#MediumFast") +DEFAULT_FREQUENCY=$(grep "^DEFAULT_FREQUENCY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "868MHz") +MAP_CENTER_LAT=$(grep "^MAP_CENTER_LAT=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "52.502889") +MAP_CENTER_LON=$(grep "^MAP_CENTER_LON=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "13.404194") +MAX_NODE_DISTANCE_KM=$(grep "^MAX_NODE_DISTANCE_KM=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "50") +MATRIX_ROOM=$(grep "^MATRIX_ROOM=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "") +API_TOKEN=$(grep "^API_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' || echo "") + +echo "📍 Location Settings" +echo "-------------------" +read_with_default "Site Name (your mesh network name)" "$SITE_NAME" SITE_NAME +read_with_default "Map Center Latitude" "$MAP_CENTER_LAT" MAP_CENTER_LAT +read_with_default "Map Center Longitude" "$MAP_CENTER_LON" MAP_CENTER_LON +read_with_default "Max Node Distance (km)" "$MAX_NODE_DISTANCE_KM" MAX_NODE_DISTANCE_KM + +echo "" +echo "📡 Meshtastic Settings" +echo "---------------------" +read_with_default "Default Channel" "$DEFAULT_CHANNEL" DEFAULT_CHANNEL +read_with_default "Default Frequency (868MHz, 915MHz, etc.)" "$DEFAULT_FREQUENCY" DEFAULT_FREQUENCY + +echo "" +echo "💬 Optional Settings" +echo "-------------------" +read_with_default "Matrix Room (optional, e.g., #meshtastic-berlin:matrix.org)" "$MATRIX_ROOM" MATRIX_ROOM + +echo "" +echo "🔐 Security Settings" +echo "-------------------" +echo "The API token is used for secure communication between the web app and ingestor." +echo "You can provide your own custom token or let us generate a secure one for you." +echo "" + +if [ -z "$API_TOKEN" ]; then + echo "No existing API token found. Generating a secure token..." + API_TOKEN=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null || echo "your-secure-api-token-here") + echo "✅ Generated secure API token: ${API_TOKEN:0:8}..." + echo "" + read -p "Use this generated token? (Y/n): " use_generated + if [[ "$use_generated" =~ ^[Nn]$ ]]; then + read -p "Enter your custom API token: " API_TOKEN + fi +else + echo "Existing API token found: ${API_TOKEN:0:8}..." + read -p "Keep existing token? (Y/n): " keep_existing + if [[ "$keep_existing" =~ ^[Nn]$ ]]; then + read -p "Enter new API token (or press Enter to generate): " new_token + if [ -n "$new_token" ]; then + API_TOKEN="$new_token" + else + echo "Generating new secure token..." + API_TOKEN=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null || echo "your-secure-api-token-here") + echo "✅ Generated new API token: ${API_TOKEN:0:8}..." + fi + fi +fi + +echo "" +echo "📝 Updating .env file..." + +# Update .env file +update_env "SITE_NAME" "\"$SITE_NAME\"" +update_env "DEFAULT_CHANNEL" "\"$DEFAULT_CHANNEL\"" +update_env "DEFAULT_FREQUENCY" "\"$DEFAULT_FREQUENCY\"" +update_env "MAP_CENTER_LAT" "$MAP_CENTER_LAT" +update_env "MAP_CENTER_LON" "$MAP_CENTER_LON" +update_env "MAX_NODE_DISTANCE_KM" "$MAX_NODE_DISTANCE_KM" +update_env "MATRIX_ROOM" "\"$MATRIX_ROOM\"" +update_env "API_TOKEN" "$API_TOKEN" + +# Add other common settings if they don't exist +if ! grep -q "^MESH_SERIAL=" .env; then + echo "MESH_SERIAL=/dev/ttyACM0" >> .env +fi + +if ! grep -q "^DEBUG=" .env; then + echo "DEBUG=0" >> .env +fi + +# Clean up backup file +rm -f .env.bak + +echo "" +echo "✅ Configuration complete!" +echo "" +echo "📋 Your settings:" +echo " Site Name: $SITE_NAME" +echo " Map Center: $MAP_CENTER_LAT, $MAP_CENTER_LON" +echo " Max Distance: ${MAX_NODE_DISTANCE_KM}km" +echo " Channel: $DEFAULT_CHANNEL" +echo " Frequency: $DEFAULT_FREQUENCY" +echo " Matrix Room: ${MATRIX_ROOM:-'Not set'}" +echo " API Token: ${API_TOKEN:0:8}..." +echo "" +echo "🚀 You can now start PotatoMesh with:" +echo " docker-compose up -d" +echo "" +echo "📖 For more configuration options, see the README.md" diff --git a/data/Dockerfile b/data/Dockerfile new file mode 100644 index 0000000..3fe70a1 --- /dev/null +++ b/data/Dockerfile @@ -0,0 +1,55 @@ +# Multi-stage build for PotatoMesh Data Ingestor +FROM python:3.13-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache \ + gcc \ + musl-dev \ + linux-headers \ + build-base + +# Set working directory +WORKDIR /app + +# Copy requirements and install Python dependencies +COPY data/requirements.txt ./ +RUN pip install --no-cache-dir --user -r requirements.txt + +# Production stage +FROM python:3.13-alpine AS production + +# Install runtime dependencies +RUN apk add --no-cache \ + tzdata \ + curl + +# Create non-root user and add to dialout group for serial access +RUN addgroup -g 1000 -S potatomesh && \ + adduser -u 1000 -S potatomesh -G potatomesh && \ + adduser potatomesh dialout + +# Set working directory +WORKDIR /app + +# Copy installed Python packages from builder stage +COPY --from=builder /root/.local /home/potatomesh/.local + +# Copy application code +COPY --chown=potatomesh:potatomesh data/ . + +# Switch to non-root user +USER potatomesh + +# Add local Python packages to PATH +ENV PATH=/home/potatomesh/.local/bin:$PATH + +# Default environment variables (can be overridden by host) +ENV MESH_SERIAL=/dev/ttyACM0 \ + MESH_SNAPSHOT_SECS=60 \ + MESH_CHANNEL_INDEX=0 \ + DEBUG=0 \ + POTATOMESH_INSTANCE="" \ + API_TOKEN="" + +# Start the mesh daemon +CMD ["python", "mesh.py"] diff --git a/data/requirements.txt b/data/requirements.txt new file mode 100644 index 0000000..70cf808 --- /dev/null +++ b/data/requirements.txt @@ -0,0 +1,7 @@ +# Production dependencies +meshtastic>=2.0.0 +protobuf>=4.21.12 + +# Development dependencies (optional) +black>=23.0.0 +pytest>=7.0.0 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..85407b0 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +version: '3.8' + +# Development overrides for docker-compose.yml +services: + web: + environment: + - DEBUG=1 + volumes: + - ./web:/app + - ./data:/data # Mount data directory for SQL files + - /app/vendor/bundle # Exclude vendor directory from volume mount + ports: + - "41447:41447" + - "9292:9292" # Additional port for development tools + + ingestor: + environment: + - DEBUG=1 + volumes: + - ./data:/app + - /app/.local # Exclude Python packages from volume mount diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..b542d26 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,33 @@ +version: '3.8' + +# Production overrides for docker-compose.yml +services: + web: + build: + target: production + environment: + - DEBUG=0 + restart: always + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + reservations: + memory: 256M + cpus: '0.25' + + ingestor: + build: + target: production + environment: + - DEBUG=0 + restart: always + deploy: + resources: + limits: + memory: 256M + cpus: '0.25' + reservations: + memory: 128M + cpus: '0.1' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..82c127e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +version: '3.8' + +services: + web: + image: ghcr.io/l5yth/potato-mesh-web-linux-amd64:latest + container_name: potatomesh-web + ports: + - "41447:41447" + environment: + - SITE_NAME=${SITE_NAME:-My Meshtastic Network} + - DEFAULT_CHANNEL=${DEFAULT_CHANNEL:-#MediumFast} + - DEFAULT_FREQUENCY=${DEFAULT_FREQUENCY:-868MHz} + - MAP_CENTER_LAT=${MAP_CENTER_LAT:-52.502889} + - MAP_CENTER_LON=${MAP_CENTER_LON:-13.404194} + - MAX_NODE_DISTANCE_KM=${MAX_NODE_DISTANCE_KM:-50} + - MATRIX_ROOM=${MATRIX_ROOM:-} + - API_TOKEN=${API_TOKEN} + - DEBUG=${DEBUG:-0} + volumes: + - potatomesh_data:/app/data + - potatomesh_logs:/app/logs + networks: + - potatomesh-network + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + reservations: + memory: 256M + cpus: '0.25' + + ingestor: + image: ghcr.io/l5yth/potato-mesh-ingestor-linux-amd64:latest + container_name: potatomesh-ingestor + environment: + - MESH_SERIAL=${MESH_SERIAL:-/dev/ttyACM0} + - MESH_SNAPSHOT_SECS=${MESH_SNAPSHOT_SECS:-60} + - MESH_CHANNEL_INDEX=${MESH_CHANNEL_INDEX:-0} + - POTATOMESH_INSTANCE=${POTATOMESH_INSTANCE:-http://web:41447} + - API_TOKEN=${API_TOKEN} + - DEBUG=${DEBUG:-0} + volumes: + - potatomesh_data:/app/data + - potatomesh_logs:/app/logs + devices: + # Map Meshtastic serial device from host to container + # Common paths: /dev/ttyACM0, /dev/ttyUSB0, /dev/cu.usbserial-* + - ${MESH_SERIAL:-/dev/ttyACM0}:${MESH_SERIAL:-/dev/ttyACM0} + privileged: false + networks: + - potatomesh-network + depends_on: + - web + restart: unless-stopped + deploy: + resources: + limits: + memory: 256M + cpus: '0.25' + reservations: + memory: 128M + cpus: '0.1' + +volumes: + potatomesh_data: + driver: local + potatomesh_logs: + driver: local + diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..258ad31 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,74 @@ +# Main application builder stage +FROM ruby:3.4-alpine AS builder + +# Install build dependencies and SQLite3 +RUN apk add --no-cache \ + build-base \ + sqlite-dev \ + linux-headers \ + pkgconfig + +# Set working directory +WORKDIR /app + +# Copy Gemfile and install dependencies +COPY web/Gemfile web/Gemfile.lock* ./ + +# Install gems with SQLite3 support +RUN bundle config set --local without 'development test' && \ + bundle install --jobs=4 --retry=3 + +# Production stage +FROM ruby:3.4-alpine AS production + +# Install runtime dependencies +RUN apk add --no-cache \ + sqlite \ + tzdata \ + curl + +# Create non-root user +RUN addgroup -g 1000 -S potatomesh && \ + adduser -u 1000 -S potatomesh -G potatomesh + +# Set working directory +WORKDIR /app + +# Copy installed gems from builder stage +COPY --from=builder /usr/local/bundle /usr/local/bundle + +# Copy application code (exclude Dockerfile from web directory) +COPY --chown=potatomesh:potatomesh web/app.rb web/app.sh web/Gemfile web/Gemfile.lock* web/public/ web/spec/ ./ +COPY --chown=potatomesh:potatomesh web/views/ ./views/ + +# Copy SQL schema files from data directory +COPY --chown=potatomesh:potatomesh data/*.sql /data/ + +# Create data directory for SQLite database +RUN mkdir -p /app/data && \ + chown -R potatomesh:potatomesh /app/data + +# Switch to non-root user +USER potatomesh + +# Expose port +EXPOSE 41447 + +# Default environment variables (can be overridden by host) +ENV APP_ENV=production \ + MESH_DB=/app/data/mesh.db \ + DB_BUSY_TIMEOUT_MS=5000 \ + DB_BUSY_MAX_RETRIES=5 \ + DB_BUSY_RETRY_DELAY=0.05 \ + MAX_JSON_BODY_BYTES=1048576 \ + SITE_NAME="Berlin Mesh Network" \ + DEFAULT_CHANNEL="#MediumFast" \ + DEFAULT_FREQUENCY="868MHz" \ + MAP_CENTER_LAT=52.502889 \ + MAP_CENTER_LON=13.404194 \ + MAX_NODE_DISTANCE_KM=50 \ + MATRIX_ROOM="" \ + DEBUG=0 + +# Start the application +CMD ["ruby", "app.rb", "-p", "41447", "-o", "0.0.0.0"]