We were all wondering by now how to create our own images. All the time we are working with pre-built images. In this article we will cover subjects like how to create your own custom image and multi-tier applications which runs as a service. Talking about multi-tier we will show you some cool features which you can use when building multi-tier apps. So let’s start and create Docker image.

Docker build image

There are two ways of creating custom images: commit operation inside container itself and Dockerfile. First option is not recommended because you rely on parent image and already prebuilt container. In other words you run the container, did some changes and run the commit command. It is not so happy solution if you want clean image which was built from beginning.


Create Docker image with Dockerfile is preferred option when you want to build your own images. Focus is on quality parent image – maintainability is then very easy especially if parent image is kept clean by its author. Motivation of building custom images include: adding runtime libraries and SSL certifications. Changing the Dockerfile can trigger updates like removal of unwanted libraries. It is not possible to use inline comments and keywords are not word sensitive.

Base or parent image?

Parent image is prepacked image which is source on which other layers are built.

For example, RHEL or Centos have their base images which are used in first line of Dockerfile with word FROM:


FROM Centos8:8.1


Base image has no parent image in FROM but uses minimal image that is required to build container. It uses FROM scratch directive:


FROM scratch

Scratch base image can be found on Public Docker repository – Docker Hub.

Steps to follow

Create working directory

Create Dockerfile specs

Docker build image with Docker command

Dockerfile format

# Comment


Example (Apache web server running on CentOS)

#This is a comment (1)

FROM centos:latest (2)

LABEL decription “Apache server” (3)

RUN yum install -y httpd; yum clean all; systemctl enable httpd.service (4)

COPY index.html /var/www/html/ (5)

EXPOSE 80 (6)


ENTRYPOINT [“/usr/sbin/httpd”] (8)

CMD [“-D”, “FOREGROUND”] (9)

  1. This is comment
  2. Parent container to build you custom image is latest version of Centos
  3. This is just metadata describing image
  4. RUN command executes command in /bin/sh shell adding extra layer above first layer of parent image. Notice that there are three commands separated by semicolon (;). In this way you will avoid creating two additional layers with RUN command. This is recommended way of creating custom images and avoiding extra layers and performance issues.
  5. COPY command copies local file which is located on host to new image.
  6. EXPOSE command exposes apache server port 80 to host port 80 for service to be visible to external world.
  7. Set environment variable – APACHEVERSION.
  8. ENTRYPOINT defines which command will be executed by default when container starts. By default command /bin/sh –c. You want httpd command to start immediately.
  9. CMD provides arguments for the ENTRYPOINT instruction


Create working directory Dockerfile under your home path. Inside directory create Dockerfile with content above. Inside same directory create another file index.html with following content:


<!doctype html>



<title>Welcome to apache server!</title>



<p>Hi! I am apache server!</p>




FROM centos:latest (2)

LABEL decription “Apache server” (3)

RUN yum install -y httpd; yum clean all; systemctl enable httpd.service (4)

COPY index.html /var/www/html/ (5)

EXPOSE 80 (6)


ENTRYPOINT [“/usr/sbin/httpd”] (8)

CMD [“-D”, “FOREGROUND”] (9)

Run the Docker build command. Parameter –f indicated which file to use. Parameter –t will name the image because you can’t name image inside Dockerfile. Dot (.) tells Docker to search for file inside current directory:

[root@swmanager dockerfiles]# docker build -f Dockerfile –t apacheserver .

Sending build context to Docker daemon 3.072kB

Step 1/8 : FROM centos:latest

Removing intermediate container 094156371bd1

—> f4db33717fe8

Step 4/8 : COPY index.html /var/www/html/

—> 29536db0b52b

Step 5/8 : EXPOSE 80

—> Running in cea0bbe2bb13

Removing intermediate container cea0bbe2bb13

—> b52bb72e819d


—> Running in 3b2353124159

Removing intermediate container 3b2353124159

—> a5579f1922d7

