Dockerfile and Docker Compose
Learning objectives
- You know what a
Dockerfile
is and what adocker-compose.yml
file is. - You follow guidelines for setting up an application that runs within a container.
- You can build, start, and stop a containerized application with Docker Compose.
Applications that run within a Docker container use a Dockerfile
that contains the commands needed for creating the docker image and for launching the application. In applications that have multiple containers, a Docker Compose file is used for managing all the containers at the same time.
Dockerfile
We first add a Dockerfile to the project. A Dockerfile is a text file that contains the commands that are executed when creating a docker image. A docker image is a template that contains instructions for creating a runnable container. For example, one can have an image for Alpine Linux operating system and use that to create and run multiple Alpine containers. Create a file called Dockerfile
to the folder api
and copy the following contents to the file Dockerfile
.
FROM denoland/deno:alpine-1.42.2
WORKDIR /app
COPY deps.js .
RUN deno cache deps.js
COPY . .
EXPOSE 8000
CMD [ "run", "--allow-net", "--unstable", "--watch", "app-run.js" ]
What the above Dockerfile effectively does is that it downloads an Alpine Linux based Docker image from https://hub.docker.com/r/denoland/deno as the starting point for the image and creates a folder app
to the image, setting that as the current working directory. Then, the file containing the dependencies is copied to the image, after which the dependencies are cached. This is followed by copying all the files from the image to the folder, exposing the port 8000 from the image, and stating the command for running the image. The base image includes deno
as entrypoint, so the given instructions after CMD
are passed to the deno
executable.
Effectively, the last line above translates to the command deno run --allow-net --unstable --watch app-run.js
.
At this point, the folder structure of the project is as follows:
tree --dirsfirst
.
└── api
├── app.js
├── app-run.js
├── deps.js
└── Dockerfile
MacOS & ARM64
If you're using a Mac with an ARM64 processor, you need to use the lukechannings/deno:v1.42.2
image instead of the denoland/deno:alpine-1.42.2
image. The denoland/deno:alpine-1.42.2
image is not available for ARM64 processors. That is, if you have an M1 or M2 processor, change the first line of the above Dockerfile to FROM lukechannings/deno:v1.42.2
.
FROM lukechannings/deno:v1.42.2
# ...
Each docker image is run in a container -- let's next look at how this is done.
Docker Compose
Docker images are typically run with Docker Compose, which is a tool that helps running Docker applications with multiple containers. In order to use Docker Compose, we need to create a YAML file called docker-compose.yml
. The file will contain information on all the containers that are run in the project.
Create a file called docker-compose.yml
to one directory higher than the folder api
. After creating the file, the file structure should be as follows.
tree --dirsfirst
.
├── api
│ ├── app.js
│ ├── app-run.js
│ ├── deps.js
│ └── Dockerfile
└── docker-compose.yml
Copy the following contents to the file docker-compose.yml
.
services:
api:
build: api
restart: unless-stopped
volumes:
- ./api:/app
ports:
- 8000:8000
This effectively states that we currently have one container -- a service called api
. The service is built from the folder api
, and it is restarted unless it is stopped (restart: unless-stopped
). The volumes
section states that the folder api
is mounted to the folder /app
in the container -- in effect, any changes that we do in our local file system are reflected to the container. The ports
section states that the port 8000 is exposed from the container as the port 8000 in the operating system.
Running with Docker Compose
Once the files are saved, we can run the application with Docker Compose (using the command docker compose
). This is done as follows. We first check that we are in the correct directory and that the docker-compose.yml
file is in the current directory. Then, we run docker compose up --build
.
tree --dirsfirst
.
├── api
│ ├── app.js
│ ├── app-run.js
│ ├── deps.js
│ └── Dockerfile
└── docker-compose.yml
1 directory, 5 files
docker compose up --build
(lots of stuff)
docker-project-api-1 | Watcher Process started.
docker-project-api-1 | Watcher Process finished. Restarting on file change...
Now, after waiting for a few moments, the application is running.
curl localhost:8000
Hello World!
We can stop the application using Ctrl + C in the terminal where we called the docker compose command. Running the command docker compose stop
in the project folder explicitly stops the containers created by the command docker compose up
.
docker compose stop
What happened?
When we ran the command docker compose up --build
, we stated that the containers outlined in the docker-compose.yml
file should be built and started. If we would have omitted the --build
flag, we would have used the images that have previously been built (if any existed) to launch the container -- if none exist, Docker may ask whether these images should be built.
Now, in principle, you have an application that you could ship as an image. We'll continue on this in the subsequent parts, which will be released during the next week.