docker.recipes
Operations9 min read

CI/CD for Self-Hosters

Deploy Docker Compose stacks automatically with GitHub Actions, webhooks, and rollback strategies.

01Why Automate Deployments?

Manual deployments are error-prone and tedious. Automation ensures: • Consistent, repeatable deployments • Faster updates • Audit trail of changes • Easy rollbacks **For self-hosters, you have options:** • GitHub Actions + SSH (most common) • Webhook-triggered deployments • GitOps with file watching • Pull-based deployment agents

02GitHub Actions with SSH

Deploy to your server via SSH whenever you push to main. This is the most straightforward approach.
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13
14 - name: Deploy to server
15 uses: appleboy/ssh-action@v1.0.0
16 with:
17 host: ${{ secrets.HOST }}
18 username: ${{ secrets.USERNAME }}
19 key: ${{ secrets.SSH_KEY }}
20 script: |
21 cd /docker/myapp
22 git pull origin main
23 docker compose pull
24 docker compose up -d --remove-orphans
25 docker image prune -f
26
27# Required secrets in GitHub:
28# - HOST: your server IP/hostname
29# - USERNAME: SSH user
30# - SSH_KEY: Private key (generate with ssh-keygen)

Create a dedicated deploy user with limited permissions. Don't use root for automated deployments.

03Setting Up a Secure Deploy User

Create a dedicated user for deployments with minimal permissions.
1# On your server:
2
3# Create deploy user
4sudo useradd -m -s /bin/bash deploy
5sudo usermod -aG docker deploy
6
7# Create directory for docker stacks
8sudo mkdir -p /docker
9sudo chown deploy:deploy /docker
10
11# Set up SSH key authentication
12sudo mkdir -p /home/deploy/.ssh
13# Paste your public key:
14sudo nano /home/deploy/.ssh/authorized_keys
15sudo chmod 700 /home/deploy/.ssh
16sudo chmod 600 /home/deploy/.ssh/authorized_keys
17sudo chown -R deploy:deploy /home/deploy/.ssh
18
19# Disable password authentication for this user
20# Add to /etc/ssh/sshd_config:
21Match User deploy
22 PasswordAuthentication no
23
24# Restart SSH
25sudo systemctl restart sshd
26
27# Clone your repo as the deploy user
28sudo -u deploy git clone git@github.com:you/repo.git /docker/myapp

04Webhook-Triggered Deployments

For servers without SSH access from GitHub, use webhooks. This pulls updates when triggered.
1# docker-compose.yml with webhook receiver
2services:
3 webhook:
4 image: almir/webhook
5 container_name: webhook
6 command: -verbose -hooks=/etc/webhook/hooks.json -hotreload
7 volumes:
8 - ./hooks.json:/etc/webhook/hooks.json
9 - ./deploy.sh:/scripts/deploy.sh
10 - /var/run/docker.sock:/var/run/docker.sock
11 - /docker:/docker
12 ports:
13 - "9000:9000"
14 restart: unless-stopped
15
16# hooks.json
17[
18 {
19 "id": "deploy-myapp",
20 "execute-command": "/scripts/deploy.sh",
21 "command-working-directory": "/docker/myapp",
22 "pass-arguments-to-command": [
23 { "source": "payload", "name": "ref" }
24 ],
25 "trigger-rule": {
26 "match": {
27 "type": "payload-hmac-sha256",
28 "secret": "your-webhook-secret",
29 "parameter": {
30 "source": "header",
31 "name": "X-Hub-Signature-256"
32 }
33 }
34 }
35 }
36]
37
38# deploy.sh
39#!/bin/bash
40cd /docker/myapp
41git pull origin main
42docker compose pull
43docker compose up -d

05Implementing Rollbacks

When deployments fail, you need to roll back quickly. Plan your rollback strategy before you need it.
1#!/bin/bash
2# deploy-with-rollback.sh
3
4set -e
5
6APP_DIR="/docker/myapp"
7cd "$APP_DIR"
8
9# Save current state
10CURRENT_COMMIT=$(git rev-parse HEAD)
11echo "Current commit: $CURRENT_COMMIT"
12
13# Pull new changes
14git pull origin main
15
16# Pull new images
17if ! docker compose pull; then
18 echo "Failed to pull images, rolling back..."
19 git checkout "$CURRENT_COMMIT"
20 exit 1
21fi
22
23# Start new containers
24if ! docker compose up -d --remove-orphans; then
25 echo "Failed to start containers, rolling back..."
26 git checkout "$CURRENT_COMMIT"
27 docker compose up -d
28 exit 1
29fi
30
31# Health check
32sleep 10
33if ! curl -sf http://localhost:8080/health > /dev/null; then
34 echo "Health check failed, rolling back..."
35 git checkout "$CURRENT_COMMIT"
36 docker compose pull
37 docker compose up -d
38 exit 1
39fi
40
41echo "Deployment successful!"
42docker image prune -f

Pin image versions in compose files (image: app:v1.2.3) for easier rollbacks. 'latest' tag makes rollback harder.

06Blue-Green Deployments

For zero-downtime deployments, run two versions and switch traffic between them.
1# Traefik-based blue-green deployment
2services:
3 traefik:
4 image: traefik:v3.0
5 ports:
6 - "80:80"
7 volumes:
8 - /var/run/docker.sock:/var/run/docker.sock:ro
9
10 # Blue (current production)
11 app-blue:
12 image: myapp:v1.0.0
13 labels:
14 - "traefik.enable=true"
15 - "traefik.http.routers.app.rule=Host(`app.example.com`)"
16 # Weight 100 = receives all traffic
17 - "traefik.http.services.app.loadbalancer.server.weight=100"
18
19 # Green (new version, initially no traffic)
20 app-green:
21 image: myapp:v1.1.0
22 labels:
23 - "traefik.enable=true"
24 - "traefik.http.routers.app.rule=Host(`app.example.com`)"
25 # Weight 0 = receives no traffic
26 - "traefik.http.services.app.loadbalancer.server.weight=0"
27
28# Deployment process:
29# 1. Deploy green with weight=0
30# 2. Test green at internal URL
31# 3. Gradually shift weight: green=10, green=50, green=100
32# 4. Remove blue once verified

07Deployment Notifications

Get notified when deployments succeed or fail.
1# GitHub Actions with notifications
2name: Deploy
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13
14 - name: Deploy
15 id: deploy
16 uses: appleboy/ssh-action@v1.0.0
17 with:
18 host: ${{ secrets.HOST }}
19 username: ${{ secrets.USERNAME }}
20 key: ${{ secrets.SSH_KEY }}
21 script: |
22 cd /docker/myapp
23 ./deploy.sh
24
25 - name: Notify Discord on Success
26 if: success()
27 uses: sarisia/actions-status-discord@v1
28 with:
29 webhook: ${{ secrets.DISCORD_WEBHOOK }}
30 status: "success"
31 title: "Deployment Successful"
32
33 - name: Notify Discord on Failure
34 if: failure()
35 uses: sarisia/actions-status-discord@v1
36 with:
37 webhook: ${{ secrets.DISCORD_WEBHOOK }}
38 status: "failure"
39 title: "Deployment Failed"

Send deployment notifications to wherever you'll see them—Discord, Slack, email, or Pushover for mobile.