Level Up Your Dev Workflow: Branch-Specific Data Volumes with Docker Compose
Imagine this: you're working on a new feature and you're deep into it, only to be suddenly interrupted by a critical bug in production. You want to switch to a new branch to work on the bug fix, but your local database is still a work-in-progress with changes from your feature branch, hindering you from being able to test any changes for the bug fix.
Yikes! If only there was a way to do this without pulling your hair out 🤔
One approach to solve this problem is using branch-specific volumes in Docker. This blog post will guide you through a script that leverages Docker Compose to spin up services with branch-specific data volumes, streamlining your development process.
With branch-specific data volumes, each branch gets its own isolated data, preventing such conflicts. No more fear of breaking your main development environment or other branches!
Benefits of Using Docker Compose for Development:
- Consistent Environments: Docker Compose ensures your development environment is consistent across different machines and setups.
- Simplified Setup: Define and manage multiple services in a single
docker-compose.yml
file. - Faster Development: Quickly spin up and tear down development environments.
- Improved Collaboration: Share your development environment with ease.
The Challenge with Default Docker Compose Volume Behavior
A common limitation of Docker Compose is that a single instance of a volume, defined within a Docker Compose file, exists for the entire project. This can be problematic when working with multiple branches, as each branch might require its own isolated data.
The Solution: Dynamic Volume Creation
To overcome these limitations, we'll use a script to dynamically create and manage branch-specific data volumes. Here's how it works:
-
Detect the Current Branch and Calculate a Hash:
CURRENT_BRANCH=$(git symbolic-ref --short HEAD) CURRENT_BRANCH_HASH=$(echo -n "$CURRENT_BRANCH" | gzip -1 -c | tail -c8 | hexdump -n4 -e '"%08x"')
-
Define Volume Names Based on the Branch or Hash:
if [[ "$CURRENT_BRANCH" == "master" ]]; then export MYSQL_VOLUME="$APP_NAME-mysql-master-data" export REDIS_VOLUME="$APP_NAME-redis-master-data" # ... other volumes else export MYSQL_VOLUME="$APP_NAME-mysql-$CURRENT_BRANCH_HASH-data" export REDIS_VOLUME="$APP_NAME-redis-$CURRENT_BRANCH_HASH-data" # ... other volumes fi # Note that the '$APP_NAME' variable has been defined earlier # which is basically the project name for the docker compose manifest.
-
Create Volumes and Copy Data from
master
if Necessary:VOLUMES=(MYSQL_VOLUME REDIS_VOLUME ...) for VOLUME in "${VOLUMES[@]}"; do if [[ -z "$(docker volume ls -q -f name="${!VOLUME}")" ]]; then echo "Creating volume: ${!VOLUME}..." docker volume create "${!VOLUME}" > /dev/null if [[ "$CURRENT_BRANCH" != "master" ]]; then MASTER_VOLUME=$(echo -n "${!VOLUME}" | sed -E "s/(.*?)-$CURRENT_BRANCH_HASH-(.*?)/\1-master-\2/g") if [[ -z "$(docker volume ls -q -f name="${MASTER_VOLUME}")" ]]; then echo "Master volume not found: $MASTER_VOLUME" docker volume rm "${!VOLUME}" > /dev/null exit 1 fi echo "Copying data from master volume..." docker run --rm -v "$MASTER_VOLUME:/from" -v "${!VOLUME}:/to" busybox sh -c "cd /from; cp -a . /to" > /dev/null fi fi done
-
Start Docker Compose:
You can use the defined volume names as environment variables in yourdocker-compose.yml
file:services: mysql: image: mysql:latest volumes: - mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: my_database redis: image: redis:latest volumes: - redis-data:/data # ... other services volumes: mysql-data: name: ${MYSQL_VOLUME} external: true redis-data: name: ${REDIS_VOLUME} external: true # ... other volumes
By dynamically creating volumes based on the branch hash, we ensure that each branch has its own isolated data, even if the branch name contains invalid characters for Docker volume names.
Level Up Your Workflow
By incorporating this script and Docker Compose into your development process, you'll enjoy a smoother, more efficient, and less stressful development experience.
For a full working example, check out this GitHub repository: