1002 lines
39 KiB
Markdown
1002 lines
39 KiB
Markdown
# Velxio Docker Infrastructure
|
|
|
|
Complete documentation of the Docker build system, CI/CD pipelines, multi-architecture support, and deployment configuration for the Velxio project.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Architecture Diagram](#architecture-diagram)
|
|
3. [Dockerfile.standalone — Multi-Stage Build](#dockerfilestandalone--multi-stage-build)
|
|
- [Stage 0: qemu-provider](#stage-0-qemu-provider)
|
|
- [Stage 0.5: espidf-builder](#stage-05-espidf-builder)
|
|
- [Stage 1: frontend-builder](#stage-1-frontend-builder)
|
|
- [Stage 2: Final Production Image](#stage-2-final-production-image)
|
|
4. [Multi-Architecture Support (amd64 + arm64)](#multi-architecture-support-amd64--arm64)
|
|
- [How TARGETARCH Works](#how-targetarch-works)
|
|
- [Architecture-Specific Binaries](#architecture-specific-binaries)
|
|
- [ESP-IDF on ARM64](#esp-idf-on-arm64)
|
|
5. [QEMU ESP32 Build Pipeline](#qemu-esp32-build-pipeline)
|
|
- [build-libqemu.yml Workflow](#build-libqemuyml-workflow)
|
|
- [Matrix Strategy](#matrix-strategy)
|
|
- [libiconv Stub Workaround](#libiconv-stub-workaround)
|
|
- [Artifact Upload to GitHub Release](#artifact-upload-to-github-release)
|
|
6. [Docker Publish CI/CD Pipeline](#docker-publish-cicd-pipeline)
|
|
- [docker-publish.yml Workflow](#docker-publishyml-workflow)
|
|
- [Multi-Platform Build with Buildx](#multi-platform-build-with-buildx)
|
|
- [Registry Configuration (GHCR + Docker Hub)](#registry-configuration-ghcr--docker-hub)
|
|
- [Build Caching with GitHub Actions Cache](#build-caching-with-github-actions-cache)
|
|
- [SEO Ping and Docker Hub Description](#seo-ping-and-docker-hub-description)
|
|
7. [Entrypoint Script](#entrypoint-script)
|
|
- [arduino-cli Initialization](#arduino-cli-initialization)
|
|
- [ESP-IDF Environment Sourcing](#esp-idf-environment-sourcing)
|
|
- [Service Startup](#service-startup)
|
|
8. [Nginx Reverse Proxy](#nginx-reverse-proxy)
|
|
- [API Proxy Configuration](#api-proxy-configuration)
|
|
- [WebSocket Support](#websocket-support)
|
|
- [SPA Routing](#spa-routing)
|
|
- [Static Asset Caching](#static-asset-caching)
|
|
- [SEO Configuration](#seo-configuration)
|
|
- [Gzip Compression](#gzip-compression)
|
|
- [Security Headers](#security-headers)
|
|
9. [Docker Compose](#docker-compose)
|
|
- [Development (docker-compose.yml)](#development-docker-composeyml)
|
|
- [Production deployment](#production-deployment)
|
|
- [Environment Variables](#environment-variables)
|
|
- [Volumes](#volumes)
|
|
- [Health Checks](#health-checks)
|
|
10. [Environment Variables Reference](#environment-variables-reference)
|
|
11. [Quick Start Guide](#quick-start-guide)
|
|
12. [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Velxio uses a **multi-stage Docker build** (`Dockerfile.standalone`) that produces a single, self-contained image capable of:
|
|
|
|
- Serving the React frontend via Nginx
|
|
- Running the FastAPI backend via Uvicorn
|
|
- Compiling Arduino sketches using `arduino-cli` (AVR, RP2040)
|
|
- Compiling ESP32 sketches using **ESP-IDF 4.4.7** with Arduino-as-component
|
|
- Emulating ESP32 (Xtensa) and ESP32-C3 (RISC-V) via **pre-built QEMU shared libraries**
|
|
- Running on both **x86_64 (amd64)** and **Apple Silicon / ARM64** hosts
|
|
|
|
The image is published to two registries:
|
|
- **GitHub Container Registry (GHCR):** `ghcr.io/davidmonterocrespo24/velxio`
|
|
- **Docker Hub:** `docker.io/<username>/velxio`
|
|
|
|
---
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Dockerfile.standalone │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
|
|
│ │ Stage 0 │ │ Stage 0.5 │ │ Stage 1 │ │
|
|
│ │ qemu-provider│ │ espidf-builder│ │ frontend-builder │ │
|
|
│ │ │ │ │ │ │ │
|
|
│ │ Downloads │ │ Clones │ │ Clones third-party │ │
|
|
│ │ libqemu-*.so │ │ ESP-IDF 4.4.7│ │ from GitHub │ │
|
|
│ │ + ROM .bin │ │ + toolchains │ │ Builds avr8js, │ │
|
|
│ │ per TARGETARCH│ │ + Arduino │ │ rp2040js, wokwi-elems │ │
|
|
│ │ │ │ component │ │ Builds React frontend │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ └───────────┬───────────┘ │
|
|
│ │ │ │ │
|
|
│ ▼ ▼ ▼ │
|
|
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
│ │ Stage 2: Final Image (python:3.12-slim) ││
|
|
│ │ ││
|
|
│ │ /app/lib/ ← QEMU .so + ROM files ││
|
|
│ │ /opt/esp-idf/ ← ESP-IDF framework ││
|
|
│ │ /root/.espressif/ ← Cross-compiler toolchains ││
|
|
│ │ /opt/arduino-esp32/← Arduino component ││
|
|
│ │ /usr/share/nginx/html/ ← Built frontend ││
|
|
│ │ /app/app/ ← FastAPI backend ││
|
|
│ │ /app/entrypoint.sh ← Startup script ││
|
|
│ └──────────────────────────────────────────────────────────────┘│
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Dockerfile.standalone — Multi-Stage Build
|
|
|
|
The Dockerfile uses **4 stages** to minimize final image size while building all dependencies.
|
|
|
|
### Stage 0: qemu-provider
|
|
|
|
**Base image:** `ubuntu:22.04`
|
|
|
|
**Purpose:** Downloads pre-built QEMU shared libraries (`.so`) and ESP32 ROM binary files from a GitHub Release. These are the QEMU emulation libraries built from the `qemu-lcgamboa` fork that enable ESP32 and ESP32-C3 simulation.
|
|
|
|
```dockerfile
|
|
FROM ubuntu:22.04 AS qemu-provider
|
|
|
|
ARG TARGETARCH
|
|
ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
|
|
```
|
|
|
|
**Key details:**
|
|
|
|
- `TARGETARCH` is automatically injected by Docker Buildx. It resolves to `amd64` or `arm64` depending on the target platform.
|
|
- The stage first checks for local prebuilt files in `prebuilt/qemu/`. If present, those are used (useful for local development). If not present, the files are downloaded from the GitHub Release.
|
|
- **Architecture-specific files** (different binary per CPU architecture):
|
|
- `libqemu-xtensa-${TARGETARCH}.so` → saved as `libqemu-xtensa.so`
|
|
- `libqemu-riscv32-${TARGETARCH}.so` → saved as `libqemu-riscv32.so`
|
|
- **Architecture-independent files** (same binary for all architectures):
|
|
- `esp32-v3-rom.bin` — ESP32 boot ROM
|
|
- `esp32-v3-rom-app.bin` — ESP32 application ROM
|
|
- `esp32c3-rom.bin` — ESP32-C3 boot ROM
|
|
|
|
The renaming from `libqemu-xtensa-amd64.so` to `libqemu-xtensa.so` means the backend code needs no architecture-aware logic — it always loads `libqemu-xtensa.so` regardless of host architecture.
|
|
|
|
### Stage 0.5: espidf-builder
|
|
|
|
**Base image:** `ubuntu:22.04`
|
|
|
|
**Purpose:** Installs the full ESP-IDF 4.4.7 development framework with cross-compiler toolchains for ESP32 (Xtensa) and ESP32-C3 (RISC-V), plus Arduino-as-component for full Arduino API support.
|
|
|
|
```dockerfile
|
|
FROM ubuntu:22.04 AS espidf-builder
|
|
|
|
# Install ESP-IDF 4.4.7 (matches Arduino ESP32 core 2.0.17 / lcgamboa QEMU ROM)
|
|
RUN git clone -b v4.4.7 --recursive --depth=1 --shallow-submodules \
|
|
https://github.com/espressif/esp-idf.git /opt/esp-idf
|
|
|
|
# Install toolchains for esp32 (Xtensa) and esp32c3 (RISC-V) only
|
|
RUN ./install.sh esp32,esp32c3
|
|
|
|
# Arduino-as-component for full Arduino API support in ESP-IDF builds
|
|
RUN git clone --branch 2.0.17 --depth=1 --recursive --shallow-submodules \
|
|
https://github.com/espressif/arduino-esp32.git /opt/arduino-esp32
|
|
```
|
|
|
|
**Key details:**
|
|
|
|
- **ESP-IDF version 4.4.7** is pinned because it matches the QEMU ROM binaries built from the lcgamboa fork. Newer ESP-IDF versions (5.x) are **not compatible** with the ROM images.
|
|
- **Arduino ESP32 core 2.0.17** matches IDF 4.4.x. The 3.x series uses IDF 5.x and is incompatible.
|
|
- `install.sh esp32,esp32c3` downloads only the Xtensa and RISC-V toolchains (not ESP32-S2, S3, etc.) to reduce image size.
|
|
- ESP-IDF's `install.sh` **auto-detects host architecture** and downloads the correct native toolchain (x86_64 or aarch64). No special handling needed for ARM64.
|
|
- The `.git` directory, docs, and examples are removed after installation to reduce size. Downloaded `.tar.*` archives in `.espressif` are also cleaned up.
|
|
|
|
**Important note on ARM64 builds:** When Docker Buildx builds the arm64 variant on an amd64 CI runner, it uses QEMU user-mode emulation. This makes the ESP-IDF stage **very slow** (~30-60 minutes) but works correctly. The GitHub Actions cache (`cache-from: type=gha`) ensures this only happens once.
|
|
|
|
### Stage 1: frontend-builder
|
|
|
|
**Base image:** `node:20`
|
|
|
|
**Purpose:** Builds the frontend React application and all third-party dependencies.
|
|
|
|
```dockerfile
|
|
FROM node:20 AS frontend-builder
|
|
|
|
# Clone third-party fresh from upstream (avoids stale submodule pointers)
|
|
RUN git clone --depth=1 https://github.com/wokwi/avr8js.git third-party/avr8js \
|
|
&& git clone --depth=1 https://github.com/wokwi/rp2040js.git third-party/rp2040js \
|
|
&& git clone --depth=1 https://github.com/wokwi/wokwi-elements.git third-party/wokwi-elements \
|
|
&& git clone --depth=1 https://github.com/wokwi/wokwi-boards.git third-party/wokwi-boards
|
|
```
|
|
|
|
**Why clone instead of COPY?**
|
|
|
|
The git submodule pointers in this repo for `rp2040js` and `wokwi-elements` are stale — they point to very old commits that predate `package.json`. Cloning fresh from GitHub HEAD ensures we get working, up-to-date versions. This is also why the GitHub Actions workflow does **not** use `submodules: recursive` in the checkout step.
|
|
|
|
**Build order:**
|
|
1. `avr8js` — `npm install && npm run build`
|
|
2. `rp2040js` — `npm install && npm run build`
|
|
3. `wokwi-elements` — `npm install && npm run build`
|
|
4. Frontend — `npm install && npm run build:docker`
|
|
|
|
**`build:docker` vs `build`:** The `build:docker` script runs `vite build` only (no `tsc -b` type-checking). This is intentional because there are known pre-existing TypeScript errors (wokwi-elements JSX custom element types, `@monaco-editor/react` compatibility with React 19) that don't affect runtime behavior.
|
|
|
|
### Stage 2: Final Production Image
|
|
|
|
**Base image:** `python:3.12-slim`
|
|
|
|
**Purpose:** The final, deployable image that contains everything needed to run Velxio.
|
|
|
|
**System packages installed:**
|
|
- `curl`, `ca-certificates` — HTTP requests, SSL
|
|
- `nginx` — Reverse proxy / static file server
|
|
- `libglib2.0-0`, `libgcrypt20`, `libslirp0`, `libpixman-1-0`, `libfdt1` — Runtime dependencies for QEMU shared libraries
|
|
- `cmake`, `ninja-build` — Required by ESP-IDF builds
|
|
- `libusb-1.0-0` — Required by ESP-IDF
|
|
- `git` — Required by ESP-IDF component management
|
|
- `packaging` (Python) — Required by ESP-IDF Python tools
|
|
|
|
**Installed tools:**
|
|
- `arduino-cli` — Downloaded and installed into `/usr/local/bin`
|
|
- Python dependencies from `backend/requirements.txt`
|
|
- ESP-IDF Python dependencies (with `esp-windows-curses` filtered out)
|
|
|
|
**Files copied from builder stages:**
|
|
| Source | Destination | Purpose |
|
|
|--------|-------------|---------|
|
|
| `frontend-builder:/app/frontend/dist` | `/usr/share/nginx/html` | Built frontend assets |
|
|
| `qemu-provider:/qemu/` | `/app/lib/` | QEMU .so + ROM files |
|
|
| `espidf-builder:/opt/esp-idf` | `/opt/esp-idf` | ESP-IDF framework |
|
|
| `espidf-builder:/root/.espressif` | `/root/.espressif` | Cross-compiler toolchains |
|
|
| `espidf-builder:/opt/arduino-esp32` | `/opt/arduino-esp32` | Arduino component for ESP-IDF |
|
|
|
|
**Environment variables set in the image:**
|
|
```dockerfile
|
|
ENV QEMU_ESP32_LIB=/app/lib/libqemu-xtensa.so
|
|
ENV QEMU_RISCV32_LIB=/app/lib/libqemu-riscv32.so
|
|
ENV IDF_PATH=/opt/esp-idf
|
|
ENV IDF_TOOLS_PATH=/root/.espressif
|
|
ENV ARDUINO_ESP32_PATH=/opt/arduino-esp32
|
|
```
|
|
|
|
**Entrypoint:** `/app/entrypoint.sh` (with CRLF→LF conversion for Windows compatibility)
|
|
|
|
**Exposed port:** `80` (Nginx)
|
|
|
|
---
|
|
|
|
## Multi-Architecture Support (amd64 + arm64)
|
|
|
|
### How TARGETARCH Works
|
|
|
|
Docker Buildx automatically injects the `TARGETARCH` build argument when building multi-platform images. Its value depends on the target platform:
|
|
|
|
| Platform | TARGETARCH |
|
|
|----------|-----------|
|
|
| `linux/amd64` (x86_64, Intel/AMD) | `amd64` |
|
|
| `linux/arm64` (aarch64, Apple Silicon, AWS Graviton) | `arm64` |
|
|
|
|
This is used in Stage 0 (qemu-provider) to download the correct architecture-specific QEMU shared library:
|
|
|
|
```dockerfile
|
|
ARG TARGETARCH
|
|
# Downloads libqemu-xtensa-amd64.so or libqemu-xtensa-arm64.so
|
|
# and saves it as libqemu-xtensa.so
|
|
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/${base}-${TARGETARCH}.so"
|
|
```
|
|
|
|
### Architecture-Specific Binaries
|
|
|
|
The following files differ per architecture:
|
|
|
|
| File | amd64 | arm64 |
|
|
|------|-------|-------|
|
|
| `libqemu-xtensa.so` | Built on x86_64 Ubuntu 20.04 | Built on aarch64 Ubuntu 22.04 |
|
|
| `libqemu-riscv32.so` | Built on x86_64 Ubuntu 20.04 | Built on aarch64 Ubuntu 22.04 |
|
|
|
|
The following files are **architecture-independent** (same binary for both):
|
|
|
|
| File | Description |
|
|
|------|-------------|
|
|
| `esp32-v3-rom.bin` | ESP32 boot ROM |
|
|
| `esp32-v3-rom-app.bin` | ESP32 application ROM |
|
|
| `esp32c3-rom.bin` | ESP32-C3 boot ROM |
|
|
|
|
### ESP-IDF on ARM64
|
|
|
|
ESP-IDF's `install.sh` automatically detects the host architecture and downloads native toolchains:
|
|
- On amd64: downloads `xtensa-esp32-elf-*-linux-amd64.tar.gz`
|
|
- On arm64: downloads `xtensa-esp32-elf-*-linux-arm64.tar.gz`
|
|
|
|
No special handling is needed in the Dockerfile. However, when Buildx is building the arm64 image on an amd64 runner (which is the case in GitHub Actions), it uses QEMU user-mode emulation to run the arm64 container. This makes:
|
|
- `install.sh` very slow (downloading + extracting under emulation)
|
|
- `pip install` slow
|
|
- The overall arm64 build significantly longer than amd64
|
|
|
|
The GitHub Actions cache (`cache-from: type=gha, cache-to: type=gha,mode=max`) ensures that once the arm64 layers are built, they are cached and reused on subsequent builds.
|
|
|
|
---
|
|
|
|
## QEMU ESP32 Build Pipeline
|
|
|
|
### build-libqemu.yml Workflow
|
|
|
|
**Location:** `third-party/qemu-lcgamboa/.github/workflows/build-libqemu.yml`
|
|
|
|
**Triggers:**
|
|
- Push to the `picsimlab-esp32` branch
|
|
- Manual dispatch (`workflow_dispatch`)
|
|
|
|
This workflow compiles the QEMU shared libraries from the lcgamboa fork (a modified QEMU with ESP32/ESP32-C3 machine emulation) and uploads them as GitHub Release assets to the main Velxio repository.
|
|
|
|
### Matrix Strategy
|
|
|
|
The workflow uses a matrix strategy to build natively on two different architectures:
|
|
|
|
```yaml
|
|
strategy:
|
|
matrix:
|
|
include:
|
|
- runner: ubuntu-22.04
|
|
arch: amd64
|
|
container: ubuntu:20.04
|
|
- runner: ubuntu-24.04-arm
|
|
arch: arm64
|
|
container: ubuntu:22.04
|
|
```
|
|
|
|
**Why different containers?**
|
|
|
|
- **amd64** uses `ubuntu:20.04` for maximum glibc compatibility (glibc 2.31). The resulting `.so` will work on any Linux with glibc >= 2.31.
|
|
- **arm64** uses `ubuntu:22.04` because GitHub's `ubuntu-24.04-arm` runners are relatively new and `ubuntu:20.04` arm64 images have occasional package availability issues. glibc 2.35 is used, which is still compatible with the final Docker image (Debian Bookworm, glibc 2.36).
|
|
|
|
**Why native ARM64 runners?**
|
|
|
|
QEMU itself is a large C project. Cross-compiling or building under QEMU user-mode emulation would be extremely slow (hours). Using GitHub's native `ubuntu-24.04-arm` runners gives native ARM64 build speed (~15-20 minutes).
|
|
|
|
### libiconv Stub Workaround
|
|
|
|
QEMU's configure script adds `-liconv` to the linker flags. On Linux, iconv is part of glibc — there is no separate `libiconv` package. To satisfy the linker without installing a non-existent library:
|
|
|
|
```bash
|
|
LIBDIR=$(dpkg-architecture -q DEB_HOST_MULTIARCH 2>/dev/null || echo "$(uname -m)-linux-gnu")
|
|
ar rcs /usr/lib/${LIBDIR}/libiconv.a
|
|
```
|
|
|
|
This creates an empty static archive `libiconv.a` in the architecture-correct library directory. The linker finds it, sees no symbols (none are needed since glibc provides iconv), and is satisfied.
|
|
|
|
The `dpkg-architecture` command returns the multiarch triplet (e.g., `x86_64-linux-gnu` or `aarch64-linux-gnu`), ensuring the stub is placed in the correct directory for each architecture.
|
|
|
|
### Artifact Upload to GitHub Release
|
|
|
|
The workflow has two jobs:
|
|
|
|
1. **`build`** (runs on both amd64 and arm64):
|
|
- Compiles `libqemu-xtensa.so` and `libqemu-riscv32.so`
|
|
- Renames with architecture suffix: `libqemu-xtensa-amd64.so`, `libqemu-xtensa-arm64.so`
|
|
- ROM files are only collected from the amd64 job (they're architecture-independent)
|
|
- Uploads as GitHub Actions artifacts
|
|
|
|
2. **`upload-release`** (runs after both build jobs complete):
|
|
- Downloads both artifact archives
|
|
- Uploads all files to the `qemu-prebuilt` tag on the `davidmonterocrespo24/velxio` repository
|
|
- Uses `--clobber` to overwrite existing files if the release already exists
|
|
- Requires the `VELXIO_RELEASE_TOKEN` secret (a PAT with `contents:write` on the velxio repo)
|
|
|
|
**Release structure at `github.com/davidmonterocrespo24/velxio/releases/tag/qemu-prebuilt`:**
|
|
```
|
|
libqemu-xtensa-amd64.so
|
|
libqemu-xtensa-arm64.so
|
|
libqemu-riscv32-amd64.so
|
|
libqemu-riscv32-arm64.so
|
|
esp32-v3-rom.bin
|
|
esp32-v3-rom-app.bin
|
|
esp32c3-rom.bin
|
|
```
|
|
|
|
---
|
|
|
|
## Docker Publish CI/CD Pipeline
|
|
|
|
### docker-publish.yml Workflow
|
|
|
|
**Location:** `.github/workflows/docker-publish.yml`
|
|
|
|
**Trigger:** Push to the `master` branch.
|
|
|
|
This workflow builds the multi-platform Docker image and publishes it to both GHCR and Docker Hub.
|
|
|
|
### Multi-Platform Build with Buildx
|
|
|
|
The workflow sets up Docker Buildx with QEMU support for cross-platform building:
|
|
|
|
```yaml
|
|
- name: Set up QEMU (for multi-arch builds)
|
|
uses: docker/setup-qemu-action@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Build and push Docker image
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: Dockerfile.standalone
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
build-args: |
|
|
ESPIDF_IMAGE=ghcr.io/davidmonterocrespo24/velxio-espidf-toolchain:latest
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
```
|
|
|
|
**How multi-platform build works:**
|
|
|
|
1. Buildx creates two parallel build contexts — one for `linux/amd64` and one for `linux/arm64`
|
|
2. For the native architecture (amd64 on GitHub's runners), Docker runs natively
|
|
3. For the foreign architecture (arm64 on amd64 runners), Docker uses QEMU user-mode emulation via `setup-qemu-action`
|
|
4. Each stage in the Dockerfile is built separately for each architecture
|
|
5. The resulting images are combined into a **multi-arch manifest** and pushed as a single tag
|
|
|
|
When a user runs `docker pull ghcr.io/davidmonterocrespo24/velxio:master`, Docker automatically selects the correct architecture variant.
|
|
|
|
### Registry Configuration (GHCR + Docker Hub)
|
|
|
|
The workflow pushes to two registries simultaneously:
|
|
|
|
**GitHub Container Registry (GHCR):**
|
|
```yaml
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
```
|
|
- Uses the automatic `GITHUB_TOKEN` — no extra secrets needed
|
|
- Image: `ghcr.io/davidmonterocrespo24/velxio`
|
|
|
|
**Docker Hub:**
|
|
```yaml
|
|
- name: Log in to Docker Hub
|
|
uses: docker/login-action@v3
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
```
|
|
- Requires `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets
|
|
- Image: `docker.io/<username>/velxio`
|
|
|
|
The `docker/metadata-action` generates tags for both registries:
|
|
```yaml
|
|
images: |
|
|
ghcr.io/${{ env.IMAGE_NAME }}
|
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/velxio
|
|
```
|
|
|
|
### Build Caching with GitHub Actions Cache
|
|
|
|
```yaml
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
```
|
|
|
|
This uses GitHub Actions' built-in cache backend for Docker layer caching:
|
|
- `type=gha` — GitHub Actions cache (up to 10GB per repo)
|
|
- `mode=max` — Cache all layers, not just the final stage
|
|
|
|
This is critical for ARM64 builds because the ESP-IDF stage is very slow under QEMU emulation. Once cached, subsequent builds skip the heavy stages entirely.
|
|
|
|
### SEO Ping and Docker Hub Description
|
|
|
|
After a successful build:
|
|
|
|
```yaml
|
|
- name: Ping search engines with sitemap
|
|
run: |
|
|
curl -s "https://www.google.com/ping?sitemap=https%3A%2F%2Fvelxio.dev%2Fsitemap.xml"
|
|
curl -s "https://www.bing.com/ping?sitemap=https%3A%2F%2Fvelxio.dev%2Fsitemap.xml"
|
|
|
|
- name: Update Docker Hub description
|
|
uses: peter-evans/dockerhub-description@v4
|
|
with:
|
|
repository: ${{ secrets.DOCKERHUB_USERNAME }}/velxio
|
|
short-description: "Local, open-source Arduino emulator..."
|
|
readme-filepath: ./README.md
|
|
```
|
|
|
|
The Docker Hub description is automatically updated from the repository's `README.md` on every push.
|
|
|
|
---
|
|
|
|
## Entrypoint Script
|
|
|
|
**Location:** `docker/entrypoint.sh`
|
|
|
|
The entrypoint script runs when the container starts. It initializes development tools and launches the application services.
|
|
|
|
### arduino-cli Initialization
|
|
|
|
```bash
|
|
# First-time setup: create config and add board manager URLs
|
|
if [ ! -f /root/.arduino15/arduino-cli.yaml ]; then
|
|
arduino-cli config init
|
|
arduino-cli config add board_manager.additional_urls \
|
|
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
|
|
arduino-cli config add board_manager.additional_urls \
|
|
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
|
|
fi
|
|
|
|
# Install board cores
|
|
arduino-cli core update-index
|
|
arduino-cli core install arduino:avr # Arduino Uno, Mega, Nano
|
|
arduino-cli core install rp2040:rp2040 # Raspberry Pi Pico
|
|
```
|
|
|
|
**Persistence:** The `/root/.arduino15` directory is mounted as a Docker volume (`arduino-libs`). This means:
|
|
- First boot: downloads and installs board cores (~500MB) — takes a few minutes
|
|
- Subsequent boots: skips installation, starts immediately
|
|
|
|
### ESP-IDF Environment Sourcing
|
|
|
|
```bash
|
|
if [ -f /opt/esp-idf/export.sh ]; then
|
|
. /opt/esp-idf/export.sh
|
|
echo "ESP-IDF $(cat /opt/esp-idf/version.txt) ready"
|
|
else
|
|
# Fallback to arduino-cli ESP32 core
|
|
arduino-cli core install esp32:esp32@2.0.17
|
|
fi
|
|
```
|
|
|
|
ESP-IDF's `export.sh` adds the cross-compiler toolchains to `$PATH` and sets up the build environment. Without sourcing this script, ESP32 compilation would fail.
|
|
|
|
**Version pinning:** The fallback installs `esp32:esp32@2.0.17` specifically. This is critical because:
|
|
- Version 2.0.17 uses IDF 4.4.x internally, matching the QEMU ROM binaries
|
|
- Version 3.x uses IDF 5.x, which is **incompatible** with the QEMU ROM images
|
|
- Using the wrong version causes boot failures in emulation
|
|
|
|
### Service Startup
|
|
|
|
```bash
|
|
# Start FastAPI backend on port 8001 (background)
|
|
uvicorn app.main:app --host 127.0.0.1 --port 8001 &
|
|
|
|
# Wait for backend to initialize
|
|
sleep 2
|
|
|
|
# Start Nginx on port 80 (foreground — keeps container alive)
|
|
exec nginx -g "daemon off;"
|
|
```
|
|
|
|
The backend binds to `127.0.0.1:8001` (localhost only — not exposed to the network). Nginx on port 80 is the only externally-accessible service and proxies API requests to the backend.
|
|
|
|
Using `exec nginx` replaces the shell process with Nginx, making it PID 1. This ensures proper signal handling — when Docker sends SIGTERM (on `docker stop`), Nginx receives it directly and shuts down gracefully.
|
|
|
|
---
|
|
|
|
## Nginx Reverse Proxy
|
|
|
|
**Location:** `docker/nginx.conf`
|
|
|
|
### API Proxy Configuration
|
|
|
|
```nginx
|
|
location /api/ {
|
|
proxy_pass http://127.0.0.1:8001/api/;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s; # 5 minutes — compilation can be slow
|
|
proxy_connect_timeout 75s;
|
|
}
|
|
```
|
|
|
|
All `/api/*` requests are proxied to the FastAPI backend. The 300-second read timeout accommodates ESP32 compilation, which can take several minutes (especially on first build when ESP-IDF initializes the build cache).
|
|
|
|
### WebSocket Support
|
|
|
|
```nginx
|
|
location /api/simulation/ws/ {
|
|
proxy_pass http://127.0.0.1:8001/api/simulation/ws/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_read_timeout 86400s; # 24 hours
|
|
proxy_send_timeout 86400s;
|
|
}
|
|
```
|
|
|
|
WebSocket connections are used for real-time ESP32 simulation communication. The 24-hour timeout ensures long-running simulation sessions aren't terminated. This location block **must come before** the generic `/api/` block so Nginx matches it with higher priority.
|
|
|
|
### SPA Routing
|
|
|
|
```nginx
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
```
|
|
|
|
This is the standard single-page application (SPA) routing pattern. For any URL that doesn't match a static file or another location block, Nginx serves `index.html` and lets React Router handle the routing client-side.
|
|
|
|
### Static Asset Caching
|
|
|
|
```nginx
|
|
# Content-hash assets (JS, CSS, fonts) — immutable, cache forever
|
|
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# Images — cache for 30 days
|
|
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp)$ {
|
|
expires 30d;
|
|
add_header Cache-Control "public";
|
|
}
|
|
```
|
|
|
|
Vite generates content-hashed filenames (e.g., `index-a1b2c3.js`), so JS/CSS files can be cached indefinitely — when the content changes, the hash changes and a new URL is used.
|
|
|
|
### SEO Configuration
|
|
|
|
```nginx
|
|
# Never cache sitemap/robots — crawlers always get the latest
|
|
location = /sitemap.xml {
|
|
try_files $uri =404;
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
}
|
|
|
|
location = /robots.txt {
|
|
try_files $uri =404;
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
}
|
|
```
|
|
|
|
### Gzip Compression
|
|
|
|
```nginx
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_min_length 1024;
|
|
gzip_proxied any;
|
|
gzip_types text/plain text/css text/xml text/javascript
|
|
application/javascript application/json
|
|
application/xml application/rss+xml;
|
|
```
|
|
|
|
Gzip is enabled for text-based content types. The `gzip_min_length 1024` prevents compressing very small responses where compression overhead would exceed savings.
|
|
|
|
### Security Headers
|
|
|
|
```nginx
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
```
|
|
|
|
- **X-Frame-Options:** Prevents the site from being embedded in iframes on other domains (clickjacking protection)
|
|
- **X-Content-Type-Options:** Prevents browsers from MIME-sniffing responses
|
|
- **X-XSS-Protection:** Enables browser's built-in XSS filter
|
|
- **Referrer-Policy:** Limits referrer information sent to other sites
|
|
|
|
---
|
|
|
|
## Docker Compose
|
|
|
|
### Development (docker-compose.yml)
|
|
|
|
**Location:** `docker-compose.yml`
|
|
|
|
```yaml
|
|
services:
|
|
velxio:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.standalone
|
|
container_name: velxio-dev
|
|
restart: unless-stopped
|
|
ports:
|
|
- "3080:80"
|
|
env_file:
|
|
- ./backend/.env
|
|
environment:
|
|
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
|
|
- DATA_DIR=/app/data
|
|
- IDF_PATH=/opt/esp-idf
|
|
- IDF_TOOLS_PATH=/root/.espressif
|
|
- ARDUINO_ESP32_PATH=/opt/arduino-esp32
|
|
volumes:
|
|
- ./data:/app/data
|
|
- arduino-libs:/root/.arduino15
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 90s
|
|
|
|
volumes:
|
|
arduino-libs:
|
|
```
|
|
|
|
**Usage:**
|
|
```bash
|
|
docker compose up --build # Build and start
|
|
docker compose up -d # Start in background (detached)
|
|
docker compose down # Stop and remove
|
|
docker compose logs -f velxio # Follow logs
|
|
```
|
|
|
|
**Access:** `http://localhost:3080`
|
|
|
|
### Production deployment
|
|
|
|
Production-only configuration (host nginx with HTTPS, deploy/backup scripts,
|
|
pinned upstream commit) lives in a separate repo:
|
|
**[github.com/velxio/velxio-prod](https://github.com/velxio/velxio-prod)**.
|
|
|
|
For self-hosters who don't need the velxio.dev-specific bits, the easiest
|
|
approach is the prebuilt image from the registry:
|
|
|
|
```bash
|
|
# Pull and run the pre-built image
|
|
docker run -d \
|
|
--name velxio \
|
|
-p 3080:80 \
|
|
-v velxio-data:/app/data \
|
|
-v arduino-libs:/root/.arduino15 \
|
|
ghcr.io/davidmonterocrespo24/velxio:master
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `DATABASE_URL` | `sqlite+aiosqlite:////app/data/velxio.db` | SQLAlchemy async database URL |
|
|
| `DATA_DIR` | `/app/data` | Directory for persistent data (SQLite DB) |
|
|
| `IDF_PATH` | `/opt/esp-idf` | ESP-IDF framework path |
|
|
| `IDF_TOOLS_PATH` | `/root/.espressif` | ESP-IDF cross-compiler toolchains |
|
|
| `ARDUINO_ESP32_PATH` | `/opt/arduino-esp32` | Arduino-as-component for ESP-IDF |
|
|
| `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Path to Xtensa QEMU library |
|
|
| `QEMU_RISCV32_LIB` | `/app/lib/libqemu-riscv32.so` | Path to RISC-V QEMU library |
|
|
| `SECRET_KEY` | (from `.env`) | JWT signing key |
|
|
| `GOOGLE_CLIENT_ID` | (from `.env`) | Google OAuth client ID |
|
|
| `GOOGLE_CLIENT_SECRET` | (from `.env`) | Google OAuth client secret |
|
|
|
|
### Volumes
|
|
|
|
| Volume | Mount Point | Purpose |
|
|
|--------|-------------|---------|
|
|
| `./data` (bind mount) | `/app/data` | SQLite database file (`velxio.db`) |
|
|
| `arduino-libs` (named volume) | `/root/.arduino15` | arduino-cli config, board cores, libraries |
|
|
|
|
The bind mount for `./data` allows easy database backup and inspection from the host. The named volume for `arduino-libs` persists board core installations across container restarts.
|
|
|
|
### Health Checks
|
|
|
|
```yaml
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 90s
|
|
```
|
|
|
|
- **`start_period: 90s`** — Gives the container 90 seconds to start before health checks begin counting failures. This accounts for first-boot arduino-cli core installation which can take over a minute.
|
|
- The `/health` endpoint is proxied by Nginx to FastAPI's `/health` endpoint, verifying both services are running.
|
|
|
|
---
|
|
|
|
## Environment Variables Reference
|
|
|
|
### Set in Dockerfile (build-time)
|
|
|
|
| Variable | Value | Set In |
|
|
|----------|-------|--------|
|
|
| `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Stage 2 |
|
|
| `QEMU_RISCV32_LIB` | `/app/lib/libqemu-riscv32.so` | Stage 2 |
|
|
| `IDF_PATH` | `/opt/esp-idf` | Stage 2 |
|
|
| `IDF_TOOLS_PATH` | `/root/.espressif` | Stage 2 |
|
|
| `ARDUINO_ESP32_PATH` | `/opt/arduino-esp32` | Stage 2 |
|
|
|
|
### Set at runtime (docker-compose / docker run)
|
|
|
|
| Variable | Required | Description |
|
|
|----------|----------|-------------|
|
|
| `DATABASE_URL` | Yes | SQLAlchemy async connection string |
|
|
| `DATA_DIR` | Yes | Data directory path |
|
|
| `SECRET_KEY` | Yes | JWT token signing key |
|
|
| `GOOGLE_CLIENT_ID` | No | For Google OAuth |
|
|
| `GOOGLE_CLIENT_SECRET` | No | For Google OAuth |
|
|
|
|
### Build arguments
|
|
|
|
| Arg | Default | Description |
|
|
|-----|---------|-------------|
|
|
| `TARGETARCH` | (auto-injected by Buildx) | Target architecture: `amd64` or `arm64` |
|
|
| `QEMU_RELEASE_URL` | `https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt` | URL prefix for QEMU binary downloads |
|
|
| `ESPIDF_IMAGE` | (unused, legacy) | Was used for external ESP-IDF image reference |
|
|
|
|
---
|
|
|
|
## Quick Start Guide
|
|
|
|
### Run from registry (recommended)
|
|
|
|
```bash
|
|
# Pull and run (auto-selects amd64 or arm64)
|
|
docker run -d \
|
|
--name velxio \
|
|
-p 3080:80 \
|
|
-v velxio-data:/app/data \
|
|
-v velxio-arduino:/root/.arduino15 \
|
|
ghcr.io/davidmonterocrespo24/velxio:master
|
|
|
|
# Open in browser
|
|
open http://localhost:3080
|
|
|
|
# First boot takes ~2 minutes (downloading arduino board cores)
|
|
# Check progress:
|
|
docker logs -f velxio
|
|
```
|
|
|
|
### Build locally
|
|
|
|
```bash
|
|
# Clone the repository
|
|
git clone https://github.com/davidmonterocrespo24/velxio.git
|
|
cd velxio
|
|
|
|
# Build and run with docker compose
|
|
docker compose up --build
|
|
|
|
# Or build just the image
|
|
docker build -f Dockerfile.standalone -t velxio .
|
|
docker run -d -p 3080:80 velxio
|
|
```
|
|
|
|
### Build for a specific architecture
|
|
|
|
```bash
|
|
# Build for ARM64 only (e.g., on Apple Silicon)
|
|
docker buildx build \
|
|
--platform linux/arm64 \
|
|
-f Dockerfile.standalone \
|
|
-t velxio:arm64 \
|
|
--load .
|
|
|
|
# Build for both architectures (requires push to registry)
|
|
docker buildx build \
|
|
--platform linux/amd64,linux/arm64 \
|
|
-f Dockerfile.standalone \
|
|
-t ghcr.io/user/velxio:latest \
|
|
--push .
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "no matching manifest for linux/arm64/v8"
|
|
|
|
**Problem:** Running `docker pull` on Apple Silicon Mac fails because the image was built for amd64 only.
|
|
|
|
**Solution:** The multi-platform build was added in the `docker-publish.yml` workflow with `platforms: linux/amd64,linux/arm64`. Ensure:
|
|
1. The `build-libqemu.yml` workflow has run successfully for both architectures (check that `libqemu-xtensa-arm64.so` exists in the `qemu-prebuilt` release)
|
|
2. The `docker-publish.yml` workflow has run after the QEMU ARM64 binaries were uploaded
|
|
3. The `docker/setup-qemu-action@v3` step is present in the workflow
|
|
|
|
### CRLF line ending issues in entrypoint.sh
|
|
|
|
**Problem:** When developing on Windows, `entrypoint.sh` may have CRLF line endings, causing `/bin/bash^M: bad interpreter`.
|
|
|
|
**Solution:** The Dockerfile includes a fix:
|
|
```dockerfile
|
|
RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh
|
|
```
|
|
|
|
This strips carriage returns from the file inside the container. No git configuration changes needed.
|
|
|
|
### ESP-IDF compilation fails with "command not found"
|
|
|
|
**Problem:** ESP-IDF cross-compilers (`xtensa-esp32-elf-gcc`) not found when compiling ESP32 sketches.
|
|
|
|
**Cause:** `export.sh` was not sourced, so the toolchains aren't in `$PATH`.
|
|
|
|
**Solution:** The entrypoint script sources ESP-IDF on startup:
|
|
```bash
|
|
. /opt/esp-idf/export.sh
|
|
```
|
|
|
|
If running manually inside the container, source it yourself:
|
|
```bash
|
|
docker exec -it velxio bash
|
|
source /opt/esp-idf/export.sh
|
|
```
|
|
|
|
### QEMU .so fails to load (missing shared libraries)
|
|
|
|
**Problem:** `ctypes.cdll.LoadLibrary` fails with missing dependencies.
|
|
|
|
**Cause:** The final image is missing runtime dependencies for the QEMU shared library.
|
|
|
|
**Solution:** The following packages are installed in Stage 2:
|
|
```
|
|
libglib2.0-0 libgcrypt20 libslirp0 libpixman-1-0 libfdt1
|
|
```
|
|
|
|
To debug which dependencies are missing:
|
|
```bash
|
|
docker exec -it velxio bash
|
|
ldd /app/lib/libqemu-xtensa.so
|
|
# Look for "not found" entries
|
|
```
|
|
|
|
### First boot is very slow
|
|
|
|
**Problem:** Container takes several minutes to become ready on first start.
|
|
|
|
**Cause:** The entrypoint script downloads and installs arduino-cli board cores:
|
|
- `arduino:avr` — ~150MB
|
|
- `rp2040:rp2040` — ~300MB
|
|
|
|
**Solution:** This is expected on first boot. The `arduino-libs` volume persists these installations, so subsequent starts are fast. Use the health check's `start_period: 90s` to give the container enough time.
|
|
|
|
### Build cache invalidation
|
|
|
|
**Problem:** Docker rebuild downloads everything from scratch despite no code changes.
|
|
|
|
**Cause:** The `COPY` instruction invalidates the cache if any file in the context changes.
|
|
|
|
**Solution:** The Dockerfile is structured to maximize cache reuse:
|
|
1. System packages (rarely change) — cached
|
|
2. `requirements.txt` copy + `pip install` — only invalidated when dependencies change
|
|
3. Application code copy — invalidated on every push
|
|
|
|
For the GitHub Actions cache:
|
|
```yaml
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
```
|
|
|
|
The `mode=max` caches all intermediate layers, not just the final layer. This is important for the ESP-IDF stage which is very slow to build from scratch.
|
|
|
|
### Docker Hub token permissions
|
|
|
|
**Problem:** `docker-publish.yml` fails at the Docker Hub login step.
|
|
|
|
**Solution:** Create a Docker Hub access token:
|
|
1. Go to Docker Hub → Account Settings → Security → New Access Token
|
|
2. Set permissions to Read & Write
|
|
3. Add as GitHub repository secrets:
|
|
- `DOCKERHUB_USERNAME` — Your Docker Hub username
|
|
- `DOCKERHUB_TOKEN` — The access token
|
|
|
|
### QEMU build fails for ARM64
|
|
|
|
**Problem:** The `build-libqemu.yml` workflow fails on the `ubuntu-24.04-arm` runner.
|
|
|
|
**Possible causes:**
|
|
1. **Runner not available:** GitHub's ARM64 runners (`ubuntu-24.04-arm`) require a GitHub plan that supports them. Check your repository's Actions settings.
|
|
2. **Package differences:** The ARM64 container uses `ubuntu:22.04` which may have slightly different package versions. Check the build logs for missing dependencies.
|
|
3. **libiconv stub path:** The `dpkg-architecture` command must be available. It's part of `dpkg-dev` which may need to be installed explicitly in the container.
|
|
|
|
### WebSocket connection drops
|
|
|
|
**Problem:** ESP32 simulation WebSocket disconnects after a period of inactivity.
|
|
|
|
**Cause:** Default Nginx proxy timeouts.
|
|
|
|
**Solution:** The Nginx config sets 24-hour timeouts for WebSocket connections:
|
|
```nginx
|
|
proxy_read_timeout 86400s;
|
|
proxy_send_timeout 86400s;
|
|
```
|
|
|
|
If issues persist, check if there's a load balancer or CDN in front of Nginx that has its own timeout settings.
|
|
|
|
### Compilation timeout
|
|
|
|
**Problem:** Arduino compilation requests time out.
|
|
|
|
**Cause:** The default Nginx `proxy_read_timeout` may be too short for ESP32 compilation, which involves the full ESP-IDF build system.
|
|
|
|
**Solution:** The Nginx config sets a 5-minute timeout for API requests:
|
|
```nginx
|
|
proxy_read_timeout 300s;
|
|
```
|
|
|
|
For ESP32 first-time compilation (cold build cache), this may still not be enough. The ESP-IDF build system caches intermediate results, so subsequent compilations are much faster.
|
|
|
|
---
|
|
|
|
## File Reference
|
|
|
|
| File | Description |
|
|
|------|-------------|
|
|
| `Dockerfile.standalone` | Multi-stage Docker build (4 stages) |
|
|
| `.github/workflows/docker-publish.yml` | CI/CD: builds + pushes multi-arch image |
|
|
| `third-party/qemu-lcgamboa/.github/workflows/build-libqemu.yml` | CI/CD: builds QEMU .so for amd64 + arm64 |
|
|
| `docker/entrypoint.sh` | Container startup script |
|
|
| `docker/nginx.conf` | Nginx reverse proxy configuration |
|
|
| `docker-compose.yml` | Self-hosting compose file (production at github.com/velxio/velxio-prod) |
|
|
| `prebuilt/qemu/` | Local QEMU prebuilt files (optional, for dev) |
|
|
| `backend/.env` | Backend environment variables (not committed) |
|