This content originally appeared on DEV Community and was authored by Altair Lage
Docker Compose: Speed Up Your Workflow with Profiles, Extends and Depends_on
If you’re reading this article, you’ve probably found yourself wrestling with a docker-compose.yml
, right?
Whether you’re a backend developer, DevOps, cloud engineer, or a Docker beginner, as a project grows it’s common for Docker Compose to start showing day‑to‑day challenges. Maybe you have services that are only needed occasionally (like debugging or testing tools), repeated configurations across different services, or you run into errors because one container started before another was ready. Fortunately, Docker Compose has advanced features to deal with these scenarios and make your development environment more organized and flexible. Three of the main features are profiles, extends and depends_on. That’s what we’ll talk about in this article.
In this article, we’ll go through each of these features with explanations and practical examples. You’ll learn to use profiles to enable/disable optional services as needed, extends to prevent redundancy and repetition (the DRY – Don’t Repeat Yourself – principle), and to manage container startup sequencing with depends_on. We’ll also share best practices and how these resources can simplify your team’s workflow.
Profiles: Controlling Optional Services in Docker compose
In many projects, beyond the core services—like your web app and your database—there are auxiliary services used only in specific situations: for example, an integration test container, a monitoring tool, or a debugging utility.
Including all these services in Docker compose and starting them every time can waste resources and time. That’s where profiles in Docker compose come in.
The profiles feature lets you activate services selectively. You can assign one or more profiles to certain services so they only start when the corresponding profile is enabled. Services without a profile defined always start, while those with a profile only come up if you explicitly request that profile.
This is excellent for keeping both essential services and optional ones (debug, tests, etc.) in the same Docker compose file without having them running all the time.
Imagine you have a main application and a database that should always run in development, but you also have a container to run integration tests and another for a fake SMTP server (to capture emails in dev) that aren’t essential. We can mark the non‑essential services as optional:
version: "3.9"
services:
app:
image: my-web-app:1.0
# ... (web application configurations, ports, volumes, etc.) e.g.:
# ports:
# - "8000:8000"
# depends_on:
# - db
db:
image: postgres:15
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=passwd
# Database service always needed in development
tests_runner:
image: my-tests:latest
profiles: ["test"]
# Non-essential service to run on-demand integration tests
mailhog:
image: mailhog/mailhog
profiles: ["devtools"]
# Optional service to capture and view emails sent by the app in dev
In the YAML above, app
and db
don’t have a profile, therefore they will always be started. The tests_runner
is associated with the "test"
profile, and mailhog
with the "devtools"
profile. That means that by default those two won’t be executed — unless you explicitly specify them.
In practice, you can control this via the Docker Compose CLI:
- Running
docker compose up
(with no profiles) would start only the default services, i.e.,app
anddb
. - Running
docker compose --profile test up
would startapp
,db
and also thetests_runner
service (because the test profile was enabled). - Running
docker compose --profile devtools up
would startapp
,db
andmailhog
. You can even enable multiple profiles at once (for example,docker compose --profile test --profile devtools up
to include both) or usedocker compose --profile "*"
to bring up all services from all profiles.
This way, profiles let you keep a single Docker Compose file with everything your project might need, while turning services on or off as necessary. That keeps your dev environment lighter and faster day‑to‑day. For example, one team member can run docker compose up
and focus on the essentials, while another, debugging a specific issue, can enable the devtools
profile to see detailed logs or capture emails.
Important best practices:**
Do not put your core containers into a profile. Leave them without a profile so they always start by default. Reserve profiles for optional components or scenario‑specific ones (like test, debug, monitoring, etc.).
Name profiles clearly: Choose names that make the purpose obvious (e.g., test
, dev
, debug
, monitoring
, ci
). Avoid generic terms so the whole team understands when to use them. Document in the project README which profiles exist and how to activate them.
Reusing Configurations with extends and Eliminating Repetition
As your Docker Compose configuration grows, you may notice several services sharing common settings. For example, imagine we have two services inside a web application: a web service and a background worker, both using the same base image, mounting the same code volume, and needing the same environment variables, such as database connection strings and credentials. Repeating identical blocks of configuration for each service makes the Compose file long, repetitive, and harder to maintain: any change requires editing multiple places, going against the DRY (Don’t Repeat Yourself) principle.
To avoid duplication, Docker Compose lets you extend common configurations across services. The extends
feature works like inheritance: you define a base service (which can even be in another file) with shared options, and then other services “inherit” from that base, overriding or adding parameters as needed. In YAML, you can also use anchors and aliases to achieve a similar effect in a simple way within the same file.
Example: we have two services, web and worker, that share most of the config. Let’s create a reusable base configuration and apply it to both:
# We define a YAML anchor with the common configurations
x-base-service: &common_config
image: my-web-app:1.0
volumes:
- .:/app # mounts the current directory into the container (useful for development)
environment:
- DATABASE_URL=postgres://user:passwd@db:5432/mydb
restart: "always"
# ... (any other common option, e.g., network, logging configs, etc.)
services:
web:
<<: *common_config # Imports all configs defined in common_config
command: npm start # Specific command to run the web application
depends_on:
- db # (example: web depends on db running)
worker:
<<: *common_config # Reuses the same base configuration
command: npm run worker # Specific command to run the background worker
db:
image: postgres:15
# ... (database configurations)
In the snippet above, we use a special x-base-service
key at the top of the YAML to define a block of common configurations identified by &common_config
. Then, in the web and worker services, we use <<: *common_config
to merge those shared configurations into each service. That way, both get image
, volumes
, environment
, and restart
from the template, and we add only what changes in each one. In this case, the specific command, and in the web service, the database dependency.
If tomorrow you need to change an environment variable or a logging option for all services, just edit it once (in the anchor block) and both the web and worker containers will automatically receive the update. Much simpler than remembering to change it in two or three places.
The native extends directive
Compose also supports the native extends
directive. With it, you can, for example, have a common-services.yml
file that defines a base service, and then, in your docker-compose.yml
, make another service extend that external definition. The effect is the same: reusing configurations. This even lets you override some values as needed.
Example:
Imagine that the common-services.yml
in our example has the following configuration:
services:
webapp:
build: .
ports:
- "8000:8000"
volumes:
- "/data"
You could then extend these configurations in your docker-compose.yml
using the extends
directive in the desired services:
services:
web:
build: alpine
command: echo
extends:
file: common-services.yml
service: webapp
webapp:
extends:
file: common-services.yml
service: webapp
You’ll get exactly the same result as if you had written a docker-compose.yaml
with the same configuration values for build, ports and volumes directly—i.e., “hardcoded.”
Use whichever approach you prefer: YAML anchors are great within a single file, while extends
shines when splitting Compose files across multiple files.
Workflow benefits: by eliminating duplication, you reduce errors and keep the Compose configuration cleaner. In development teams, this means everyone shares consistent configurations. If multiple microservices use the same image or variables, you ensure they’re all using exactly the same values. Plus, the file becomes smaller and easier to understand—new developers can quickly identify what’s common to all services and what’s specific to each.
Orchestrating Startup Order with depends_on
Another classic challenge in multi‑container environments is ensuring that certain services only start when others are already ready. Imagine your web API depends on a database. If the API container comes up before the database is up, the app will likely fail to connect and its startup may be compromised. These situations are frustrating, but Docker Compose helps with the depends_on
parameter.
depends_on
lets you declare explicit dependencies between services, making Compose start containers in the correct order. In the short syntax, you simply list the names of the services another depends on, and Compose will ensure it starts the “dependency” containers first and stops them last. For example:
services:
api:
image: my-api:latest
depends_on:
- db
- redis
db:
image: mysql:8.0
redis:
image: redis:7-alpine
In the example above, when you run docker compose up
, Compose will start db
and redis
first, and only then start the api
container. Likewise, when stopping containers, it will stop api
before bringing down db
and redis
. This helps avoid many issues related to startup and shutdown sequencing.
But what if we want to make sure the database is **actually**** ready for connections before starting the API?** The good news is that recent versions of Compose support the long syntax of depends_on
, which has conditions. We can specify a condition like service_healthy
to indicate that the depended‑on service only counts as “ready” when its healthcheck is OK. Let’s improve the example by adding a healthcheck
to the database:
services:
api:
image: my-api:latest
depends_on:
db:
condition: service_healthy # wait for db healthcheck to pass
redis:
condition: service_started # wait for redis to start (container started)
# ... (rest of API config, ports, etc.)
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] # checks if MySQL responds
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
# (we could include a healthcheck here as well, if we want to monitor readiness)
Now, Compose will wait for the MySQL healthcheck to succeed — in this case, the mysqladmin ping
command indicating the server is responding—before starting the api
service.
For Redis, we use service_started
simply to ensure the container has started. There’s no wait for “Healthy”, because we might not have defined a healthcheck
for it.
This setup reflects a common scenario: wait for the database to become available and also ensure the Redis cache has started before the application comes online. In practice, api
will only begin initializing when Compose verifies that db
is healthy and redis
is already running, avoiding immediate “could not connect to database” failures.
Important tips:
To use condition: service_healthy
, make sure to define an adequate healthcheck
in the dependency service. Otherwise Compose has no way to know the health state and will treat the container as ready as soon as the process starts.
In the example, we used a native MySQL command (mysqladmin ping
) to check availability. For PostgreSQL, for example, there’s pg_isready
.
Also keep in mind that although Compose waits for a successful healthcheck, it’s recommended that your application also implement connection retry logic. That gives you extra robustness in case of unexpected conditions—for example, a slight delay even after the healthcheck or a brief connection drop. In short, depends_on
handles the initial orchestration (and already helps a lot), but good resilience practices in the application are always welcome.
In a team development context, using depends_on
, especially with healthchecks, standardizes how everyone brings the environment up. New developers don’t need to “guess” the order to start each service manually or run wait scripts. A simple docker compose up
already does everything in the right sequence. This greatly reduces the “works on my machine” class of errors caused by startup race conditions, making everyone’s workflow more reliable.
How These Features Help Teams
Profiles = Flexibility for different scenarios:
They let each team member run only the set of services needed for their task. This speeds up the development cycle. For example, running integration tests without bringing up the entire stack, or running debugging tools only when you’re going to use them. Teams can define standard profiles (likedev
,test
,debug
) and thus avoid the need for multiple Compose files for environment variations. The result is a customizable yet centralized environment in a single file.
Extends = Consistent configuration and less repeated code:
By reusing configurations withextends
or anchors, you ensure all related services share identical parameters where it makes sense: same base image, same credentials, same logging policies, etc. This avoids discrepancies that could cause “it works here but not there.” Also, reducing repetition makes editing the Compose faster. A change to a common port or variable reflects everywhere, saving time and avoiding oversights. In teams, this consistency means fewer configuration bugs and a smoother onboarding for anyone reading/editing Compose for the first time.
Depends_on = Ordered startup and fewer failures:
With well‑defined dependencies—and using healthcheck conditions when applicable—the process of starting the environment becomes much more reliable. Developers don’t need to take manual steps to ensure “the database is up” before running the application, for example. In CI pipelines and local demos, everything comes up in the right order automatically. This reduces time lost with trivial troubleshooting like “oh, never mind, you just had to start service X first…” and keeps the focus on the application logic.
In short, these advanced Docker Compose features act as small productivity multipliers for teams: less time spent tweaking configuration and more time on the application itself.
Final tip
- Keep your Docker Compose up to date: Make sure you’re using a recent version to access new features, bug fixes and, most importantly, to maintain compatibility with the Docker engine and avoid runtime issues!
This content originally appeared on DEV Community and was authored by Altair Lage