Local CI/CD for Raspberry Pi: Automating Docker Compose Deployments
Part 3 of 7 in the series Homelab on the Raspberry Pi

Contents
Managing containerized infrastructure on edge devices such as a Raspberry Pi often involves repetitive manual SSH sessions, manual image pulls, and direct execution of Docker Compose commands. Transitioning to a localized CI/CD pipeline automates the lifecycle of self-hosted services—including Home Assistant, Nextcloud, and custom Python automation scripts—by leveraging Git repositories, webhook listeners, and local deployment agents.
1. Architecture of Local GitOps for Edge Devices
Adopting a GitOps methodology shifts the source of truth for all infrastructure configurations from local filesystem modifications to a centralized Git repository. Rather than editing YAML files directly on the server, deployment definitions are maintained in version control.
- Declarative Configuration: Every service, network definition, and persistent volume is declared explicitly inside structured
docker-compose.ymlfiles stored in a Git repository. - Auditability & Rollbacks: Version control provides an immutable history of configuration changes, allowing immediate rollbacks to previous working states if an update causes service instability.
- Zero-Touch Edge Deployments: The edge server acts as a continuous consumer of repository states, eliminating the need for interactive shell access during routine updates.
2. Automated Pulls and Deployments via Webhook Listeners
To eliminate manual SSH deployment commands, lightweight webhook servers (such as adnanh/webhook or dedicated automation daemons) can be deployed natively on the Raspberry Pi. These services listen for HTTP POST payloads triggered by code repository push events or CI/CD pipelines.
Webhook Execution Workflow
- Payload Verification: Incoming webhook requests are cryptographically validated using HMAC secret tokens to prevent unauthorized remote execution.
- Automated Git Fetch: Upon successful validation, the local daemon triggers a non-interactive pull sequence to update the local workspace from the remote branch.
- Stack Re-convergence: The orchestration daemon runs
docker compose up -d --remove-orphans, ensuring that only modified container images or updated service declarations are recreated while preserving active services.
3. Container Lifecycle Management without SSH
Continuous deployment architectures for self-hosted environments benefit from dedicated local agents that monitor container image registries and automate container recreation without external intervention.
- Automated Registry Polling: Tools of the Watchtower family periodically inspect base image digests for running containers and automatically restart services when newer builds become available. The original
containrrr/watchtowerrepository was archived in December 2025 and the older Ouroboros project is no longer developed; maintained community forks such asnickfedor/watchtowercontinue as drop-in replacements. - Local Build Automation: For custom Python scripts or microservices, local automation runners can execute Dockerfile builds directly on the ARM architecture, circumventing cross-compilation complexities.
- Health Checks & Auto-Recovery: Implementing native Docker health checks ensures that updated containers failing initial startup routines can be flagged immediately without requiring manual log inspections.
4. Security Considerations for Local CI/CD Pipelines
Exposing automated deployment endpoints within a home or enterprise network requires strict privilege separation. Webhook daemons should never run as root; instead, access to the Docker socket should be regulated through dedicated group permissions or read-only socket proxies. Furthermore, isolating deployment listeners within a restricted management VLAN ensures that build automation remains completely protected from untrusted client devices.
5. Push Instead of Pull: GitHub Actions and Scheduled Image Updates
With the webhook pattern, the Raspberry Pi fetches its own state. The opposite direction, in which a CI workflow pushes the deployment, is common with GitHub Actions and comes in two variants that differ mainly in how reachable the device has to be.
- GitHub-hosted runner over SSH: the workflow runs in GitHub’s cloud and opens an SSH connection to the Pi. This requires the Pi’s SSH port to be reachable from the internet, which contradicts a purely local setup.
- Self-hosted runner on the Pi: GitHub supports self-hosted runners on Linux for ARM64 and ARM32. The runner only needs outbound HTTPS connections over port 443, so no inbound port has to be opened. GitHub recommends self-hosted runners for private repositories only, because a pull request from a fork of a public repository can run its code on the runner machine.
Restricting the Deployment Key
Where the SSH variant stays in use, the key stored as a repository secret can be pinned to a single action in the ~/.ssh/authorized_keys file of a dedicated deployment user:
restrict,command="/usr/local/bin/deploy-stack" ssh-ed25519 AAAA… ci-deploy
command= runs the stored script on every login with this key and ignores the command sent by the client; the original request reaches the script only through the SSH_ORIGINAL_COMMAND environment variable. restrict disables port, agent and X11 forwarding as well as PTY allocation and the execution of ~/.ssh/rc. A leaked secret can then trigger a deployment and nothing else. Membership in the docker group is no restriction in this setup: according to the Docker documentation, the group grants root-level privileges.
Scheduling Watchtower Updates
By default, Watchtower checks for new images every 86,400 seconds (24 hours). A fixed maintenance window is set with WATCHTOWER_SCHEDULE, a cron expression with six fields whose first field is seconds; 0 0 4 * * * runs daily at 04:00. The schedule requires a time zone via TZ or a mounted /etc/localtime and cannot be combined with WATCHTOWER_POLL_INTERVAL. WATCHTOWER_CLEANUP=true removes old images after an update, so the SD card or SSD does not fill up over time. With WATCHTOWER_LABEL_ENABLE=true, only containers carrying the label com.centurylinklabs.watchtower.enable=true are updated, which leaves the Compose file in charge of which services Watchtower touches at all.
Questions and answers
Can a read-only Docker socket proxy serve a webhook deployment that runs docker compose up -d?
No, and that is the useful part of the answer. A read-only socket proxy permits inspection, such as listing containers and reading image digests, and refuses everything that changes state. That is enough for monitoring; it covers neither a deployment that recreates containers nor updates of the Watchtower kind, which restart containers as well.
So the separation belongs one step earlier, between two processes rather than inside one. The listener that receives the webhook needs no socket access at all: it verifies the HMAC and writes a job, whether as a file, a queue entry or a systemd unit trigger. A second, very small process with actual socket rights picks that job up and runs the compose command, and it accepts nothing else.
The gain is that the component reachable over the network holds no privileges, and the component holding privileges is not reachable over the network. A single daemon with a proxy permissive enough to deploy has neither property; the proxy is then only a longer path to the same authority.
Sources
- Docker Compose documentation
- GitHub Docs: Self-hosted runners reference
- GitHub Docs: Adding self-hosted runners
- OpenBSD manual: sshd(8), AUTHORIZED_KEYS FILE FORMAT
- Docker: Linux post-installation steps for Docker Engine
- Watchtower (nicholas-fedor): Arguments
- robfig/cron v1.2.0: CRON Expression Format
Homelab on the Raspberry Pi
- Raspberry Pi 4 and 5: Booting From an SSD by Changing the Bootloader
- Uninterruptible Power Supply (UPS) for Raspberry Pi 4 and 5: Top 5 Solutions for High-Load Setups
- Local CI/CD for Raspberry Pi: Automating Docker Compose Deployments
- Autonomous Docker-Compose Home Server with Traefik, SSL, and Watchtower
- Zero-Latency Local Media Server: Jellyfin on Raspberry Pi 5 with Hardware Acceleration
- Raspberry Pi as a Hardened WireGuard VPN Gateway with Split-Tunneling
- Bare-Metal Performance Diagnostics: Prometheus & Grafana Dashboard for Raspberry Pi