The most reliable way to setup remote access for Raspberry Pi without exposing your home network to the public internet is to use a zero-config mesh VPN like Tailscale paired with a local Flask Python API. This approach gives you a secure, encrypted tunnel directly to your Pi's GPIO pins, bypassing the need for dangerous router port forwarding or complex dynamic DNS configurations.
In this guide, we are targeting the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm. We will wire a 5V relay module, write a robust Python web server to trigger it, and debug the specific lgpio permission errors that plague newer Pi OS releases.
Decision Tree: Choosing Your Remote Access Method
Before writing code, you must choose the right transport layer. Makers often default to port forwarding, which is a massive security risk. Use this decision matrix to pick the right tool for your embedded project.
| Method | Best For | Security Profile | Setup Time | Verdict |
|---|---|---|---|---|
| Tailscale (WireGuard) | Single Pi / Direct API access | High (End-to-end encrypted mesh) | 5 mins | Default Pick |
| Cloudflare Tunnel | Public-facing web dashboards | High (Hides origin IP) | 20 mins | Use for HTML UIs |
| Router Port Forwarding | Legacy apps requiring public IPs | Low (Exposes ports to bots) | 30 mins | Avoid completely |
| MQTT Broker (HiveMQ) | Fleets of >5 sensor nodes | Medium (Requires TLS setup) | 45 mins | Use for IoT scale |
Hardware & Parts List
This build assumes you are driving an inductive load (like a solenoid or contactor coil) via a relay. The Raspberry Pi 5 has a stricter power envelope than the Pi 4, so proper power supply selection is critical.
| Component | Exact Variant / Spec | Estimated Cost | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 | Requires Bookworm OS for RP1 chip support |
| Power Supply | Official Raspberry Pi 27W USB-C PD | $12.00 | Provides 5V/5A; prevents brownout warnings |
| Cooling | Official Active Cooler | $5.00 | Mandatory for Pi 5 under network load |
| Actuator | Songle SRD-05VDC-SL-C 5V Relay | $2.00 | Opto-isolated version preferred |
| Wiring | 24 AWG Dupont Jumper Wires | $3.00 | Female-to-Female for Pi GPIO header |
Pin Mapping & Wiring the Relay
The Pi 5 uses the RP1 I/O chip, but the physical 40-pin header remains backward compatible. We will use BCM GPIO 18 (Physical Pin 12) because it supports hardware PWM if you later decide to swap the relay for a transistor-driven DC motor.
| Pi 5 Physical Pin | BCM GPIO | Function | Relay Module Pin |
|---|---|---|---|
| Pin 12 | GPIO 18 | Control Signal (3.3V Logic) | IN1 (Signal Input) |
| Pin 2 | N/A | 5V Power Rail | VCC (Relay Coil Power) |
| Pin 6 | N/A | Ground | GND |
Power Budget Check: The Pi 5 base draw is roughly 2A. The Songle 5V relay coil draws about 70mA. Total 5V draw is ~2.07A, well within the 5A limit of the official 27W PSU. If you add a camera module or USB peripherals, monitor for the lightning bolt icon indicating a brownout.
The Code: Flask GPIO API with Error Handling
On Raspberry Pi OS Bookworm, the legacy RPi.GPIO library is deprecated and often fails on the Pi 5. We must use gpiozero, which automatically routes through the lgpio backend for the RP1 chip.
Prerequisites: Install the required packages via terminal:
sudo apt update && sudo apt install python3-rpi-lgpio python3-flask
Then install Tailscale via the official script from Tailscale Knowledge Base.
# app.py - Remote GPIO Control Server
# Target: Raspberry Pi 5 (Bookworm) + Tailscale
from flask import Flask, jsonify, request
from gpiozero import OutputDevice
import logging
import sys
# Configure logging for remote debugging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# Pin Definitions (BCM Numbering)
RELAY_PIN = 18
# Initialize Relay
# active_high=True means 3.3V turns the relay ON.
# Change to False if using an active-low opto-isolated relay.
try:
relay = OutputDevice(RELAY_PIN, active_high=True, initial_value=False)
logging.info(f'Successfully initialized GPIO {RELAY_PIN}')
except Exception as e:
logging.critical(f'Failed to initialize GPIO: {e}')
sys.exit(1)
@app.route('/api/relay/on', methods=['POST'])
def turn_on():
try:
relay.on()
logging.info('Relay engaged')
return jsonify({'status': 'success', 'pin': RELAY_PIN, 'state': 'ON'}), 200
except Exception as e:
logging.error(f'GPIO write error: {e}')
return jsonify({'error': str(e)}), 500
@app.route('/api/relay/off', methods=['POST'])
def turn_off():
try:
relay.off()
logging.info('Relay disengaged')
return jsonify({'status': 'success', 'pin': RELAY_PIN, 'state': 'OFF'}), 200
except Exception as e:
logging.error(f'GPIO write error: {e}')
return jsonify({'error': str(e)}), 500
@app.route('/api/relay/status', methods=['GET'])
def get_status():
# gpiozero provides .value (1 or 0) for output devices
state = 'ON' if relay.value == 1 else 'OFF'
return jsonify({'pin': RELAY_PIN, 'state': state}), 200
if __name__ == '__main__':
# Bind to 0.0.0.0 to accept connections from Tailscale interface (e.g., 100.x.y.z)
app.run(host='0.0.0.0', port=5000, debug=False)
Testing locally: Run python3 app.py. Find your Tailscale IP by typing tailscale ip -4 in the terminal (it will look like 100.84.12.35). From your laptop on the same tailnet, send a POST request: curl -X POST http://100.84.12.35:5000/api/relay/on.
Debugging: First Three Things to Check When It Fails
Embedded networking and GPIO access on modern Linux distributions frequently trip up makers migrating from older Pi models. If your API fails, follow this ranked troubleshooting path.
1. Exact Error: lgpio.error: 'Failed to open /dev/gpiochip4'
Cause: In Pi OS Bookworm, the gpiozero library uses the lgpio C extension under the hood. If your user account lacks permissions to access the GPIO character device, or if the python3-rpi-lgpio package is missing, it throws this exact error.
Fix: Ensure the backend is installed and your user is in the correct group.
sudo apt install python3-rpi-lgpio
sudo usermod -aG gpio $USER
Log out and log back in for group changes to take effect.
2. Exact Error: ConnectionRefusedError: [Errno 111] Connection refused
Cause: Flask defaults to binding to 127.0.0.1 (localhost) for security. If you try to hit the Pi's Tailscale IP from your laptop, the Pi's firewall rejects it because the app isn't listening on the external network interface.
Fix: Verify that your app.run() command explicitly includes host='0.0.0.0' as shown in the code block above. If it's already there, check if UFW (Uncomplicated Firewall) is blocking port 5000: sudo ufw allow 5000/tcp.
3. Exact Error: tailscale: command not found or Tailscale IP is unreachable
Cause: Tailscale daemon crashed or didn't start on boot, meaning the Pi dropped off your mesh network and reverted to local-only routing.
Fix: Check the daemon status via sudo systemctl status tailscaled. If it is inactive, restart it with sudo systemctl enable --now tailscaled. You can verify the mesh connection by pinging another device on your tailnet using its MagicDNS name (e.g., ping my-laptop).
Extending and Simplifying the Build
Once you have the baseline API running over Tailscale, you will inevitably need to adapt it for production or scale. Here is how to pivot based on your project requirements.
How to Simplify: Switch to MQTT for Headless IoT
If you don't need a REST API and just want the Pi to react to sensor data from an ESP32 across the internet, drop Flask entirely. Use the paho-mqtt Python library. Connect the Pi to a free tier HiveMQ Cloud broker. The Pi subscribes to a topic like home/garage/door, and when an ESP32 publishes a '1' to that topic, the Pi triggers the relay. This eliminates the need to manage HTTP state and reduces overhead on the Pi 5's CPU.
How to Extend: Add Token Authentication
While Tailscale encrypts the transport layer, anyone with a device on your tailnet (like a family member's phone) can toggle your relay. To extend security to the application layer, add a simple Bearer token check in Flask. Generate a 32-byte hex string using python3 -c 'import secrets; print(secrets.token_hex(32))', store it in an environment variable, and require it in the Authorization header of your POST requests. This ensures that even if your tailnet is compromised, the GPIO endpoints remain locked.
How to Extend: Integrate Pi Camera Module 3
Because the Pi 5 has dedicated MIPI CSI lanes and the RP1 chip handles I/O, you can add a Pi Camera Module 3 to verify the relay's physical action (e.g., confirming a gate actually opened). Use the libcamera Python bindings to capture a JPEG upon receiving the /api/relay/on POST request, and return the base64-encoded image directly in the JSON response. This transforms a blind actuator into a verified remote control system.