Step 7/8 : ENTRYPOINT [“/usr/sbin/httpd”]

—> Running in 686bc014a689

Removing intermediate container 686bc014a689

—> c0f10b14cb95

Step 8/8 : CMD [“-D”, “FOREGROUND”]

—> Running in ee59e916572f

Removing intermediate container ee59e916572f

—> dd803595bc27

If you have latest CentOS image locally, Docker will use it. Otherwise it will try to fetch it from Docker Hub site.

Check that both images are now on your local system:

[root@swmanager dockerfiles]# docker images


apacheserver latest dd803595bc27 17 seconds ago 237MB

centos latest 831691599b88 5 weeks ago 215MB

Run the apache server container from image:

[root@swmanager dockerfiles]# docker run -dit -p 80:80 apacheserver


Check that container is running:

[root@swmanager dockerfiles]# docker ps


b921ced4b400 apacheserver “/usr/sbin/httpd -D …” 3 seconds ago Up 1 second>80/tcp ecstatic_chaplygin

Browse the home page (it should display message from custom index.html above):

[root@swmanager dockerfiles]# curl

<!doctype html>



<title>Welcome to apache server!</title>



<p>Hi! I am apache server!</p>



Multi-container applications

In multi-container world you can merge two or more containers into the one stack. Imagine classic three layer stack with front-end, business and database layer. You would normally start these services individually, configuring their endpoints and establishing communication between layers. Here you can define which type of containers will be part of multi-container world, network, storage, ports, environment variables and many more. You build multi-tier with docker compose. But you can always start containers individually with Docker run but this is more convenient way because you have dynamic file where you put all configuration.

Due its dynamic nature, multi-containers bring challenge in the form of IP addressing. Each time container is stopped and started there is no guarantee that it will get the same IP address. That’s way it is important to establish DNS services. Remember the network part where containers which use default bridge network do not have DNS services. Only containers with custom user network use built-in DNS server. Docker compose by default will always create custom network.

Docker compose

Compose is a tool for defining and running multi-container Docker applications. Compose is software as well as YAML file to configure your application layers. Dockerfile can be used in production, staging, development, testing, as well as CI workflows.

To build multi-tier container you need:

1) Dockerfile

2) Docker-compose file

3) Docker-compose up command to start building

version: “3”




– “80:80”


image: mysql


– “3306:3306”


Building services with Docker compose install default network if not specified. Default network is built from scratch and includes project name (directory where Docker compose file is located), underscore and word default: projectname_default. In this way all containers are reachable on that network by hostname defined in file, there is no firewall between them. You can always use pre-existing network.

Docker environment variables

Docker environment variables is just one of the options you can put in Docker compose file. There are various way to set variables but two main options are direct assignment or assignment by file:

Direct assignment:




Assignment by file:



– web-variables.env


Assignment during docker run:

Docker-compose run -e DEBUG=1 web python

Multiple Compose files

If you use different environments and workloads you can use multiple Compose files. By default docker Compose runs two files: docker-compose.yml and docker-compose.override.yml. First one contains base configuration and second one includes specific configured based on environment. By convention, the docker-compose.yml contains your base configuration. -f option to specify the list of files.

Extending services

If your environment has more services that have common set of configuration data, you can use feature which is named extending services. You can set options in one place and use it anywhere. Your shared configuration is some kind of template.





file: common.yml

service: app



build: .


– “5000:5000”


– “/var/lib/data”

This instructs web service to use all configuration defined by common.yml file.


In the following example you will learn how to deploy multi-tier application which consists of two images:

  • automation_db (simple web application coded in Python to add,delete,update data)
  • mysql:5.7 (mysql database which holds records about data)

Image automation_db was created with following Dockerfile:


FROM python:alpine3.7

COPY . /app


RUN pip install -r requirements.txt


ENTRYPOINT [ “python” ]

CMD [ “app/” ]

