Skip to main content
Automating Deployments on a Raspberry Pi with Go, Nginx, and Webhooks

Automating Deployments on a Raspberry Pi with Go, Nginx, and Webhooks

·627 words·3 mins
Author
Matt Delashaw

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!