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.yml2name: Deploy34on: 5 push: 6 branches: [main]78jobs: 9 deploy: 10 runs-on: ubuntu-latest11 steps: 12 - uses: actions/checkout@v41314 - name: Deploy to server15 uses: appleboy/ssh-action@v1.0.016 with: 17 host: ${{ secrets.HOST }}18 username: ${{ secrets.USERNAME }}19 key: ${{ secrets.SSH_KEY }}20 script: |21 cd /docker/myapp22 git pull origin main23 docker compose pull24 docker compose up -d --remove-orphans25 docker image prune -f2627# Required secrets in GitHub:28# - HOST: your server IP/hostname29# - USERNAME: SSH user30# - 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:23# Create deploy user4sudo useradd -m -s /bin/bash deploy5sudo usermod -aG docker deploy67# Create directory for docker stacks8sudo mkdir -p /docker9sudo chown deploy:deploy /docker1011# Set up SSH key authentication12sudo mkdir -p /home/deploy/.ssh13# Paste your public key:14sudo nano /home/deploy/.ssh/authorized_keys15sudo chmod 700 /home/deploy/.ssh16sudo chmod 600 /home/deploy/.ssh/authorized_keys17sudo chown -R deploy:deploy /home/deploy/.ssh1819# Disable password authentication for this user20# Add to /etc/ssh/sshd_config:21Match User deploy22 PasswordAuthentication no2324# Restart SSH25sudo systemctl restart sshd2627# Clone your repo as the deploy user28sudo -u deploy git clone git@github.com:you/repo.git /docker/myapp04Webhook-Triggered Deployments
For servers without SSH access from GitHub, use webhooks. This pulls updates when triggered.
1# docker-compose.yml with webhook receiver2services: 3 webhook: 4 image: almir/webhook5 container_name: webhook6 command: -verbose -hooks=/etc/webhook/hooks.json -hotreload7 volumes: 8 - ./hooks.json:/etc/webhook/hooks.json9 - ./deploy.sh:/scripts/deploy.sh10 - /var/run/docker.sock:/var/run/docker.sock11 - /docker:/docker12 ports: 13 - "9000:9000"14 restart: unless-stopped1516# hooks.json17[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]3738# deploy.sh39#!/bin/bash40cd /docker/myapp41git pull origin main42docker compose pull43docker compose up -d05Implementing Rollbacks
When deployments fail, you need to roll back quickly. Plan your rollback strategy before you need it.
1#!/bin/bash2# deploy-with-rollback.sh34set -e56APP_DIR="/docker/myapp"7cd "$APP_DIR"89# Save current state10CURRENT_COMMIT=$(git rev-parse HEAD)11echo "Current commit: $CURRENT_COMMIT"1213# Pull new changes14git pull origin main1516# Pull new images17if ! docker compose pull; then18 echo "Failed to pull images, rolling back..."19 git checkout "$CURRENT_COMMIT"20 exit 121fi2223# Start new containers24if ! docker compose up -d --remove-orphans; then25 echo "Failed to start containers, rolling back..."26 git checkout "$CURRENT_COMMIT"27 docker compose up -d28 exit 129fi3031# Health check32sleep 1033if ! curl -sf http://localhost:8080/health > /dev/null; then34 echo "Health check failed, rolling back..."35 git checkout "$CURRENT_COMMIT"36 docker compose pull37 docker compose up -d38 exit 139fi4041echo "Deployment successful!"42docker image prune -fPin 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 deployment2services: 3 traefik: 4 image: traefik:v3.05 ports: 6 - "80:80"7 volumes: 8 - /var/run/docker.sock:/var/run/docker.sock:ro910 # Blue (current production)11 app-blue: 12 image: myapp:v1.0.013 labels: 14 - "traefik.enable=true"15 - "traefik.http.routers.app.rule=Host(`app.example.com`)"16 # Weight 100 = receives all traffic17 - "traefik.http.services.app.loadbalancer.server.weight=100"1819 # Green (new version, initially no traffic)20 app-green: 21 image: myapp:v1.1.022 labels: 23 - "traefik.enable=true"24 - "traefik.http.routers.app.rule=Host(`app.example.com`)"25 # Weight 0 = receives no traffic26 - "traefik.http.services.app.loadbalancer.server.weight=0"2728# Deployment process:29# 1. Deploy green with weight=030# 2. Test green at internal URL31# 3. Gradually shift weight: green=10, green=50, green=10032# 4. Remove blue once verified07Deployment Notifications
Get notified when deployments succeed or fail.
1# GitHub Actions with notifications2name: Deploy34on: 5 push: 6 branches: [main]78jobs: 9 deploy: 10 runs-on: ubuntu-latest11 steps: 12 - uses: actions/checkout@v41314 - name: Deploy15 id: deploy16 uses: appleboy/ssh-action@v1.0.017 with: 18 host: ${{ secrets.HOST }}19 username: ${{ secrets.USERNAME }}20 key: ${{ secrets.SSH_KEY }}21 script: |22 cd /docker/myapp23 ./deploy.sh2425 - name: Notify Discord on Success26 if: success()27 uses: sarisia/actions-status-discord@v128 with: 29 webhook: ${{ secrets.DISCORD_WEBHOOK }}30 status: "success"31 title: "Deployment Successful"3233 - name: Notify Discord on Failure34 if: failure()35 uses: sarisia/actions-status-discord@v136 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.