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
Decision Path Conclusion: If you are controlling a single Pi 5 from your phone or laptop, choose Tailscale. It assigns a static 100.x.y.z IP to your Pi, works through strict NAT firewalls, and requires zero router configuration.

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.

⚠️ HIGH VOLTAGE WARNING: The relay module's common (COM), normally open (NO), and normally closed (NC) screw terminals will be switching mains voltage (120V/240V AC). De-energize the mains circuit, lock out the breaker, and verify dead with a CAT III multimeter before terminating any wires on the relay output side. Local code may require a licensed electrician for permanent mains wiring.
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.