Running personal projects on a Raspberry Pi is a fantastic way to host applications efficiently and affordably. For my own React/NodeJS project I wanted a way to deploy updates without manually SSH-ing into the Pi, pulling the latest git changes, and restarting services. I needed a simple, lightweight CI/CD pipeline that wouldn’t bog down the Pi’s limited resources.
The solution? A custom-built webhook listener written in Go, fronted by an Nginx reverse proxy, and managed by systemd. This setup automatically deploys the latest version of the application whenever I push a change to the main
branch on GitHub. Waiting for a push from Github, not continually polling for updates.
Here’s how it all works.
The Core: A Go Webhook Listener#
The heart of the system is a small Go application that listens for incoming webhooks from GitHub. Go is a perfect choice for this task because it’s compiled, incredibly fast, and has a low memory footprint.
The entire application is a single file, main.go
:
package main
import (
"fmt"
"net/http"
"os"
"os/exec"
"github.com/go-playground/webhooks/v6/github"
)
const (
path = "/webhook"
port = ":3030"
)
func main() {
secret := os.Getenv("GITHUB_WEBHOOK_SECRET")
hook, err := github.New(github.Options.Secret(secret))
if err != nil {
panic(err)
}
// The path to the git repository on the Pi.
repoPath := os.Getenv("GIT_REPO_PATH")
if repoPath == "" {
repoPath = "/path/to/source/code" // Fallback for development
}
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
payload, err := hook.Parse(r, github.PushEvent)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// If the event is a push to the main branch, deploy!
if push, ok := payload.(github.PushPayload); ok && push.Ref == "refs/heads/main" {
fmt.Println("Received push event for main branch. Deploying...")
cmd := exec.Command("bash", "-c", "cd "" + repoPath + "" && git pull && npm install && npm run build && pm2 restart all")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("Deployment failed: %v
", err)
http.Error(w, "Deployment failed", http.StatusInternalServerError)
return
}
fmt.Println("Deployment successful!")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Deployment successful!"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Webhook received, but no action taken."))
})
fmt.Println("Webhook listener started on port", port)
http.ListenAndServe(port, nil)
}
Nginx as a Reverse Proxy#
To expose our Go webhook listener to the internet securely, we’ll use Nginx as a reverse proxy. This allows Nginx to handle SSL termination and forward requests to our Go application, which is listening on localhost:3030
.
Here’s a sample Nginx configuration:
server {
listen 80;
server_name your_domain.com;
location /webhook {
proxy_pass http://localhost:3030;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Remember to replace your_domain.com
with your actual domain name and configure SSL if you’re using HTTPS (highly recommended for webhooks). Also, ensure Nginx is configured to start on boot.
Systemd Service for the Go Application#
To ensure our Go webhook listener runs continuously and restarts automatically if it crashes or after a reboot, we’ll create a systemd service. Create a file named webhook.service
in /etc/systemd/system/
.
[Unit]
Description=Go Webhook Listener
After=network.target
[Service]
Environment="GITHUB_WEBHOOK_SECRET=your_secret_here"
Environment="GIT_REPO_PATH=/path/to/your/repo"
ExecStart=/usr/local/bin/webhook-listener
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
After creating the service file, enable and start it:
sudo systemctl enable webhook.service
sudo systemctl start webhook.service
sudo systemctl status webhook.service
GitHub Webhook Configuration#
Finally, configure your GitHub repository to send webhook payloads to your Raspberry Pi. Go to your repository settings, then Webhooks
, and click Add webhook
.
Payload URL: https://your_domain.com/webhook
Content type: application/json
Secret: your_secret_here
(This should match the GITHUB_WEBHOOK_SECRET
environment variable in your systemd service file.)
Which events would you like to trigger this webhook?: Just the Push events
Make sure the webhook is active.
Conclusion#
With this setup, you have a robust and automated deployment pipeline for your Raspberry Pi projects. Pushing to your GitHub main
branch will automatically trigger a deployment, saving you time and effort. Happy automating!