10 KiB
Extending services in Compose
Docker Compose's extends
keyword enables sharing of common configurations
among different files, or even different projects entirely. Extending services
is useful if you have several applications that reuse commonly-defined services.
Using extends
you can define a service in one place and refer to it from
anywhere.
Alternatively, you can deploy the same application to multiple environments with a slightly different set of services in each case (or with changes to the configuration of some services). Moreover, you can do so without copy-pasting the configuration around.
Understand the extends configuration
When defining any service in docker-compose.yml
, you can declare that you are
extending another service like this:
web:
extends:
file: common-services.yml
service: webapp
This instructs Compose to re-use the configuration for the webapp
service
defined in the common-services.yml
file. Suppose that common-services.yml
looks like this:
webapp:
build: .
ports:
- "8000:8000"
volumes:
- "/data"
In this case, you'll get exactly the same result as if you wrote
docker-compose.yml
with that build
, ports
and volumes
configuration
defined directly under web
.
You can go further and define (or re-define) configuration locally in
docker-compose.yml
:
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
You can also write other services and link your web
service to them:
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
links:
- db
db:
image: postgres
For full details on how to use extends
, refer to the reference.
Example use case
In this example, you’ll repurpose the example app from the quick start guide. (If you're not familiar with Compose, it's recommended that you go through the quick start first.) This example assumes you want to use Compose both to develop an application locally and then deploy it to a production environment.
The local and production environments are similar, but there are some
differences. In development, you mount the application code as a volume so that
it can pick up changes; in production, the code should be immutable from the
outside. This ensures it’s not accidentally changed. The development environment
uses a local Redis container, but in production another team manages the Redis
service, which is listening at redis-production.example.com
.
To configure with extends
for this sample, you must:
-
Define the web application as a Docker image in
Dockerfile
and a Compose service incommon.yml
. -
Define the development environment in the standard Compose file,
docker-compose.yml
.- Use
extends
to pull in the web service. - Configure a volume to enable code reloading.
- Create an additional Redis service for the application to use locally.
- Use
-
Define the production environment in a third Compose file,
production.yml
.- Use
extends
to pull in the web service. - Configure the web service to talk to the external, production Redis service.
- Use
Define the web app
Defining the web application requires the following:
-
Create an
app.py
file.This file contains a simple Python application that uses Flask to serve HTTP and increments a counter in Redis:
from flask import Flask from redis import Redis import os app = Flask(__name__) redis = Redis(host=os.environ['REDIS_HOST'], port=6379) @app.route('/') def hello(): redis.incr('hits') return 'Hello World! I have been seen %s times.\n' % redis.get('hits') if __name__ == "__main__": app.run(host="0.0.0.0", debug=True)
This code uses a
REDIS_HOST
environment variable to determine where to find Redis. -
Define the Python dependencies in a
requirements.txt
file:flask redis
-
Create a
Dockerfile
to build an image containing the app:FROM python:2.7 ADD . /code WORKDIR /code RUN pip install -r requirements.txt CMD python app.py
-
Create a Compose configuration file called
common.yml
:This configuration defines how to run the app.
web: build: . ports: - "5000:5000"
Typically, you would have dropped this configuration into
docker-compose.yml
file, but in order to pull it into multiple files withextends
, it needs to be in a separate file.
Define the development environment
-
Create a
docker-compose.yml
file.The
extends
option pulls in theweb
service from thecommon.yml
file you created in the previous section.web: extends: file: common.yml service: web volumes: - .:/code links: - redis environment: - REDIS_HOST=redis redis: image: redis
The new addition defines a
web
service that:- Fetches the base configuration for
web
out ofcommon.yml
. - Adds
volumes
andlinks
configuration to the base (common.yml
) configuration. - Sets the
REDIS_HOST
environment variable to point to the linked redis container. This environment uses a stockredis
image from the Docker Hub.
- Fetches the base configuration for
-
Run
docker-compose up
.Compose creates, links, and starts a web and redis container linked together. It mounts your application code inside the web container.
-
Verify that the code is mounted by changing the message in
app.py
—say, fromHello world!
toHello from Compose!
.Don't forget to refresh your browser to see the change!
Define the production environment
You are almost done. Now, define your production environment:
-
Create a
production.yml
file.As with
docker-compose.yml
, theextends
option pulls in theweb
service fromcommon.yml
.web: extends: file: common.yml service: web environment: - REDIS_HOST=redis-production.example.com
-
Run
docker-compose -f production.yml up
.Compose creates just a web container and configures the Redis connection via the
REDIS_HOST
environment variable. This variable points to the production Redis instance.Note: If you try to load up the webapp in your browser you'll get an error—
redis-production.example.com
isn't actually a Redis server.
You've now done a basic extends
configuration. As your application develops,
you can make any necessary changes to the web service in common.yml
. Compose
picks up both the development and production environments when you next run
docker-compose
. You don't have to do any copy-and-paste, and you don't have to
manually keep both environments in sync.
Reference
You can use extends
on any service together with other configuration keys. It
always expects a dictionary that should always contain two keys: file
and
service
.
The file
key specifies which file to look in. It can be an absolute path or a
relative one—if relative, it's treated as relative to the current file.
The service
key specifies the name of the service to extend, for example web
or database
.
You can extend a service that itself extends another. You can extend
indefinitely. Compose does not support circular references and docker-compose
returns an error if it encounters them.
Adding and overriding configuration
Compose copies configurations from the original service over to the local one,
except for links
and volumes_from
. These exceptions exist to avoid
implicit dependencies—you always define links
and volumes_from
locally. This ensures dependencies between services are clearly visible when
reading the current file. Defining these locally also ensures changes to the
referenced file don't result in breakage.
If a configuration option is defined in both the original service and the local service, the local value either overrides or extends the definition of the original service. This works differently for other configuration options.
For single-value options like image
, command
or mem_limit
, the new value
replaces the old value. This is the default behaviour - all exceptions are
listed below.
# original service
command: python app.py
# local service
command: python otherapp.py
# result
command: python otherapp.py
In the case of build
and image
, using one in the local service causes
Compose to discard the other, if it was defined in the original service.
# original service
build: .
# local service
image: redis
# result
image: redis
# original service
image: redis
# local service
build: .
# result
build: .
For the multi-value options ports
, expose
, external_links
, dns
and
dns_search
, Compose concatenates both sets of values:
# original service
expose:
- "3000"
# local service
expose:
- "4000"
- "5000"
# result
expose:
- "3000"
- "4000"
- "5000"
In the case of environment
and labels
, Compose "merges" entries together
with locally-defined values taking precedence:
# original service
environment:
- FOO=original
- BAR=original
# local service
environment:
- BAR=local
- BAZ=local
# result
environment:
- FOO=original
- BAR=local
- BAZ=local
Finally, for volumes
and devices
, Compose "merges" entries together with
locally-defined bindings taking precedence:
# original service
volumes:
- /original-dir/foo:/foo
- /original-dir/bar:/bar
# local service
volumes:
- /local-dir/bar:/bar
- /local-dir/baz/:baz
# result
volumes:
- /original-dir/foo:/foo
- /local-dir/bar:/bar
- /local-dir/baz/:baz