← Back to Blog

Embedded Systems – Arduino Server-Side Compiler

Production-ready Docker-based Arduino compilation server – stable, reproducible builds for ESP8266, ESP32, STM32, and RP2040. Persistent library management, fast compilation, and server-side automation.

8 min read Roman Swetly

Embedded Systems – Arduino Server-Side Compiler

Production-ready Docker-based Arduino compilation server – stable, reproducible builds for ESP8266, ESP32, STM32, and RP2040.


Overview

Arduino development traditionally requires a full IDE installation with platform-specific tools, libraries, and dependencies. This creates challenges:

  • Inconsistent environments across developer machines.
  • Slow updates when new board versions are released.
  • Large disk footprints for each platform.
  • Reproducibility issues – builds work on one machine but fail on another.

My solution: A Docker-based Arduino compilation server that provides:

  • Stable, reproducible builds – same environment every time.
  • Fast compilation – no GUI overhead.
  • Persistent library storage – install once, use forever.
  • Multi-platform support – ESP8266, ESP32, STM32, RP2040, and more.
  • Server-side automation – compiles without opening the IDE.

Architecture

Embedded Systems – Arduino Server-Side Compiler

Project Structure

~/arduinoServer/
├── Dockerfile
├── docker-compose.yml
├── arduino-cli.yaml
├── scripts/
│   ├── setup.sh
│   └── compile.sh
├── sketches/
│   ├── Blink/
│   │   └── Blink.ino
│   └── HeltecLED/
│       └── HeltecLED.ino
├── data/
│   └── .arduino15/          # Persistent data (cores, libs, tools)
└── output/
    ├── Blink.bin
    ├── HeltecLED.bin
    ├── Blink/
    └── HeltecLED/

Dockerfile

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
ENV PATH="/root/.local/bin:${PATH}"

RUN apt-get update && apt-get install -y \
    curl \
    python3.11 \
    python3-pip \
    git \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Create symlink for python3
RUN ln -sf /usr/bin/python3.11 /usr/bin/python3

# Install Arduino CLI
RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
RUN mv bin/arduino-cli /usr/local/bin/

# Install esptool for uploads
RUN pip3 install esptool

# Create working directory
RUN mkdir -p /workspace /root/.arduino15

WORKDIR /workspace

ENTRYPOINT ["arduino-cli"]
CMD ["--help"]

docker-compose.yml

version: '3.8'

services:
  # Main arduino-cli service
  arduino-cli:
    build: .
    entrypoint: arduino-cli
    volumes:
      - ./sketches:/workspace/sketches:ro
      - ./output:/workspace/output
      - ./data/.arduino15:/root/.arduino15
      - ./arduino-cli.yaml:/root/.arduino15/arduino-cli.yaml:ro
    environment:
      - ARDUINO_CONFIG_FILE=/root/.arduino15/arduino-cli.yaml
    working_dir: /workspace

  # Interactive shell for setup and debugging
  shell:
    build: .
    entrypoint: bash
    volumes:
      - ./sketches:/workspace/sketches
      - ./output:/workspace/output
      - ./data/.arduino15:/root/.arduino15
      - ./arduino-cli.yaml:/root/.arduino15/arduino-cli.yaml:ro
      - ./scripts:/scripts:ro
    working_dir: /workspace

  # One-time setup service
  setup:
    build: .
    entrypoint: bash
    volumes:
      - ./data/.arduino15:/root/.arduino15
      - ./scripts:/scripts:ro
    environment:
      - ARDUINO_CONFIG_FILE=/root/.arduino15/arduino-cli.yaml
    command: /scripts/setup.sh
    profiles:
      - setup

arduino-cli.yaml

board_manager:
  additional_urls:
    - https://arduino.esp8266.com/stable/package_esp8266com_index.json
    - https://espressif.github.io/arduino-esp32/package_esp32_index.json
    - https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
    - https://m5stack.com/resource/package_m5stack_index.json
    - https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json

directories:
  data: /root/.arduino15
  downloads: /root/.arduino15/staging
  user: /root/.arduino15

library:
  enable_unsafe_install: false

sketch:
  always_export_binaries: true

Setup Script (scripts/setup.sh)

#!/bin/bash

echo "========================================="
echo "🔧 Arduino CLI Setup Script"
echo "========================================="

# Update core indexes
echo "📦 Updating core indexes..."
arduino-cli core update-index
arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json
arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json

# Install platforms
echo "📦 Installing ESP8266 platform..."
arduino-cli core install esp8266:esp8266

