The most reliable way to remote into a Raspberry Pi for embedded hardware projects is via SSH over a Tailscale mesh network. This gives you secure, zero-config CLI access from anywhere without exposing port 22 to the public internet. For this guide, we are targeting the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Lite (64-bit), building a headless TCP-controlled GPIO relay server.
Decision Tree: Which Remote Access Stack to Pick
Choosing the wrong remote access method leads to dropped connections, blocked ports, and bricked headless setups. Use this decision matrix to select your stack. We terminate on the optimal choice for 95% of embedded IoT and debugging scenarios.
| Use Case | Method | Pros | Cons |
|---|---|---|---|
| Local network CLI debugging | Standard SSH (mDNS) | Zero setup, native to OS | Fails outside local subnet |
| Local GUI / Desktop access | RealVNC / WayVNC | Visual desktop environment | Heavy CPU/RAM overhead on Pi |
| Remote IoT telemetry / async | MQTT + Node-RED | Decoupled, highly scalable | Overkill for direct CLI debugging |
| Remote CLI + GPIO control | Tailscale + SSH | Works anywhere, no port forwarding, encrypted | Requires 3rd-party auth account |
Parts List & Hardware Spec Sheet
This build assumes a physical hardware control element to demonstrate remote GPIO triggering. Do not use generic phone chargers for the Pi 5; it requires USB-C PD negotiation to deliver full current to the GPIO headers.
- Compute: Raspberry Pi 5 (8GB RAM) - ~$80 USD
- Power: Official Raspberry Pi 27W USB-C PD Power Supply - ~$12 USD
- Storage: SanDisk Extreme 64GB microSD (A2 rating) or official NVMe SSD - ~$15 USD
- Thermal: Official Active Cooler (mandatory for Pi 5 under load) - ~$5 USD
- Actuator: Omron G5V-2 5V DC DPDT Relay Module (Opto-isolated) - ~$4 USD
- Input: Momentary tactile pushbutton (12x12mm)
GPIO Pin Mapping Table
The Python script below targets these exact physical pins. Wire the opto-isolated relay to avoid back-EMF frying the Pi 5's BCM2712 GPIO bank.
| Physical Pin | BCM GPIO | Function | Wiring Destination |
|---|---|---|---|
| Pin 11 | GPIO 17 | Digital Output | Relay Module IN (Signal) |
| Pin 13 | GPIO 27 | Digital Input | Pushbutton (Normally Open) |
| Pin 1 | 3V3 | Power | Pushbutton (Common) |
| Pin 6 | GND | Ground | Relay GND & Pi Ground Plane |
Step-by-Step: Configuring Headless SSH & Tailscale
Follow these steps to flash, configure, and secure the network stack before writing any application code.
- Flash the OS: Use Raspberry Pi Imager. Select Raspberry Pi OS Lite (64-bit). In the advanced settings (gear icon), enable SSH (Use password authentication), set your hostname to
pi5-relay, and configure your local WiFi. - Boot and Connect: Insert the microSD, apply power, and wait 90 seconds. From your host machine, ping the mDNS address:
ping pi5-relay.local. - SSH In: Connect via terminal:
ssh your_username@pi5-relay.local. - Update the System: Run
sudo apt update && sudo apt upgrade -yto patch the BCM2712 kernel and firmware. - Install Tailscale: Execute the official install script:
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up - Authenticate: The terminal will output a URL. Open it on your phone or laptop to authorize the Pi 5 to your private Tailnet. You will now receive a static
100.x.y.zIP address for this Pi.
Remote GPIO Control: The Python Server Script
This complete, compilable Python script creates a lightweight TCP socket server. It listens for remote commands to toggle the relay and monitors the physical pushbutton. It uses the gpiozero library, which is pre-installed on Raspberry Pi OS Lite.
#!/usr/bin/env python3
"""
Remote GPIO TCP Server for Raspberry Pi 5
Target Board: Raspberry Pi 5 (8GB) running Pi OS Lite 64-bit
Dependencies: gpiozero (native), socket, threading
"""
import socket
import threading
import time
import sys
from gpiozero import LED, Button
from signal import pause
# --- PIN DEFINITIONS ---
RELAY_PIN = 17 # Physical Pin 11
BUTTON_PIN = 27 # Physical Pin 13
# --- HARDWARE INITIALIZATION ---
# Using LED class for relay as it maps cleanly to digital on/off
relay = LED(RELAY_PIN, active_high=True)
# Button with internal pull-up, active state is False (pressed connects to GND)
button = Button(BUTTON_PIN, pull_up=True, bounce_time=0.05)
HOST = '0.0.0.0' # Listen on all interfaces (Tailscale + Local)
PORT = 65432
def handle_button_press():
print("[HW] Physical button pressed. Toggling relay.")
relay.toggle()
def handle_client(conn, addr):
print(f"[NET] Connected by {addr}")
try:
while True:
data = conn.recv(1024).decode('utf-8').strip().upper()
if not data:
break
if data == 'ON':
relay.on()
conn.sendall(b"RELAY STATE: ON\n")
elif data == 'OFF':
relay.off()
conn.sendall(b"RELAY STATE: OFF\n")
elif data == 'STATUS':
state = "ON" if relay.is_lit else "OFF"
conn.sendall(f"RELAY STATE: {state}\n".encode('utf-8'))
else:
conn.sendall(b"ERR: Unknown command. Use ON, OFF, STATUS.\n")
except ConnectionResetError:
print(f"[NET] Client {addr} disconnected abruptly.")
finally:
conn.close()
print(f"[NET] Connection closed for {addr}")
def main():
# Bind physical button to toggle function
button.when_pressed = handle_button_press
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server_socket.bind((HOST, PORT))
server_socket.listen(5)
print(f"[SYS] TCP Server listening on {HOST}:{PORT}")
print("[SYS] Send 'ON', 'OFF', or 'STATUS' via netcat or telnet.")
while True:
conn, addr = server_socket.accept()
client_thread = threading.Thread(target=handle_client, args=(conn, addr))
client_thread.daemon = True
client_thread.start()
except KeyboardInterrupt:
print("\n[SYS] Shutting down server...")
except OSError as e:
print(f"[ERR] Socket binding failed: {e}")
sys.exit(1)
finally:
# Ensure hardware is left in a safe state on exit
relay.off()
server_socket.close()
print("[SYS] Relay OFF. Ports closed. Exiting.")
if __name__ == "__main__":
main()
How to test remotely: From your laptop (connected to the same Tailscale network), open a terminal and type:
echo "ON" | nc 100.x.y.z 65432
Replace 100.x.y.z with your Pi's Tailscale IP. The relay will click, and the terminal will return RELAY STATE: ON.
Debugging: Exact Errors & The First Three Checks
When remote embedded setups fail, the issue is almost always network routing, daemon status, or GPIO namespace collisions. Here is how to debug the exact errors you will encounter.
Error 1: ssh: connect to host 100.x.y.z port 22: Connection refused
Ranked Causes:
- SSH Daemon is disabled: Raspberry Pi OS Lite ships with SSH disabled by default for security. If you forgot to place the
sshfile in the boot partition or enable it in Imager, the daemon isn't running. - Tailscale is down: The Pi lost internet and Tailscale's DERP relay fallback failed, or the service crashed.
- IP Change: You are using a stale local IP instead of the static Tailscale IP.
Error 2: gpiozero.exc.GPIOPinInUse: pin 17 is already in use
Ranked Causes:
- Zombie Python Process: You killed a previous run of the script with
kill -9before it could hit thefinallyblock, leaving the BCM2712 pin locked in the kernel's pinctrl subsystem. - Device Tree Overlay Conflict: An entry in
/boot/firmware/config.txt(likedtoverlay=gpio-ir) has claimed GPIO 17 at the hardware level.
- Check SSH Service: Run
systemctl status ssh. If it says inactive, runsudo systemctl enable --now ssh. - Verify Network Route: Run
tailscale statuson your host machine to ensure the Pi shows as "active" and note the exact 100.x.y.z IP. - Check Interface Settings: Run
sudo raspi-config, navigate to Interface Options > SSH, and ensure it is explicitly enabled.
Extending and Simplifying the Build
Depending on your deployment environment, you should adapt this baseline architecture.
How to Simplify (Local Bench Only)
If this Pi never leaves your local network and you don't want to manage a Tailscale account, strip the mesh VPN out entirely. Rely on mDNS (Multicast DNS). Ensure the avahi-daemon package is installed (sudo apt install avahi-daemon), and simply SSH into ssh user@pi5-relay.local. This removes the internet dependency but restricts access to your local subnet.
How to Extend (Production IoT Deployment)
For a remote deployment where the Pi is controlling critical infrastructure (e.g., a greenhouse heater or server room AC), TCP sockets are too fragile. Extend the build by:
- Adding MQTT: Replace the TCP socket with the
paho-mqttlibrary. Subscribe to a topic likesite1/greenhouse/heater/cmdand publish state tosite1/greenhouse/heater/status. - Hardware Watchdog: Enable the BCM2712 hardware watchdog in
config.txtand use thewatchdogsystemd service to automatically hard-reboot the Pi if the Python script hangs or the kernel panics. - Systemd Service: Wrap the Python script in a
/etc/systemd/system/relay-server.servicefile withRestart=alwaysandRestartSec=5to ensure it survives power blips and reboots.






