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
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 Family | URL |
|---|---|
| ESP8266 | https://arduino.esp8266.com/stable/package_esp8266com_index.json |
| ESP32 | https://espressif.github.io/arduino-esp32/package_esp32_index.json |
| STM32 | https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json |
| M5Stack | https://m5stack.com/resource/package_m5stack_index.json |
| RP2040 | https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json |
| Seeed SAMD | https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json |
| Adafruit | https://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
| Category | Technology |
|---|---|
| Base OS | Ubuntu 22.04 (Docker) |
| Arduino CLI | v0.34+ |
| Platforms | ESP8266, ESP32, STM32, RP2040 |
| Upload Tool | esptool.py |
| Libraries | Heltec, RadioLib, HotButton, SSD1306 |
| Orchestration | Docker Compose |
| Storage | Persistent volume (./data/.arduino15) |
Related Projects
- Embedded Systems Engineering – Main embedded systems page.
- Embedded Systems – Docker-Powered Development Environment – Docker development environment.
- Orange Pi One Automation Platform – ARM-based automation.
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.