Docker file builds custom image from parent image python: alpine3.7. Copies app directory with source code and set app as working directory. After it installs all requirements which are defined in requirements.txt file. Exposes port 5000 to outside and defines app/ as starting point when run image.

Docker compose file created service and merged two container together:

version: “2”



image: automation:latest


– db


– “5000:5000”


image: mysql:5.7


– “3306:3306”



MYSQL_DATABASE: automation_db


– /mnt/mysql/mysql57:/var/lib/mysql

File consists of two containers: app and db. App container uses previously build image which we called automation. It links to db container and exposes port 5000 to outside.

Second container -db uses clean mysql: 5.7 image and exposes port 3306 to outside. There are two environment variables which are set to successfully start mysql:

MYSQL_ROOT_PASSWORD: password (defines root password)

MYSQL_DATABASE: automation_db (defines pre-defined database)

Last entry defines volume path where mysql container reads data. Path already has automation_db database created.

Two things to consider:

What about network?

When you don’t specify network in docker compose file, Docker compose will create custom network. For example if docker-compose.yml file is in /home/john/ directory, custom network john_default will be created. Both containers will join this network and each container can now look up the hostname web or db and get back the appropriate container’s IP address. There is no firewall between containers.

Link keyword in docker compose file

Link keyword has two functions: dependency and extra alias by which service is reachable. In our example, for app to start successfully it is mandatory to have running database. In this case first database is up and then app is connecting to database. It is worth to mention that compose does not wait till container is ready, only running. For mysql full availability you have to define extra logic. Second function is extra alias which is just one way for services to reach itself by names.

Proof of concept

1 Download two containers to your local system: automation

and mysql data files.

2. Upload two files to your test Linux machine with Docker installed

3. Download the mysql: 5.7 container to your local repository:

docker pull mysql:5.7

4. Unzip mysql data to /mnt/mysql directory

[root@swmanager tmp]# mkdir /mnt/mysql

[root@swmanager tmp]# unzip -d /mnt/mysql


creating: /mnt/mysql/mysql57/

extracting: /mnt/mysql/mysql57/auto.cnf

creating: /mnt/mysql/mysql57/automation_db/

inflating: /mnt/mysql/mysql57/automation_db/db.opt

inflating: /mnt/mysql/mysql57/automation_db/esxi.frm

inflating: /mnt/mysql/mysql57/automation_db/esxi.ibd

inflating: /mnt/mysql/mysql57/ca-key.pem

inflating: /mnt/mysql/mysql57/ca.pem

inflating: /mnt/mysql/mysql57/client-cert.pem

inflating: /mnt/mysql/mysql57/client-key.pem

inflating: /mnt/mysql/mysql57/ibdata1

inflating: /mnt/mysql/mysql57/ib_buffer_pool

inflating: /mnt/mysql/mysql57/ib_logfile0

inflating: /mnt/mysql/mysql57/ib_logfile1

creating: /mnt/mysql/mysql57/mysql/

Data will be stored in following path: /mnt/mysql/mysql57

5. Load the automation_db image:

[root@swmanager tmp]# docker load -i automation.tar

629164d914fc: Loading layer [==================================================>] 4.464MB/4.464MB

50f8b07e9421: Loading layer [==================================================>] 838.7kB/838.7kB

9b77965e1d3f: Loading layer [==================================================>] 73.31MB/73.31MB

88e61e328a3c: Loading layer [==================================================>] 4.608kB/4.608kB

5fa31f02caa8: Loading layer [==================================================>] 6.453MB/6.453MB

f5b2455b79ed: Loading layer [==================================================>] 96.77kB/96.77kB

c5a6e8eabb7a: Loading layer [==================================================>] 28.24MB/28.24MB

Loaded image: automation:latest

6. Load the mysql container

[root@swmanager tmp]# docker load -i mysql.tar

95ef25a32043: Loading layer [==================================================>] 72.49MB/72.49MB

492e36400248: Loading layer [==================================================>] 338.4kB/338.4kB

b279ae737dff: Loading layer [==================================================>] 9.539MB/9.539MB

