Tutorial: Docker-Compose Deployment
Views: 197
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:inservicesis an arbitrary service namebuild:tells Docker Compose to build the image from the current directory using the localDockerfileimage:defines the image name in the formrepository/image:tag. The definition here uses the variables${REPOSITORY}and${TAG:-develop}.REPOSITORYrefers to a Docker Hub account like mwaeckerlin, or a private repository. TheTAGallows to specify a version, here it defaults todevelop
if not setports:maps local port4000to 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-worldonhttp://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-worldconnectstraefikwithhello-worldwordpressconnectstraefikwithwordpressdatabaseconnectswordpresswithdb
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: