Smart irrigation system with web-based control interface, automated scheduling, and weather integration.
Split Deployment:
- Frontend (NAS): React Router 7 SSR in Docker container, proxied via custom nginx
- Backend (Pi): Node.js Express API with PostgreSQL, serial port control for valves
Browser → Nginx (NAS:80) → Docker (localhost:3000) → API (Pi:3001) → Hardware
- Stack: React 19 + React Router 7 (SSR) + Recharts + Socket.IO Client
- Deployment: Docker container (irrigation-frontend)
- Reverse Proxy: Custom nginx instance on NAS host
- Location:
/volume1/home/jonclow/irrigation/ - Access:
http://irrigation.home/orhttp://192.168.20.92/ - Auto-restart: Docker (unless-stopped), nginx (manual or cron)
- Stack: Node.js 22 (NVM) + Express + Socket.IO + PostgreSQL
- Service: systemd (
irrigation.service) - Location:
~/irrigation-controller/ - Hardware: Serial port control for 5-zone valve system
- Access:
http://192.168.20.59:3001/
All files in /volume1/home/jonclow/irrigation/:
/volume1/home/jonclow/irrigation/
├── nginx.conf # Nginx proxy configuration (persistent)
├── docker-compose.nas.yml # Docker container definition
├── start-nginx.sh # Start nginx proxy script
├── stop-nginx.sh # Stop nginx proxy script
├── README.md # Detailed documentation (on NAS)
├── nginx.pid # Nginx process ID (generated)
├── nginx-access.log # Access logs (generated)
└── nginx-error.log # Error logs (generated)
-
Disable Web Center (prevents config regeneration):
- Open ADM → Web Center → Web Server
- Uncheck "Enable the web server"
- Click Apply
-
Deploy the container:
cd ~/redmercury/irrigate ./docker-deploy-nas-local.sh
-
Start nginx:
ssh nas cd /volume1/home/jonclow/irrigation sudo ./start-nginx.sh -
(Optional) Auto-start nginx on boot:
ssh nas sudo crontab -e # Add: @reboot sleep 30 && /volume1/home/jonclow/irrigation/start-nginx.sh
# On Pi, install systemd service:
sudo cp irrigation.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable irrigation
sudo systemctl start irrigationFrom your development machine:
cd ~/redmercury/irrigate
git commit -am "Your changes"
./docker-deploy-nas-local.shWhat happens:
- Builds Docker image locally with Node 24 LTS
- Tests image locally on port 3333
- Transfers compressed image to NAS (~129MB)
- Loads image on NAS
- Restarts container
- Cleans up old dangling images
Note: Nginx continues running, only the Docker container restarts.
From your development machine:
cd ~/redmercury/irrigate
git commit -am "Your changes"
./deploy-pi-backend.shWhat happens:
- Pushes to git
- SSHs to Pi, pulls latest changes
- Installs npm dependencies
- Rebuilds native modules (serialport) for Node 22
- Restarts systemd service
# Start
ssh nas "cd /volume1/home/jonclow/irrigation && sudo ./start-nginx.sh"
# Stop
ssh nas "cd /volume1/home/jonclow/irrigation && sudo ./stop-nginx.sh"
# Restart (after config changes)
ssh nas "cd /volume1/home/jonclow/irrigation && sudo ./stop-nginx.sh && sudo ./start-nginx.sh"
# View logs
ssh nas "tail -f /volume1/home/jonclow/irrigation/nginx-access.log"
ssh nas "tail -f /volume1/home/jonclow/irrigation/nginx-error.log"
# Check status
ssh nas "ps aux | grep nginx | grep irrigation"ssh nas
cd /volume1/home/jonclow/irrigation
# View status
docker compose -f docker-compose.nas.yml ps
# Restart
docker compose -f docker-compose.nas.yml restart
# View logs
docker compose -f docker-compose.nas.yml logs -f
# Stop
docker compose -f docker-compose.nas.yml down
# Start
docker compose -f docker-compose.nas.yml up -d# View logs
ssh irrigation 'sudo journalctl -u irrigation -f'
# Restart
ssh irrigation 'sudo systemctl restart irrigation'
# Status
ssh irrigation 'sudo systemctl status irrigation'
# Stop
ssh irrigation 'sudo systemctl stop irrigation'
# Start
ssh irrigation 'sudo systemctl start irrigation'-
Edit the config:
ssh nas vi /volume1/home/jonclow/irrigation/nginx.conf
-
Test configuration:
sudo /usr/builtin/sbin/nginx -t -c /volume1/home/jonclow/irrigation/nginx.conf
-
Restart nginx:
sudo ./stop-nginx.sh && sudo ./start-nginx.sh
-
Edit docker-compose:
ssh nas vi /volume1/home/jonclow/irrigation/docker-compose.nas.yml
-
Restart container:
docker compose -f docker-compose.nas.yml down docker compose -f docker-compose.nas.yml up -d
irrigate/
├── client/ # Frontend React app
│ ├── app/ # React Router 7 routes & components
│ │ ├── components/ # React components
│ │ ├── css/ # Stylesheets
│ │ ├── routes/ # Route definitions
│ │ ├── root.tsx # Root layout
│ │ └── socket.ts # Socket.IO client
│ ├── public/ # Static assets (favicon, manifest)
│ ├── Dockerfile # Docker build config
│ ├── vite.config.ts # Vite configuration
│ └── package.json
├── server/ # Backend API
│ ├── api/ # Controllers, services, models
│ │ ├── controllers/ # Request handlers
│ │ ├── services/ # Business logic
│ │ └── models/ # Data models
│ ├── config/ # Configuration files
│ ├── app.js # Express server entry
│ └── package.json
├── docker-compose.nas.yml # Docker config for NAS
├── docker-deploy-nas-local.sh # Deploy frontend to NAS (local build)
├── deploy-pi-backend.sh # Deploy backend to Pi
├── irrigation.service # Systemd service for Pi
├── start-systemd.sh # Backend startup (called by systemd)
└── stop.sh # Backend shutdown (called by systemd)
Built into Docker image during build (from .env.production):
VITE_BASE_URL=http://192.168.20.59:3001
Located in server/.env on Pi:
- Database connection details
- Serial port configuration
- API keys (if any)
502 Bad Gateway / Site Not Accessible:
-
Check nginx is running:
ssh nas "ps aux | grep nginx | grep irrigation" -
Check container is running:
ssh nas "docker ps | grep irrigation" -
Test internal connectivity:
ssh nas "curl -I http://localhost:80" # Should reach nginx ssh nas "curl -I http://localhost:3000" # Should reach container
-
View logs:
ssh nas "tail -50 /volume1/home/jonclow/irrigation/nginx-error.log" ssh nas "docker logs irrigation-frontend --tail=50"
-
Restart both services:
ssh nas "cd /volume1/home/jonclow/irrigation && sudo ./stop-nginx.sh && docker compose -f docker-compose.nas.yml restart && sudo ./start-nginx.sh"
Nginx Won't Start:
- Check port 80 isn't in use:
ssh nas "sudo netstat -tlnp | grep ':80 '" - Verify binary exists:
ssh nas "ls -la /usr/builtin/sbin/nginx" - Test config:
ssh nas "sudo /usr/builtin/sbin/nginx -t -c /volume1/home/jonclow/irrigation/nginx.conf"
Favicon Showing 404:
- Clear browser cache (Ctrl+Shift+R)
- Test favicon:
curl -I http://irrigation.home/favicon.ico - Check file in container:
ssh nas "docker exec irrigation-frontend ls -la /app/build/client/ | grep favicon"
API Not Responding:
-
Check service status:
ssh irrigation 'sudo systemctl status irrigation' -
View recent logs:
ssh irrigation 'sudo journalctl -u irrigation -n 100' -
Test API directly:
curl http://192.168.20.59:3001/weather/getBasicWeather
-
Restart service:
ssh irrigation 'sudo systemctl restart irrigation'
Serial Port Issues:
-
Check port exists:
ssh irrigation 'ls -la /dev/ttyACM0' -
Check user permissions:
ssh irrigation 'groups' # Should include dialout ssh irrigation 'sudo usermod -a -G dialout $USER' # Add if missing
-
Check service has access:
ssh irrigation 'sudo journalctl -u irrigation | grep -i serial'
Node Version Issues:
The backend uses Node 22 via NVM. If you see Node 18 errors:
-
Verify NVM is sourced in systemd:
ssh irrigation 'sudo systemctl cat irrigation | grep nvm' -
Check Node version in service:
ssh irrigation 'sudo journalctl -u irrigation | grep "Using Node"' -
Rebuild native modules:
ssh irrigation 'cd ~/irrigation-controller && npm rebuild'
GET http://192.168.20.59:3001/health
GET http://192.168.20.59:3001/weather/getBasicWeather
Returns:
{
"dtg": "2026-02-12T08:17:52.513Z",
"rain": 0,
"baro": 1000.42,
"air_temp": 18.5,
"humid": 100,
"solar": 27,
"wind_mean": {"sp": 7, "dir": 17},
"wind_high": {"sp": 15, "dir": 49},
"wind_low": {"sp": 2, "dir": 41},
"rain1": "0.0",
"rain24": "2.3",
"rain48": "177.4",
"rainweek": "1046.5",
"serialStatus": {
"connected": true,
"reconnecting": false,
"attempts": 0,
"port": "/dev/ttyACM0"
}
}✅ Permanent Configuration - Nginx config won't be overwritten by ADM ✅ Simple Management - Start/stop scripts, no complex networking ✅ Organized - All files in one location on NAS ✅ Modern Stack - React 19, Node 24 (frontend), Node 22 (backend) ✅ Resource Efficient - Frontend on NAS (more RAM), backend on Pi (hardware access) ✅ Standard Ports - Port 80 for web, no :8080 in URLs ✅ WebSocket Support - Real-time valve status and weather updates
- Pi has limited RAM (908MB) - frontend moved to NAS for resource management
- Nginx provides security layer (rate limiting, headers, request filtering)
- Docker container only exposed to localhost, accessed via nginx proxy
- Socket.IO connections proxied with WebSocket upgrade support
- Frontend build time ~6 seconds locally, ~2 minutes on NAS (if building remotely)
- Backend uses systemd for auto-restart on Pi
- Web Center is disabled to prevent nginx config regeneration
- Local Docker builds use Node 24 LTS for better compatibility