Back to articles
Technology Insight

Building a Lightweight CI/CD Pipeline with Webhooks and Docker Compose: Saying Goodbye to Heavy CI Tools

June 7, 2026

Introduction: The Cost of Over-Engineering in CI/CD

In the modern DevOps landscape, tools like Jenkins, GitLab CI/CD, and GitHub Actions have become the industry standard. They offer robust, enterprise-grade features for automated testing, building, and deployment. However, for small-scale projects, startups, or single-instance VPS deployments, these tools can quickly become a double-edged sword.

Running a dedicated GitLab Runner or a Jenkins instance requires significant system memory and CPU cycles—resources that could otherwise be allocated to your core applications. When your deployment needs are straightforward (e.g., pulling the latest code, rebuilding a Docker container, and restarting the service), spinning up a heavy CI/CD infrastructure is simply over-engineering. This article provides a comprehensive, step-by-step guide to building your own lightweight Continuous Deployment (CD) platform using nothing but standard Git Webhooks, simple automated scripts, and Docker Compose.

---

Why Go Serverless and Agentless for Your CD Pipeline?

Before diving into the implementation, let us analyze the distinct advantages of a minimalist approach compared to traditional enterprise setups:

  • Ultra-low Resource Footprint: A simple Webhook listener written in Go, Node.js, or even a lightweight Python script consumes less than 20MB of RAM. In contrast, Jenkins or GitLab Runner can easily consume hundreds of megabytes or even gigabytes under load.
  • Cost Efficiency: You can host your application and its deployment mechanism on a single $5/month VPS without experiencing low-memory crashes.
  • Architectural Simplicity: There are no complex plugins to update, no extensive user permissions to manage, and no hidden configuration files. If something breaks, debugging a shell script takes minutes.
  • Absolute Control: You write the exact deployment logic required for your system, eliminating the black-box abstraction layer of third-party CI tools.
---

The Architectural Blueprint

The workflow of our lightweight CD system is elegant in its simplicity. When a developer pushes code to a specific branch (such as main or production), the following sequence is triggered automatically:

  1. Git Provider Trigger: GitHub, GitLab, or Gitea detects the push event and sends an asynchronous HTTP POST payload (a Webhook) to a designated public URL on your target server.
  2. Webhook Authentication & Parsing: A lightweight listener application running on the server validates the incoming request using a secure pre-shared secret token. If valid, it extracts relevant metadata, such as the branch name.
  3. Execution Phase: If the branch matches your target deployment branch, the listener executes a local bash script.
  4. Docker Compose Deployment: The bash script navigates to the project directory, pulls the latest source code via Git, builds the updated Docker images, and replaces the running containers with zero down-time configurations.
Security Note: Because this listener has the authority to execute scripts that modify running system containers, securing the communication channel between your Git provider and your server is absolutely critical.
---

Step-by-Step Implementation Guide

Step 1: Preparing Your Application with Docker Compose

First, ensure your application is containerized correctly using Docker Compose. Below is a standard production configuration file (docker-compose.yml) for a web application utilizing a reverse proxy and a Node.js backend. This setup ensures that data persists across deployments and containers restart automatically if they crash.

version: '3.8'

services:
  web_app:
    image: myapp:latest
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    restart: always
    volumes:
      - app_logs:/app/logs

volumes:
  app_logs:

Step 2: Writing the Automated Deployment Script

Next, we create the core bash script (deploy.sh) that handles the actual system update. This script acts as our automated operator. It must have execution permissions (chmod +x deploy.sh) and should be kept in a secure location on your server.

#!/bin/bash

# Exit immediately if a command exits with a non-zero status
set -e

PROJECT_DIR="/var/www/my-application"
BRANCH="main"

echo "[$(date)] Deployment started..."

# Navigate to project directory
cd $PROJECT_DIR

# Fetch latest code and force reset to match origin branch
echo "Fetching latest changes from Git..."
git fetch --all
git reset --hard origin/$BRANCH

# Rebuild and restart containers cleanly
echo "Rebuilding Docker images..."
docker compose down
docker compose up --build -d

# Clean up dangling images to save disk space
echo "Cleaning up unused Docker resources..."
docker image prune -f

echo "[$(date)] Deployment completed successfully!"

Step 3: Setting Up a Secure Webhook Listener

To bridge the gap between GitHub/GitLab and our local bash script, we need a lightweight HTTP server. While you can write a short script in Node.js or Python, using an open-source, dedicated tool like adnanh's webhook (written in Go) provides enterprise-grade performance out of the box with zero boilerplate code.

Install the tool via your server's package manager:

sudo apt-get install webhook

Create a configuration file named hooks.json. This file defines the endpoints, matches the incoming secret token for security, and points directly to our deployment script:

[
  {
    "id": "deploy-app",
    "execute-command": "/var/www/my-application/deploy.sh",
    "command-working-directory": "/var/www/my-application",
    "trigger-rule": {
      "and": [
        {
          "match": {
            "type": "value",
            "value": "refs/heads/main",
            "parameter": {
              "source": "payload",
              "name": "ref"
            }
          }
        },
        {
          "match": {
            "type": "header",
            "value": "X-Hub-Signature-256",
            "parameter": {
              "name": "X-Hub-Signature-256"
            }
          }
        }
      ]
    }
  }
]

Step 4: Configuring the Git Provider (GitHub/GitLab)

With your listener running on the server (typically on port 9000), you must now register it with your repository management platform:

  • Navigate to your GitHub or GitLab Repository Settings -> Webhooks.
  • Click Add webhook.
  • Set the Payload URL to http://your-server-ip:9000/hooks/deploy-app.
  • Set the Content type to application/json.
  • Input a strong, randomly generated string into the Secret field. This secret must match your configuration validations to protect your server from unauthorized executions.
  • Select the Just the push event trigger and save.
---

Operational Best Practices & Security Considerations

While this architecture is incredibly efficient, operating outside the protective sandbox of platforms like GitHub Actions requires adherence to strict operational guardrails:

1. Never Deploy as Root

Ensure the Webhook listener service runs under a dedicated, limited user account (e.g., deploy-user). Grant this user targeted sudo access exclusively for Docker commands using the /etc/sudoers file, minimizing potential damage in the event of a remote code execution vulnerability.

2. Implement Reverse Proxy and SSL

Do not expose port 9000 directly to the public internet. Instead, route incoming traffic through a reverse proxy like Nginx or Caddy. Secure the connection with a free Let's Encrypt SSL certificate, ensuring your webhooks are transmitted over an encrypted https:// connection.

3. Automated Space Management

Docker builds can rapidly accumulate disk space over multiple deployments. Always include docker image prune -f or docker system prune -f --volumes within your deployment automation scripts to prevent your host machine from running out of disk storage.

---

Conclusion: Finding the Right Tool for the Job

Building a custom, lightweight CD pipeline using Webhooks and Docker Compose frees you from the heavy infrastructure overhead of enterprise tools while providing lightning-fast deployment cycles. It proves that you do not always need a massive, resource-heavy orchestration ecosystem to achieve elegant automation.

For complex applications requiring extensive multi-stage automated testing, matrix builds, or multi-cloud parallel deployments, traditional tools remain indispensable. However, for microservices, monolithic applications, web APIs, and MVP development pipelines, this lightweight strategy is a game-changing addition to your DevOps toolkit.