with Compose Project Names and Profiles
Anyone needing to check multiple feature branches at the same time quickly hits the limits of a single Docker Compose stack: port conflicts, shared databases and naming collisions between containers and networks. COMPOSE_PROJECT_NAME combined with profiles solves exactly this problem by letting every environment run side by side, cleanly isolated.
Table of Contents
- 1. The problem: one stack, multiple feature branches
- 2. COMPOSE_PROJECT_NAME as an isolation mechanism
- 3. Dynamic port assignment per branch
- 4. Combining profiles with project names for team specific stacks
- 5. Generating environments automatically per branch
- 6. Avoiding network and volume naming collisions
- 7. Reliably cleaning up orphaned feature environments
- 8. Using the same pattern for review apps in CI
- 9. Approaches to parallel environments compared
- 10. Summary
- 11. FAQ
1. The problem: one stack, multiple feature branches
As soon as several feature branches are being developed in parallel, a single Docker Compose stack is no longer enough. A classic scenario: developer A works on a checkout feature, developer B simultaneously on a product import change, both need a running shop stack with its own database, but neither wants to accidentally overwrite the other's test data. Without isolation between branches, both share the same container names, the same ports and the same database, leading to constant conflicts.
The obvious but impractical workaround is running a separate machine or virtual machine for each branch. It is far more efficient to run several parallel feature environments on the same machine, each fully isolated through its own Compose project name. This lets multiple branches be built, tested and manually checked at the same time, without the environments interfering with each other.
This article shows how to technically implement parallel feature environments cleanly using COMPOSE_PROJECT_NAME, dynamic port assignment and Compose profiles, including an automation script that derives the environment directly from the current Git branch name.
2. COMPOSE_PROJECT_NAME as an isolation mechanism
Docker Compose internally uses a project name to uniquely label containers, networks and volumes. By default this name is derived from the directory name, which is unproblematic with a single checkout of the repository, but leads to collisions with multiple parallel checkouts of the same project. The environment variable COMPOSE_PROJECT_NAME explicitly overrides this automatic name and is the central building block for parallel feature environments.
If the project name is derived from the branch name, for example shop-feature-checkout instead of simply shop, Docker Compose automatically generates unique container, network and volume names with that prefix. Two checkouts of the same repository on different branches can then run docker compose up at the same time, without Docker overwriting or reusing the other branch's resources.
# .env file per checkout, derived from the current branch
COMPOSE_PROJECT_NAME=shop-feature-checkout
# Running compose now creates isolated resources:
# Network: shop-feature-checkout_default
# Volume: shop-feature-checkout_db_data
# Container: shop-feature-checkout-app-1
docker compose up -d
# A second checkout on a different branch uses a different name
# and never collides with the first environment
export COMPOSE_PROJECT_NAME=shop-feature-import
docker compose up -d
Important for parallel feature environments: the project name must be set consistently per checkout directory, otherwise Docker Compose falls back to the directory name on every call. The most reliable approach is a project local .env file in each checkout, not version controlled and generated individually per branch.
3. Dynamic port assignment per branch
Parallel feature environments also need unique ports besides unique names, otherwise the first environment started blocks the port for all the others. Instead of hardcoding fixed ports like 8080:80 in the Compose file, environment variables are referenced that are set differently per checkout. A simple but robust approach computes the port from a hash of the branch name, so every branch reproducibly gets the same port without developers having to memorize port numbers.
# docker-compose.yml — ports come from environment variables, not hardcoded
services:
app:
build: .
ports:
- "${APP_PORT:-8080}:80"
db:
image: mysql:8.4
ports:
- "${DB_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: secret
#!/usr/bin/env bash
# derive-ports.sh — deterministic port offset from branch name
set -euo pipefail
branch="$(git rev-parse --abbrev-ref HEAD)"
# Hash the branch name into a stable offset between 0 and 999
offset=$(( 0x$(echo -n "$branch" | md5sum | cut -c1-4) % 1000 ))
echo "APP_PORT=$((8080 + offset))"
echo "DB_PORT=$((13306 + offset))"
This script produces a reproducible but unique offset for every branch name, so feature-checkout always gets the same port no matter how many times the environment is restarted, while feature-import is guaranteed a different port. For parallel feature environments, this determinism property matters, because it keeps browser bookmarks and notes in team chat valid over time.
4. Combining profiles with project names for team specific stacks
Compose profiles allow making services optional, so a service only starts when its profile is explicitly activated. Combined with COMPOSE_PROJECT_NAME, this creates a powerful pattern for parallel feature environments: each branch not only gets its own container names and ports, but can additionally decide for itself which optional services it actually needs. A feature branch that only tests backend changes, for example, does not need to start an Elasticsearch container, while a branch with search functionality changes enables exactly that profile.
# docker-compose.yml — optional services gated behind profiles
services:
app:
build: .
ports:
- "${APP_PORT:-8080}:80"
db:
image: mysql:8.4
ports:
- "${DB_PORT:-3306}:3306"
search:
image: elasticsearch:8.15.0
profiles: ["search"]
ports:
- "${SEARCH_PORT:-9200}:9200"
mail-catcher:
image: axllent/mailpit
profiles: ["debug"]
ports:
- "${MAIL_PORT:-8025}:8025"
# Feature branch touching search functionality activates the "search" profile
export COMPOSE_PROJECT_NAME=shop-feature-search-relevance
export APP_PORT=8180 SEARCH_PORT=9300
docker compose --profile search up -d
# A plain backend feature branch skips search and mail-catcher entirely
export COMPOSE_PROJECT_NAME=shop-feature-checkout
export APP_PORT=8080
docker compose up -d
This combination significantly reduces unnecessary resource usage in parallel feature environments: Elasticsearch, by far the most memory hungry element of many Magento stacks, only runs where it is actually needed, while plain backend branches stay considerably leaner and use less RAM on the developer's machine.
5. Generating environments automatically per branch
So developers do not have to manually set project names and ports on every branch switch, it is worth writing a small wrapper script that takes care of the entire derivation. This script reads the current Git branch, computes the matching project name and ports, writes both into a local .env file and then starts the stack. This turns managing parallel feature environments into a single command instead of a list of manual steps.
#!/usr/bin/env bash
# feature-env.sh — one command per branch checkout
set -euo pipefail
branch="$(git rev-parse --abbrev-ref HEAD | tr '/' '-')"
project_name="shop-${branch}"
offset=$(( 0x$(echo -n "$branch" | md5sum | cut -c1-4) % 1000 ))
cat > .env <<EOF
COMPOSE_PROJECT_NAME=${project_name}
APP_PORT=$((8080 + offset))
DB_PORT=$((13306 + offset))
SEARCH_PORT=$((19200 + offset))
EOF
echo "[INFO] Starting environment '${project_name}' on port $((8080 + offset))"
docker compose up -d
Such a script, once added to the repository, makes parallel feature environments accessible to the entire team without every developer needing to understand the details of COMPOSE_PROJECT_NAME or port hashing. New team members simply run ./feature-env.sh and get an isolated environment for their current branch.
6. Avoiding network and volume naming collisions
Even with a correctly set project name, collisions can still occur if Compose files define explicit names for networks or volumes instead of letting them be derived automatically from the project name. A hardcoded name: shop_network in the networks section overrides the automatic prefixing and again causes conflicts with two parallel checkouts, because both environments try to claim the same explicitly named network.
# WRONG: explicit names bypass project-name prefixing
networks:
default:
name: shop_network # collides across all parallel environments
volumes:
db_data:
name: shop_db_data # collides across all parallel environments
# RIGHT: let Compose derive names from COMPOSE_PROJECT_NAME automatically
networks:
default: {}
volumes:
db_data: {}
For parallel feature environments, this is therefore a fixed rule: avoid explicit name keys on networks and volumes unless there is a deliberate reason to share a resource across multiple projects. Automatic name derivation from COMPOSE_PROJECT_NAME is the more robust choice in almost all cases.
7. Reliably cleaning up orphaned feature environments
The more parallel feature environments accumulate over the course of a week, the more important a reliable cleanup process becomes. Branches no longer needed are often deleted without anyone remembering to stop the associated Docker stack, so RAM and disk space slowly but steadily fill up with forgotten containers and volumes.
#!/usr/bin/env bash
# cleanup-feature-envs.sh — remove environments for deleted branches
set -euo pipefail
existing_branches=$(git branch --format='%(refname:short)' | tr '/' '-')
docker compose ls --format json | jq -r '.[].Name' | while read -r project; do
branch_part="${project#shop-}"
if ! echo "$existing_branches" | grep -qx "$branch_part"; then
echo "[CLEANUP] Removing orphaned environment: $project"
docker compose -p "$project" down --volumes --remove-orphans
fi
done
This script compares running Compose projects against Git branches that still exist and removes every environment whose branch was already deleted, including its volumes. As a weekly cron job or a manual call at the end of the day, it keeps resource usage of parallel feature environments permanently under control, without developers having to think about every single environment manually.
8. Using the same pattern for review apps in CI
The principle behind parallel feature environments transfers one to one to CI systems that provide a dedicated review app for every open merge request. The CI runner sets COMPOSE_PROJECT_NAME to a combination of project name and merge request number, letting multiple review deployments exist simultaneously on the same CI host or staging machine, following exactly the same isolation pattern as locally on the developer's machine.
This consistency between local workflow and CI pipeline has a practical extra benefit: developers already used to parallel feature environments locally understand the behavior of review apps in CI intuitively, because the same environment variables and the same naming scheme underlie both. This reduces questions within the team and makes troubleshooting failed review app deployments faster.
9. Approaches to parallel environments compared
There are several ways to make multiple branches testable at once, with notably different resource requirements and isolation levels.
| Approach | Isolation | Resource usage | Setup effort |
|---|---|---|---|
| One stack, serial | None, constant switching | Low | None |
| Project name plus profiles | Complete | Medium | Low to medium |
| Dedicated VM per branch | Complete | High | High |
The serial approach with a single stack saves resources but constantly costs time for switching and restarting. A full VM per branch offers maximum isolation but consumes unnecessarily large amounts of RAM and disk space for use cases that do not need such strict separation. Parallel feature environments via project name and profiles sit right in the middle: complete isolation at manageable resource cost.
Mironsoft
Docker Compose automation and multi branch workflows
Test multiple feature branches at once without chaos?
We set up Compose configurations with automatic project name derivation, dynamic port assignment and a profile strategy for your team, including cleanup automation and optional CI integration for review apps.
Isolation setup
Setting up COMPOSE_PROJECT_NAME, dynamic ports and profiles per branch
Automation scripts
Wrapper scripts for starting, stopping and cleaning up orphaned environments
CI integration
Using the same pattern for review apps in merge request pipelines
10. Summary
Parallel feature environments solve a problem every growing development team eventually runs into: several branches need to be runnable at the same time, without fighting each other over ports, databases or container names. COMPOSE_PROJECT_NAME as an explicit isolation mechanism, combined with dynamic port assignment and optional profiles for heavy services like Elasticsearch, makes exactly that possible, without running a dedicated virtual machine for every branch.
An automation script that derives the project name and ports directly from the Git branch name lowers the entry barrier for the whole team to a single command. A matching cleanup process prevents forgotten branches from permanently occupying resources. The same pattern transfers seamlessly into CI pipelines for review apps, making parallel feature environments a consistent concept across local development and CI.
Parallel Feature Environments — The Essentials at a Glance
COMPOSE_PROJECT_NAME
Set per checkout, automatically isolates containers, networks and volumes with a unique prefix.
Dynamic ports
Deterministically derived from the branch name, no manual memorizing of port numbers needed.
Profiles
Only activate heavy, optional services like Elasticsearch where they are actually needed.
Cleanup
Regularly comparing running Compose projects against existing branches prevents wasted resources.