Webhook endpoint to trigger docker container rebuild
- You build and push your container images from a CI/CD pipeline to a container registry
- You run your own container from a VM created from a docker compose file
- You are looking for a way to pull and recreate your docker image after CI/CD completes
- You want to avoid polling the container registry on an interval
- You want to avoid setting up SSH from the pipeline into your server
Docker Compose example:
services:
docker-delivery-hook:
image: ghcr.io/bbilly1/docker-delivery-hook
container_name: docker-delivery-hook
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /path/to/docker-compose.yml:/path/to/docker-compose.yml
ports:
- "127.0.0.1:8000:8000"
environment:
SECRET_KEY: "your-very-secret-key"
Make sure you specify the container_name
key for all services on your host system, as that is used for identification.
- Docker Socket: Mount host docker socket into the container to allow the container to execute docker commands as the host user. See security considerations below.
- Compose File: Crucially mount the docker-compose.yml file exactly at the same absolute path inside the container as outside on the host machine. Docker tracks the compose environment with the labels
com.docker.compose.project.config_files
andcom.docker.compose.project.working_dir
. Interacting with existing containers requires the same compose location otherwise docker will treat this as a separate compose file.
Configure the API with these environment variables:
SECRET_KEY
: Required, shared secret key for signature validation.UVICORN_PORT
: Optional, overwrite internal web server port from default 8000.SHOW_DOCS
: Optional, set to anything except an empty string to show default FastAPI docs. Only for your local dev environment.
This API implements these endpoints:
/pull
: Rebuilding the container by pulling the new image. Only applicable if your compose file defines animage
key. That is equivalent to:
docker compose pull container_name && docker compose up -d container_name
/build
: Rebuild the container by building locally. Only applicable if your compose file defines abuild
key. Be aware that you either need to pull the context from a remote like git or mount the correct build context into the container and not just the compose file. That is equivalent to:
docker compose up -d --build container_name
These endpoints are async. Meaning after request validation will return while the docker commands will process in the background.
PAYLOAD='{"container_name": "my-container-name"}'
SECRET_KEY="your-very-secret-key"
TIMESTAMP=$(date +%s)
MESSAGE="${PAYLOAD}${TIMESTAMP}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET_KEY" | cut -d " " -f 2)
curl -X POST -H "Content-Type: application/json" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-d "$PAYLOAD" \
$API_ENDPOINT
Explanation:
SECRET_KEY
: That's the shared secret between your pipeline and the API container. That is usually stored as a secret variable in your pipeline.TIMESTAMP
: UTC epoch timestamp.MESSAGE
: String concatenated from Payload and Timestamp.SIGNATURE
: SHA256 HMAC signature from the message. See below for additional examples.PAYLOAD
: JSON body with key"container_name"
and value the container name as defined in your compose file.
Depending what you have available in your pipeline environment, you might want to choose one over the other. Here are some examples:
Using OpenSSL:
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET_KEY" | cut -d " " -f 2)
Using Python standard library:
SIGNATURE=$(python -c "import hmac, hashlib; print(hmac.new(b'$SECRET_KEY', b'$MESSAGE', hashlib.sha256).hexdigest())")
Using NodeJS:
SIGNATURE=$(node -e "
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', '$SECRET_KEY').update('$MESSAGE').digest('hex');
console.log(signature);
")
If you see any flaws here, reach out.
- Signature Verification: By passing the
X-Signature
header with your request, the API will be able to verify that the origin has the sameSECRET_KEY
as the API and the origin receives the same data as expected. - Timestamp: By passing the
X-Timestamp
header plus by using the timestamp in the message to verify the signature, you are able to guarantee that even an intercepted message wouldn't be able to be reused in a future time. - Container Name: The container name you send with the payload is verified by docker directly first by checking all existing containers and searching for a match.
- Compose Validation: The compose file location is validated by inspecting the container name
- Predefined commands: The commands executed are predefined. The variables going into the commands are validated as described above.
Alternate approach to solve this problem is to setup SSH in your pipeline. That usually means to create a least privileged user on your VM, lock down SSH for that user to limit what that user is allowed to do, then add the private SSH key in your pipeline. Then as part of your pipeline, you'll register the key, login to your VM, and run the needed commands.
That has a few downsides:
- Requires configurations on the VM. That can be automated with scripting or tools like Ansible, but that's something that needs to be maintained additionally to your application code base.
- Another SSH key on the VM is required to basically just execute a single command. That is an additional exposure that you might want to avoid.
- The private key needs to be in the CI/CD pipeline and will be accessible by everyone with access to the pipeline.
- That is difficult to manage with infrastructure as code. Having a CI/CD listener on your VM that can react to webhooks can be managed in your regular docker compose file. All can be committed to version control as part of your application, obviously except the
SECRET_KEY
. - Needing SSH from your pipeline makes hardening your SSH exposure much more difficult. Depending on your environment you might not know all possible IPs from your runners and you might not allow SSH to be available to the internet unrestricted.
You might also want to read up on the implication for mounting docker.sock
into the container. Verify the code first, use at your own risk before publishing that to the internet.