echo "📦 Installing ESP32 platform..."
arduino-cli core install esp32:esp32 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json

# Install libraries
echo "📚 Installing required libraries..."
libraries=(
    "Heltec ESP32"
    "RadioLib"
    "ESP8266 and ESP32 OLED driver for SSD1306 displays"
)

for lib in "${libraries[@]}"; do
    echo "  Installing: $lib"
    arduino-cli lib install "$lib" 2>/dev/null || echo "    (may already be installed)"
done

# Install Heltec_ESP32_LoRa_v3 (unofficial)
echo "📚 Installing Heltec_ESP32_LoRa_v3..."
if [ ! -d "/root/.arduino15/libraries/Heltec_ESP32_LoRa_v3" ]; then
    cd /root/.arduino15/libraries
    git clone https://github.com/ropg/Heltec_ESP32_LoRa_v3.git
    cd Heltec_ESP32_LoRa_v3
    ln -sf src/heltec_unofficial.h .
    cd /
    echo "  ✅ Installed and configured"
else
    echo "  ✅ Already installed"
fi

# Install HotButton library
echo "📚 Installing HotButton..."
arduino-cli lib install HotButton 2>/dev/null || {
    cd /root/.arduino15/libraries
    git clone https://github.com/ropg/HotButton.git
    echo "  ✅ Cloned from GitHub"
}

echo ""
echo "========================================="
echo "✅ Setup Complete!"
echo "========================================="

Compile Script (scripts/compile.sh)

#!/bin/bash

SKETCH_NAME=$1
FQBN=$2

if [ -z "$SKETCH_NAME" ] || [ -z "$FQBN" ]; then
    echo "Usage: $0 <sketch_name> <fqbn>"
    echo "Example: $0 Blink esp8266:esp8266:d1_mini"
    echo "Example: $0 HeltecLED esp32:esp32:heltec_wifi_lora_32_V3"
    exit 1
fi

echo "========================================="
echo "🔨 Compiling: $SKETCH_NAME"
echo "Board: $FQBN"
echo "========================================="

mkdir -p /workspace/output/$SKETCH_NAME

arduino-cli compile \
    --fqbn "$FQBN" \
    --output-dir "/workspace/output/$SKETCH_NAME" \
    "/workspace/sketches/$SKETCH_NAME"

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ Compilation successful!"
    cp "/workspace/output/$SKETCH_NAME/${SKETCH_NAME}.ino.bin" "/workspace/output/${SKETCH_NAME}.bin"
    echo "📁 Binary: /workspace/output/${SKETCH_NAME}.bin"
    ls -la "/workspace/output/${SKETCH_NAME}.bin"
else
    echo "❌ Compilation failed!"
    exit 1
fi

Example Sketches

Blink.ino (ESP8266 D1 Mini)

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, LOW);   // LED ON (active LOW)
  delay(1000);
  digitalWrite(LED_BUILTIN, HIGH);  // LED OFF
  delay(2000);
}

HeltecLED.ino (Heltec WiFi LoRa V3)

#include <heltec.h>

void setup() {
  // Initialize Heltec board: Display ON, LoRa OFF, Serial ON
  Heltec.begin(true, false, true);
  
  // Initialize display
  Heltec.display->init();
  Heltec.display->setContrast(255);
  Heltec.display->clear();
  Heltec.display->setFont(ArialMT_Plain_16);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(Heltec.display->getWidth()/2, Heltec.display->getHeight()/2 - 10, "LED Demo");
  Heltec.display->display();
  
  Serial.begin(115200);
  Serial.println("LED Demo Started");
  
  // On Heltec V3, the built-in LED is typically on GPIO35
  pinMode(35, OUTPUT);
}

void loop() {
  // Blink LED
  digitalWrite(35, HIGH);
  displayMessage("LED ON");
  delay(500);
  
  digitalWrite(35, LOW);
  displayMessage("LED OFF");
  delay(500);
}

void displayMessage(String msg) {
  Heltec.display->clear();
  Heltec.display->setFont(ArialMT_Plain_16);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(Heltec.display->getWidth()/2, Heltec.display->getHeight()/2, msg);
  Heltec.display->display();
}

Usage Commands

Initial Setup (One Time)

cd ~/arduinoServer

# Build the container
docker-compose build

# Run setup (installs all platforms and libraries)
docker-compose run --rm setup

Compile for ESP8266 D1 Mini

cd ~/arduinoServer

# Using the compile script
docker-compose run --rm arduino-cli bash -c "/workspace/scripts/compile.sh Blink esp8266:esp8266:d1_mini"

# Or direct command
docker-compose run --rm arduino-cli \
  compile --fqbn esp8266:esp8266:d1_mini \
  --output-dir /workspace/output/Blink \
  /workspace/sketches/Blink

Compilation Output:

Variables and constants in RAM (global, static), used 28104 / 80192 bytes (35%)
║   SEGMENT  BYTES    DESCRIPTION
╠══ DATA     1496     initialized variables
╠══ RODATA   920      constants       
╚══ BSS      25688    zeroed variables
. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59667 / 65536 bytes (91%)
. Code in flash (default, ICACHE_FLASH_ATTR), used 232148 / 1048576 bytes (22%)

Compile for Heltec WiFi LoRa V3

cd ~/arduinoServer

# Using the compile script
docker-compose run --rm arduino-cli bash -c "/workspace/scripts/compile.sh HeltecLED esp32:esp32:heltec_wifi_lora_32_V3"

# Or direct command
docker-compose run --rm arduino-cli \
  compile --fqbn esp32:esp32:heltec_wifi_lora_32_V3 \
  --output-dir /workspace/output/HeltecLED \
  /workspace/sketches/HeltecLED

Compilation Output:

Sketch uses 396262 bytes (11%) of program storage space. Maximum is 3342336 bytes.
Global variables use 24652 bytes (7%) of dynamic memory, leaving 303028 bytes for local variables.

Upload to Device

# Find the USB port
ls /dev/ttyUSB*

# Upload ESP8266 D1 Mini
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash 0x00000 output/Blink.bin

Adding Additional Boards

Method 1: Direct Command

# Install STM32
docker-compose run --rm arduino-cli core update-index \
  --additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json

docker-compose run --rm arduino-cli core install STMicroelectronics:stm32 \
  --additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json

Method 2: Interactive Shell

docker-compose run --rm shell

# Inside container:
arduino-cli core update-index --additional-urls https://your-board-url.com
arduino-cli core install vendor:architecture:board --additional-urls https://your-board-url.com

exit

Method 3: Update arduino-cli.yaml

Add URLs to arduino-cli.yaml, then:

docker-compose run --rm arduino-cli core update-index
docker-compose run --rm arduino-cli core install STMicroelectronics:stm32

Common Board URLs

Board FamilyURL
ESP8266https://arduino.esp8266.com/stable/package_esp8266com_index.json
ESP32https://espressif.github.io/arduino-esp32/package_esp32_index.json
STM32https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
M5Stackhttps://m5stack.com/resource/package_m5stack_index.json
RP2040https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
Seeed SAMDhttps://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
Adafruithttps://adafruit.github.io/arduino-board-index/package_adafruit_index.json

Install Libraries

# Search for a library
docker-compose run --rm arduino-cli lib search "sensor"

# Install library
docker-compose run --rm arduino-cli lib install "Adafruit GFX Library"
docker-compose run --rm arduino-cli lib install "DHT sensor library"
docker-compose run --rm arduino-cli lib install "PubSubClient"

# Install specific version
docker-compose run --rm arduino-cli lib install "RadioLib@7.5.0"

# Install from GitHub (manual)
docker-compose run --rm shell -c "cd /root/.arduino15/libraries && git clone https://github.com/user/library.git"

Key Achievements

  • ✅ Stable server-side compilation – no GUI dependencies.
  • ✅ Multi-platform support – ESP8266, ESP32, STM32, RP2040.
  • ✅ Persistent environment – libraries and cores survive container rebuilds.
  • ✅ Fast compilation – no IDE overhead, direct CLI.
  • ✅ Automated setup – one-command installation of all dependencies.
  • ✅ Reproducible builds – same result every time.
  • ✅ Zero manual configuration – everything is Dockerized.

Technology Stack

CategoryTechnology
Base OSUbuntu 22.04 (Docker)
Arduino CLIv0.34+
PlatformsESP8266, ESP32, STM32, RP2040
Upload Toolesptool.py
LibrariesHeltec, RadioLib, HotButton, SSD1306
OrchestrationDocker Compose
StoragePersistent volume (./data/.arduino15)


This Arduino server-side compiler is part of my broader Embedded Systems Engineering practice. For a detailed technical walkthrough or custom CI/CD integration, feel free to reach out.

← Previous Post
Raspberry Pi – Hotspot Music Server with GPIO Controls
Next Post →
Embedded Systems – Radxa Zero 3W Setup & Docker Environment

Related Articles