To safely shut down a Raspberry Pi without corrupting the SD card, wire a momentary pushbutton to BCM GPIO 21 (Physical Pin 40) and GND (Physical Pin 39), then run a Python gpiozero script as a background systemd service. This guide targets the Raspberry Pi 4 Model B (4GB/8GB) and the Raspberry Pi 5 (4GB/8GB) running Raspberry Pi OS Bookworm.

Why You Need a Dedicated Raspberry Pi Power Off Button

Pulling the USB-C power cable on a running Raspberry Pi is the leading cause of SD card corruption. While the ext4 filesystem uses journaling to protect metadata, a hard power cut during an active write operation can still orphan inodes or corrupt the superblock. Over time, this leads to unbootable systems and kernel panics.

A dedicated hardware shutdown button sends a clean ACPI-style halt signal to the OS. This allows the system to flush write caches, unmount partitions gracefully, and park the filesystem before the power is physically removed. On headless setups (like 3D printer controllers, Pi-hole DNS servers, or retro gaming consoles), a physical button eliminates the need to SSH in just to type sudo shutdown -h now.

Component Selection & Shutdown Method Comparison

Before wiring, evaluate which shutdown method fits your build. The table below compares the four most common approaches for Pi 4 and Pi 5 deployments in 2026.

Shutdown Method Hardware Cost SD Corruption Risk Wake from Halt? Best Use Case
Hard Power Cut (Pulling Plug) $0.00 High No Read-only filesystems (e.g., kiosk mode)
GPIO Python Script (This Guide) ~$1.50 Zero No (Requires Pin 5 for wake) Headless servers, custom enclosures, Pi 4 builds
Pi 5 Native Power Button $0.00 (Built-in) Zero Yes Standard Pi 5 desktop or media center builds
UPS HAT (e.g., PiJuice V2 / Geekworm) $45.00 - $65.00 Zero Yes + Battery Backup Remote IoT nodes, critical data logging, outdoor cams
Expert Note on Pi 5: The Raspberry Pi 5 includes a native power button on the PCB. If you are using a Pi 5 and do not need to route the button to a custom front panel, simply use the native button. This guide is essential for Pi 4 users, or Pi 5 users integrating the board into a custom 3D-printed enclosure where the native button is inaccessible.

Wiring the Momentary Pushbutton

For this build, we use a standard 12mm or 6mm tactile momentary pushbutton. We will utilize the Raspberry Pi's internal pull-up resistors via software, meaning you only need two wires. No external 10kΩ resistor is required unless your wire run exceeds 12 inches (which acts as an antenna for EMI).

Pin Mapping Table

We intentionally avoid BCM 3 (Physical Pin 5). While Pin 5 supports hardware wake-from-halt, it is hardwired to the I2C SCL bus. If you enable I2C for sensors later, Pin 5 will cause bus contention. BCM 21 is safe, unused by default interfaces, and sits conveniently next to a ground pin.

Component Leg BCM GPIO Physical Pin Wire Color (Recommended)
Button Leg 1 (Normally Open) BCM 21 Pin 40 Yellow or Orange
Button Leg 2 (Common) GND Pin 39 Black

Physical Wiring Steps:

  1. Power down the Pi and disconnect the USB-C cable.
  2. Connect one leg of the momentary switch to Physical Pin 40 (BCM 21).
  3. Connect the opposite leg of the switch to Physical Pin 39 (GND).
  4. If mounting the button to a panel, use heat-shrink tubing over the solder joints to prevent shorting against the metal chassis.

The Python Shutdown Script (Pi 4 & Pi 5 Compatible)

Historically, makers used the RPi.GPIO library. However, with the release of Raspberry Pi OS Bookworm and the Pi 5's new RP1 southbridge chip, RPi.GPIO is largely deprecated and will throw segmentation faults. The modern, supported standard is gpiozero using the lgpio backend.

Create a new file named shutdown_button.py in your home directory (~/shutdown_button.py) and paste the following code. This script includes a 2-second hold requirement to prevent accidental shutdowns from bumping the enclosure.

#!/usr/bin/env python3
import os
import sys
import logging
from gpiozero import Button
from signal import pause

