ESPHome Gitea Sync Service
A Docker-based synchronization service that bridges Gitea repositories with ESPHome device configurations. Automatically syncs YAML configuration files between your Gitea git repository and your ESPHome installation.
Features
- Bidirectional Git Sync: Sync configurations between Gitea and ESPHome
- File Watching: Automatically detect YAML changes and push to Gitea
- Gitea Webhooks: Receive push events and pull changes automatically
- REST API: Manual sync endpoints and device listing
- Docker Polling: Uses filesystem polling for reliable change detection on Docker bind mounts
- Thread-Safe: Concurrent operation handling with locks
- Archive Support: Detects when files are moved to archive folders
Architecture
This service works alongside the official ESPHome container:
- ESPHome Container: Handles all compilation and device deployment (immutable)
- Sync Service: Manages git synchronization between Gitea and ESPHome (this service)
The ESPHome container remains unchanged and simply consumes the YAML configs. This service only handles git operations.
Quick Start
1. Configure Environment Variables
Edit docker-compose.yml and set your Gitea details:
environment:
- GITEA_URL=https://your-gitea-instance.com
- GITEA_REPO=username/esphome-configs
- GITEA_TOKEN=your_gitea_access_token
- GITEA_BRANCH=main
- AUTO_PUSH=false # Set to true for automatic push on file changes
2. Start the Services
docker-compose up -d
This starts two services:
- ESPHome Dashboard:
http://localhost:6052(compilation and deployment) - Gitea Sync Service:
http://localhost:5000(git synchronization)
3. Access the Services
- ESPHome Dashboard: http://localhost:6052
- Sync API Health: http://localhost:5000/health
- Device List: http://localhost:5000/devices
4. Configure Gitea Webhook (Optional)
To automatically pull changes when you push to Gitea:
- Go to your Gitea repository → Settings → Webhooks
- Add Webhook → Gitea
- Set Target URL:
http://your-server:5000/webhook/gitea - Content Type:
application/json - Trigger On:
Push Events - Click "Add Webhook"
Environment Variables
Configure the sync service in docker-compose.yml:
| Variable | Default | Description |
|---|---|---|
ESPHOME_CONFIG_DIR |
/config |
Directory containing device configs |
DEBOUNCE_SECONDS |
5 |
Delay before triggering after file change |
USE_POLLING |
true |
Use polling for Docker compatibility (required) |
POLLING_INTERVAL |
1.0 |
Seconds between filesystem polls |
GITEA_URL |
- | URL of your Gitea instance |
GITEA_REPO |
- | Repository path (username/repo) |
GITEA_TOKEN |
- | Gitea access token (generate in user settings) |
GITEA_BRANCH |
main |
Git branch to sync |
AUTO_PUSH |
false |
Automatically push local changes to Gitea |
GIT_USER_NAME |
ESPHome Sync Service |
Git commit author name |
GIT_USER_EMAIL |
esphome-sync@localhost |
Git commit author email |
API Endpoints
Health Check
curl http://localhost:5000/health
List Devices
Lists all YAML files in the config directory:
curl http://localhost:5000/devices
Returns:
{
"devices": [
{
"name": "bedroom-light",
"config_path": "/config/bedroom-light.yaml",
"last_modified": "2026-01-13T10:30:00"
}
]
}
Gitea Webhook
Receives push events from Gitea and pulls changes:
curl -X POST http://localhost:5000/webhook/gitea \
-H "Content-Type: application/json" \
-H "X-Gitea-Event: push" \
-d '{"repository": {"full_name": "user/repo"}}'
Manual Pull from Gitea
Manually trigger a git pull:
curl -X POST http://localhost:5000/sync/pull
Manual Push to Gitea
Manually trigger a git push:
curl -X POST http://localhost:5000/sync/push \
-H "Content-Type: application/json" \
-d '{"message": "Manual sync from API"}'
File Watching
The service monitors all .yaml and .yml files in the root of the config directory.
Detected Events
- File Modified - Edit existing YAML file
- File Created - Add new YAML file
- File Deleted - Remove YAML file
- File Renamed - Rename YAML file
- File Archived - Move YAML file to subdirectory (e.g.,
archive/) - File Restored - Move YAML file from subdirectory back to root
When AUTO_PUSH=true, all these events automatically trigger a git commit and push to Gitea.
Commit Messages
- Modified/Created:
Auto-sync: device-name.yaml changed - Deleted:
Auto-sync: device-name.yaml deleted - Renamed:
Auto-sync: Renamed old-name.yaml to new-name.yaml - Archived:
Auto-sync: device-name.yaml archived - Restored:
Auto-sync: device-name.yaml restored from archive
Directory Structure
config/
├── bedroom-light.yaml # Device configs (flat structure)
├── kitchen-sensor.yaml
├── garage-door.yaml
├── .gitignore # Optional: ignore archive folder
├── archive/ # Optional: archived configs (not monitored)
│ └── old-device.yaml
└── .git/ # Git repository (managed by sync service)
Workflows
Editing in ESPHome Dashboard
- Edit YAML files in ESPHome dashboard
- File watcher detects the change
- If
AUTO_PUSH=true: Changes automatically committed and pushed to Gitea - If
AUTO_PUSH=false: Manually trigger push via API
Editing in Gitea (or git client)
- Commit and push changes to Gitea repository
- Gitea webhook triggers
/webhook/giteaendpoint - Service runs
git pullto fetch changes - ESPHome sees updated files and reloads dashboard
Manual Sync
# Pull latest changes from Gitea
curl -X POST http://localhost:5000/sync/pull
# Push local changes to Gitea
curl -X POST http://localhost:5000/sync/push \
-H "Content-Type: application/json" \
-d '{"message": "My commit message"}'
Troubleshooting
Service Won't Start
Check logs:
docker-compose logs webhook
docker-compose logs esphome
File Watcher Not Detecting Changes
Cause: inotify events don't propagate through Docker bind mounts on WSL2, Unraid, and macOS.
Solution: The service uses polling mode by default (USE_POLLING=true). Ensure this is enabled in docker-compose.yml.
Check logs for:
Using PollingObserver (interval: 1.0s) for Docker bind mount compatibility
Git Operations Failing
- Verify Gitea token has correct permissions (read/write repository)
- Check
GITEA_URL,GITEA_REPO, andGITEA_TOKENare set correctly - Check logs for git command errors:
docker-compose logs webhook | grep -i "git command"
Webhook Not Receiving Events
- Verify webhook URL is accessible from Gitea (firewall, network)
- Check Gitea webhook delivery logs (Repository → Settings → Webhooks → Recent Deliveries)
- Ensure
Content-Typeisapplication/jsonand trigger isPush Events
Changes Not Syncing to Gitea
- Check if
AUTO_PUSH=truein docker-compose.yml - Verify file is in the root of
/config/(not in subdirectory) - Check logs for file change detection:
docker-compose logs webhook | grep -i "detected"
Development
Run Locally (Without Docker)
# Install dependencies
pip install -r requirements.txt
# Set environment variables
export ESPHOME_CONFIG_DIR=/path/to/config
export DEBOUNCE_SECONDS=5
export USE_POLLING=true
export GITEA_URL=https://gitea.example.com
export GITEA_REPO=username/esphome-configs
export GITEA_TOKEN=your_token
export GITEA_BRANCH=main
export AUTO_PUSH=false
export GIT_USER_NAME="Your Name"
export GIT_USER_EMAIL="your@email.com"
# Run the service
python app.py
Build Custom Image
docker build -t esphome-gitea-sync:custom .
Testing File Watcher
Create, modify, delete, or move files in the config directory and watch the logs:
docker-compose logs -f webhook
You should see:
Detected change in /config/test.yaml
Change detected in device: test
AUTO_PUSH enabled, pushing changes to Gitea
Security Considerations
- The sync service has no authentication by default
- Gitea token is stored in environment variables (keep docker-compose.yml secure)
- Only expose port 5000 on trusted networks
- Use a reverse proxy (nginx, Traefik) with authentication for external access
- Keep
AUTO_PUSH=falseunless you trust all changes made in ESPHome dashboard - Review the
.gitignorefile to avoid committing sensitive data
Migration from Old Structure
If migrating from device-name/main.yaml to flat device-name.yaml structure:
cd config
for dir in */; do
if [ -f "${dir}main.yaml" ]; then
device_name="${dir%/}"
mv "${dir}main.yaml" "${device_name}.yaml"
rmdir "${dir}"
fi
done
git add -A
git commit -m "Migrate to flat YAML structure"
git push origin main
License
MIT License - See LICENSE file for details.