10263d924b2a: Loading layer [==================================================>] 4.2MB/4.2MB

ec9e20671283: Loading layer [==================================================>] 1.536kB/1.536kB

7dbd11e8dc05: Loading layer [==================================================>] 53.75MB/53.75MB

4fb0c46aefac: Loading layer [==================================================>] 6.656kB/6.656kB

49368a7f4157: Loading layer [==================================================>] 3.584kB/3.584kB

fb6af29036b4: Loading layer [==================================================>] 313.4MB/313.4MB

f803b994f6aa: Loading layer [==================================================>] 16.38kB/16.38kB

a52072ac0f07: Loading layer [==================================================>] 1.536kB/1.536kB

7. Check that images are stored on your local system:


automation latest a24edd32a29c 36 minutes ago 108MB

mysql 5.7 8679ced16d20 7 days ago 448MB

8. Install docker-compose

[root@swmanager ~]# curl -L “$(uname -s)-$(uname -m)” -o /usr/local/bin/docker-compose

% Total % Received % Xferd Average Speed Time Time Current

Dload Upload Total Spent Left Speed

100 638 100 638 0 0 2133 0 –:–:– –:–:– –:–:– 2133

100 11.6M 100 11.6M 0 0 570k 0 0:00:20 0:00:20 –:–:– 700k

Add permissions:

[root@swmanager ~]# chmod +x /usr/local/bin/docker-compose

9 . Start docker compose in directory where Docker-compose file is:

[root@swmanager ~] docker-compose up

Creating network “root_default” with the default driver

Creating root_db_1 … done

Creating root_app_1 … done

Attaching to root_db_1, root_app_1

db_1 | 2020-08-04 19:52:50+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.31-1debian10 started.

db_1 | 2020-08-04 19:52:51+00:00 [Note] [Entrypoint]: Switching to dedicated user ‘mysql’

db_1 | 2020-08-04 19:52:51+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.31-1debian10 started.

db_1 | 2020-08-04 19:52:51+00:00 [Note] [Entrypoint]: Initializing database files

db_1 | 2020-08-04T19:52:51.355285Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use –explicit_defaults_for_timestamp server option (see documentation for more details).

db_1 | 2020-08-04T19:52:52.116427Z 0 [Warning] InnoDB: New log files created, LSN=45790

db_1 | 2020-08-04T19:52:52.311565Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.

db_1 | 2020-08-04T19:52:52.461787Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 1504b770-d68c-11ea-91b3-0242ac130002.

db_1 | 2020-08-04T19:52:52.479514Z 0 [Warning] Gtid table is not ready to be used. Table ‘mysql.gtid_executed’ cannot be opened.

db_1 | 2020-08-04T19:52:56.696757Z 0 [Warning] CA certificate ca.pem is self signed.

db_1 | 2020-08-04T19:52:57.975162Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the –initialize-insecure option.

app_1 | * Serving Flask app “__init__” (lazy loading)

app_1 | * Environment: production

app_1 | WARNING: This is a development server. Do not use it in a production deployment.

app_1 | Use a production WSGI server instead.

app_1 | * Debug mode: on

app_1 | * Running on (Press CTRL+C to quit)

app_1 | * Restarting with stat

app_1 | * Debugger is active!

app_1 | * Debugger PIN: 213-953-578

10. Verify that both containers are up and running:

[root@swmanager ~]# docker ps


0a4845b3b33f automation:latest “python app/__init__…” 5 days ago Up 3 minutes>5000/tcp root_app_1

0788f12440c9 mysql:5.7 “docker-entrypoint.s…” 5 days ago Up 3 minutes>3306/tcp, 33060/tcp root_db_1

11. Start your favorite browser from host machine and enter ip address of virtual machine which is hosting containers: IPADDRESS: 5000. If you have installed docker on host just enter:

Image 1 Web App Frontend

12. Select ESXI and if database connection is successful you should see following screen:

Image 2 Web App Database Connection Test