# Configure logging to journalctl
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# BCM GPIO 21 is Physical Pin 40
SHUTDOWN_PIN = 21
HOLD_TIME = 2.0  # Require 2-second hold to prevent accidental bumps

def initiate_shutdown():
    logging.info('Shutdown button held. Initiating safe system halt...')
    # Note: When run via systemd as root, sudo is not required
    os.system('shutdown -h now')

try:
    # pull_up=True uses internal resistor. bounce_time=0.2 prevents switch chatter.
    shutdown_btn = Button(
        SHUTDOWN_PIN,
        pull_up=True,
        bounce_time=0.2,
        hold_time=HOLD_TIME
    )
    shutdown_btn.when_held = initiate_shutdown
    logging.info(f'Power off button listener active on BCM {SHUTDOWN_PIN}.')
    
    # Keep the script running in the background
    pause()

except RuntimeError as e:
    logging.error(f'GPIO Hardware Error: {e}')
    sys.exit(1)
except Exception as e:
    logging.error(f'Unexpected failure: {e}')
    sys.exit(1)

Autostart Configuration via systemd

To ensure the script runs on boot without requiring you to log in, we register it as a systemd service. This is vastly superior to using rc.local or .bashrc, as systemd handles automatic restarts if the script crashes and logs output to the system journal.

Create a new service file:

sudo nano /etc/systemd/system/shutdown-button.service

Paste the following configuration. Note that we use absolute paths, which is a strict requirement for systemd units.

[Unit]
Description=Raspberry Pi GPIO Power Off Button
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/shutdown_button.py
Restart=on-failure
RestartSec=5s
User=root

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable shutdown-button.service
sudo systemctl start shutdown-button.service

Verify it is running with sudo systemctl status shutdown-button.service. You should see a green active (running) state. Press and hold your button for 2 seconds; the Pi will cleanly shut down.

Debugging & Extending the Build

Hardware integrations rarely work perfectly on the first try. If your button fails to trigger a shutdown, check these three specific failure modes first.

The First Three Things to Check When It Fails

  1. Exact Error: ModuleNotFoundError: No module named 'gpiozero' or lgpio.
    Cause: Bookworm OS uses strictly sandboxed Python environments (PEP 668).
    Fix: Install the OS-level packages rather than using pip. Run: sudo apt install python3-gpiozero python3-lgpio.
  2. Exact Error: RuntimeError: Failed to add edge detection or gpiozero.exc.PinFactoryFallback.
    Cause: Another process is hogging BCM 21, or the pin mapping is incorrect.
    Fix: Check for conflicting processes using sudo lsof | grep gpio. Ensure you wired to Physical Pin 40, not Pin 21. (Physical Pin 21 is GND).
  3. Symptom: Service fails to start on boot (code=exited, status=1/FAILURE).
    Cause: The systemd file uses relative paths or lacks root privileges to execute the shutdown command.
    Fix: Ensure ExecStart uses /usr/bin/python3 and the absolute path to your script. Verify User=root is set in the [Service] block, as standard users cannot execute shutdown -h now without passwordless sudoers configuration.

For deeper hardware diagnostics, consult the official Raspberry Pi Configuration Documentation to verify your GPIO pinout overlays.

How to Extend or Simplify the Build

To Simplify: If you are using a Raspberry Pi 5 and don't want to write code, abandon this script entirely. The Pi 5's native power button handles graceful shutdown and wake-from-halt at the firmware level. For Pi 4 users seeking a zero-code alternative, look into the dtoverlay=gpio-shutdown line in /boot/firmware/config.txt. Adding dtoverlay=gpio-shutdown,gpio_pin=21,active_low=1,gpio_pull=up achieves the exact same result using kernel-level device tree overlays instead of Python.

To Extend: Add a 'Safe to Unplug' status LED. Wire a 3mm LED (with a 220Ω current-limiting resistor) to BCM 17 (Physical Pin 11) and GND. Modify the Python script to initialize led = LED(17) and turn it on during boot. When the initiate_shutdown() function triggers, call led.off() before the os.system command. This gives you a visual indicator that the filesystem has unmounted and the power can be safely cut. For advanced power management and brownout protection, review the systemd.service documentation to integrate graceful shutdown hooks with UPS daemon (nut) services.