Tutorial: Docker-Compose Deployment
Views: 8
After following the steps in Image from Dockerfile, where you created the Simple NestJS Project and the associated Dockerfile
, now we will deploy this using Docker-Compose. This will allow us to manage multiple services, including our NestJS application and WordPress.
Docker Compose has an extensible YAML format, which is defined in the Compose file reference.
Simple Application Deployment
Start with the simple project and Dockerfile
from the previous tutorial. Then in the same project path create a docker-compose.yaml
file to build and deploy the NestJS image with the following content:
services: hello-world: build: . image: ${REPOSITORY}/hello-world:${TAG:-develop} ports: - "4000:3000"
services:
declares all services in this deployment. For now, it is ourhello-world
service, which is the NestJS project we wrote in the last sessionhello-world:
inservices
is an arbitrary service namebuild:
tells Docker Compose to build the image from the current directory using the localDockerfile
image:
defines the image name in the formrepository/image:tag
. The definition here uses the variables${REPOSITORY}
and${TAG:-develop}
.REPOSITORY
refers to a Docker Hub account like mwaeckerlin, or a private repository. TheTAG
allows to specify a version, here it defaults todevelop
if not setports:
maps local port4000
to internal container port3000
, allowing access to the application
As you see, you can use variables everywhere in the Docker Compose. Variables may have default values, as in the bash
parameter expansion: ${VARIABLE_NAME:-default}
. These variables may be set in the environment, or in a .env
file, e.g. create a local file in the same project path named .env
containing:
REPOSITORY=mwaeckerlin TAG=latest
Instead of mwaeckerlin
you may set your own Docker Hub repository name, or any repository elsewhere. If it is not in Docker Hub, then the repository name requires the full URL.
Build and Deploy the Project
Once your docker-compose.yaml
and .env
are set up, build the image (if docker compose
does not work for you, try docker-compose
, that’s the deprecated command; if it complains something about version, add version: '3.8'
as the first line on top, that’s also deprecated syntax):
docker compose build
This builds the image. Last line you see if you copied the above .env
file is: naming to docker.io/mwaeckerlin/hello-world:latest
. As you see, the repository URL docker.io
is implicit default.
After building, you can start all services in the file, option -d
starts them detached in the background:
docker compose up -d
Now you have started your service, which listens on port 5000
, so head your browser to http://localhost:5000 to get the output of your service.
Check the logs of your service with docker compose logs -tf
, where -t
adds a time stamp and -f
follows the logs (continues logging output until the services stopped). In the logs, you see all of stdout
and stderr
.
You can now stop your service with docker compose stop
, start again with docker compose start
, or remove it with docker compose rm -vfs
, where the options -v
also removes anonymous (temporary) volumes, -f
prevents asking to confirm and -s
stops running processes, so removes all.
If you want to push the built image to the repository, simply call docker compose push
.
The option --help
is your friend, be it docker compose --help
to see all commands or docker compose up --help
to see all options of a specific command. Have fun!
Don’t forget to clean your system from time to time, e.g. with docker system prune
. You may add it to a cron job.
Extended Docker-Compose with WordPress and MySQL
Next, we will expand the docker-compose.yaml
to add WordPress, MySQL, and Traefik as a reverse proxy.
First we add three new variables to the `.env` file, a database name, a user name and a password for the wordpress database, so change your .env
file to:
REPOSITORY=mwaeckerlin TAG=latest WORDPRESS_DB_NAME=wordpress WORDPRESS_DB_USER=wordpress WORDPRESS_DB_PASSWORD=S3crE7P@55w0rd
Of course in real life, you would change a more secure password, and you would never store a file containing the password anywhere in an unprotected location, such as a git repository, but you would rather use secrets.
Now you may change and extend the docker-compose.yaml
file to:
services: traefik: image: traefik command: - "--log.level=DEBUG" - "--api.insecure=true" - "--providers.docker=true" - "--entrypoints.web.address=:80" ports: - "9000:80" - "9001:8080" networks: - hello-world - wordpress volumes: - /var/run/docker.sock:/var/run/docker.sock hello-world: build: . image: ${REPOSITORY}/hello-world:${TAG:-develop} labels: - "traefik.http.routers.hello-world.rule=Host(`localhost`) && PathPrefix(`/api`)" - "traefik.http.services.hello-world.loadbalancer.server.port=3000" - "traefik.http.middlewares.hello-world-strip-path.stripprefix.prefixes=/api" - "traefik.http.routers.hello-world.middlewares=hello-world-strip-path" networks: - hello-world wordpress: image: wordpress environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: WORDPRESS_DB_PASSWORD: WORDPRESS_DB_NAME: WORDPRESS_CONFIG_EXTRA: | define('WP_SITEURL', 'http://localhost:9000/wordpress'); define('WP_HOME', 'http://localhost:9000/wordpress'); labels: - "traefik.http.routers.wordpress.rule=Host(`localhost`) && PathPrefix(`/wordpress`)" - "traefik.http.services.wordpress.loadbalancer.server.port=80" - "traefik.http.middlewares.wordpress-strip-path.stripprefix.prefixes=/wordpress" - "traefik.http.routers.wordpress.middlewares=wordpress-strip-path" networks: - wordpress - database depends_on: - db db: image: mysql environment: MYSQL_RANDOM_ROOT_PASSWORD: "yes" MYSQL_DATABASE: ${WORDPRESS_DB_NAME} MYSQL_USER: ${WORDPRESS_DB_USER} MYSQL_PASSWORD: ${WORDPRESS_DB_PASSWORD} volumes: - db_data:/var/lib/mysql networks: - database networks: hello-world: driver_opts: encrypted: 1 wordpress: driver_opts: encrypted: 1 database: driver_opts: encrypted: 1 volumes: db_data:
Try it:
docker compose build docker compose up
Then you may load:
hello-world
onhttp://localhost:9000/api
- WordPress on
http://localhost:9000/wordpress
- Traefik dashboard on
http://localhost:9001
There is no more port opened for hello-world, so http://localhost:4000
does not work anymore. This removal is optional, if you keep the port definition, you can access hello-world
also on http://localhost:4000
, in addition to http://localhost:9000/api
.
Now to the details of the configuration: A simple WordPress installation on http://localhost:9000
with a MySQL database would just be:
services: wordpress: image: wordpress ports: - "9000:80" environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_NAME: WORDPRESS_DB_USER: WORDPRESS_DB_PASSWORD: depends_on: - db db: image: mysql environment: MYSQL_RANDOM_ROOT_PASSWORD: "yes" MYSQL_DATABASE: ${WORDPRESS_DB_NAME} MYSQL_USER: ${WORDPRESS_DB_USER} MYSQL_PASSWORD: ${WORDPRESS_DB_PASSWORD} volumes: - db_data:/var/lib/mysql volumes: db_data:
The WORDPRESS_CONFIG_EXTRA
is required to place WordPress into /wordpress
path.
Traefik needs the local docker socket /var/run/docker.sock
to be mounted. This allows Trafik to scan other services for traefik related labels to setup the routing. So the labels starting with traefik.
are used by Traefik.
This connects hello-world on the host name localhost
to path /api
, forwards to port 3000
, where the process runs and strips /api
from the path:
labels: - "traefik.http.routers.hello-world.rule=Host(`localhost`) && PathPrefix(`/api`)" - "traefik.http.services.hello-world.loadbalancer.server.port=3000" - "traefik.http.middlewares.hello-world-strip-path.stripprefix.prefixes=/api" - "traefik.http.routers.hello-world.middlewares=hello-world-strip-path"
Same here for WordPress, this connects WordPress on the host name localhost
to path /wordpress
, forwards to port 80
, where the process runs and strips /wordpress
from the path:
labels: - "traefik.http.routers.wordpress.rule=Host(`localhost`) && PathPrefix(`/wordpress`)" - "traefik.http.services.wordpress.loadbalancer.server.port=80" - "traefik.http.middlewares.wordpress-strip-path.stripprefix.prefixes=/wordpress" - "traefik.http.routers.wordpress.middlewares=wordpress-strip-path"
As you see, we now define three networks, all encrypted:
hello-world
connectstraefik
withhello-world
wordpress
connectstraefik
withwordpress
database
connectswordpress
withdb
So hello-world
can neither connect to wordpress
not to db
, these services are segregated, which is an important security feature.
The database stores data in a locally mounted volume, so the data even survives a deletion and recreation of the db
container. Just don’t delete the volume:
volumes: - db_data:/var/lib/mysql
If a variable is not defined, then it is set by an environment variable of the same name, such as in:
environment: WORDPRESS_DB_NAME: WORDPRESS_DB_USER: WORDPRESS_DB_PASSWORD: