Docker For Beginners - Creating Your First Docker Container
Host A Django App With Docker Container - Learning By Doing
Introduction
Awesome folks, we're back for part two! Today, we'll build our first Docker container and run a Django to-do app with MySQL and Nginx containers! If you have not read my first blog on Docker, where I delve deep into Docker and Virtual Machines, please read it here and then proceed with this!
I hope you have gone through my introductory blog, to understand what Docker is and its industry applications. Now, let's dive into creating Docker containers—this is where the real fun begins!
We will first install Docker, create our first sample, and then Python and MySQL containers. After that, we will host a Django todo app with Nginx and MySQL using Docker containers. Throughout, you'll get familiar with Docker Hub, pulling images, creating standalone Docker containers and common Docker commands, and using Dockerfile to optimize our todo app setup!
That being said, let’s jump in and install Docker on our system!
Here is the sample Django Todo application that we are going to host using Containerization.
The frontend code is minimal, as I am not very familiar with expert frontend coding; I hope you don't mind the simplicity!
Installation
Installing Docker can be a bit tricky! To run Docker, we require a Linux environment. Given that many of us use the Windows operating system, which doesn't directly support a Linux environment, we need to utilize the WSL feature provided by Windows to create one. In such a scenario, you should have at least 8 GB of memory in your system; otherwise, running Docker might be challenging. If your system meets the requirements, please follow along with these resources (Install Docker on Windows, Install Docker on Windows 2, Docker Official Doc, What is WSL - Windows Official Doc, What is WSL - YT) to enable WSL on your Windows operating system.
Alternatively, I have a better solution—using a remote machine! Yes, you can create a Linux instance either in AWS called EC2 or in Azure called Virtual Machine, or in GCP. All you need is a debit card to create an account on any of the mentioned platforms and set up your virtual machine. You can then use the instance as if it were your own computer, and the best part is, that you get 12 months of free service, so don't worry about creating one! Please follow this tutorial to set up a Linux environment in AWS EC2.
For Windows users, I hope you have configured either WSL or created a virtual machine in any cloud platform. If you are using a Mac or any Linux distro (we will follow Ubuntu in this tutorial), you are awesome!
Open your terminal, or if you are using a cloud solution, connect with your instance as shown in the 11th point in the mentioned tutorial and follow along with the below commands. You can also use MobaXterm to connect with the cloud instance. See this tutorial to connect your cloud instance with MobaXterm.
A. Update the Ubuntu Package List
This will update your Ubuntu machine to later security versions and updates for various system packages.
sudo apt update && sudo apt upgrade -y
B. Add Docker Repository
Although we can install Docker directly from Ubuntu’s system repository, we would go with Docker’s official repository. This would help us to update Docker in real-time in future updates.
Install Common Ubuntu’s Required Packages
sudo apt install -y ca-certificates curl gnupg lsb-release nano
Add Docker’s GPG Key. Run the both commands
sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Add the Official Docker Repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Refresh and System Running the Update Command
sudo apt update
C. Install The Docker
If we have correctly followed the above commands, we have rightly configured the necessary files required to get the latest Docker version in our system. Now, install the Docker and its other tools.
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
D. Check The Version
It’s time to check the version to see whether Docker is successfully installed in our system or not.
sudo docker --version
E. Add Ubuntu User to Docker Group
By this time, you are good to go to use Docker but you have to use sudo
it each time you give any command to Docker. Sometimes this is irritating, so we would add the current user to the Docker group so that we do not need to add sudo
every time.
sudo usermod -aG docker $USER
Check if our current user is in the Docker group, run
id $USER
You will see the result below
ubuntu@ip-172-31-15-44:~$ id $USER uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),119(netdev),120(lxd), 999(docker)
By this time, we need to disconnect from the instance if you are using a cloud instance, or we can reload the shell session of docker to make a final effect by running the below command
newgrp docker
F. Check The Docker Daemon Status
systemctl status docker
If you receive results something like below, congratulations, you have successfully installed Docker in your system!
ubuntu@ip-172-31-15-44:~$ docker -v
Docker version 24.0.7, build afdd53b
ubuntu@ip-172-31-15-44:~$ systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2024-01-23 11:39:39 UTC; 3 days ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 549 (dockerd)
Tasks: 18
Memory: 36.0M
CPU: 26.134s
CGroup: /system.slice/docker.service
└─549 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Jan 23 11:39:36 ip-172-31-15-44 dockerd[549]: time="2024-01-23T11:39:36.702225463Z" level=info msg="Starting up"
Jan 23 11:39:39 ip-172-31-15-44 systemd[1]: Started Docker Application Container Engine.
G. Test Docker Creating a Sample Container
docker run hello-world
hello-world is a testing image from Docker to test Docker installation. If you see the below lines, your Docker setup is completed!
ubuntu@ip-172-31-15-44:~$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Create Your First Container
Now that we've successfully installed Docker on our system, let's dive into the most awaited step - creating our first Docker container. In this section, we will pull a Python slim version image from the Docker Hub. Docker Hub is like GitHub for code, but it hosts Docker images.
This is the Python image on Docker Hub. If you carefully observe, there is an Overview and Tag tab. The overview page contains necessary information on how to use the image, and the Tag section contains various versions of the image, each tailored for specific needs with varying sizes.
For our first Docker container, we'll use a Python image with a slim tag. A slim tag is a lightweight version with only the necessary files and may lack additional dependencies, but it's perfect for our first Docker container (slim versions are relatively smaller in size).
A. Python Container
Similar to copying code from Github using git pull
or git clone
, in the Docker ecosystem, we have a similar process. After choosing the image with the appropriate tag, let's pull it into our system with the following command:
docker pull python:3.9.18-slim
Ensure you have an active internet connection as this will download the image from Docker Hub. Now, with the image in our system, let's create a container with the following command:
docker run -it --name my-first-python-container python:3.9.18-slim
Congratulations on creating your first useful Docker container! Now, you have a container running Python 3.9, ensuring compatibility with the Python version on your host computer or virtual machine.
The docker run
command is used to create a Docker container with an image. The --name
tag allows attaching a custom name to the container and -it
creates an interactive terminal for running Python code.
Now, you will see something like below:
ubuntu@ip-172-31-15-44:~$ docker run -it --name my-first-docker-container python:3.9.18-slim
Python 3.9.18 (main, Dec 19 2023, 03:57:15)
[GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
# This is the Python environment and you can run any Python code here!
>>> name = "my first docker container"
>>> print(name)
my first docker container
>>> a = 10
>>> b = 20
>>> c = a + b
>>> print("a + b = ", c)
a + b = 30
>>>
# run ctrl + d to exit the Python environment.
Run the below command and you will see a result something like this. This command shows all the containers in the system.
docker ps -a
#result
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fb7f45724f06 python:3.9.18-slim "python3" 3 minutes ago Exited (0) 2 minutes ago my-first-docker-container
B. MySQL Container
Enough on Python; let's dive into some data! Now, we'll create another Docker container, this time for a database - MySQL! First, we'll pull version 8.0 of MySQL, and then we'll create a container running a database isolated from our host system. This way, we can run multiple containers of the same service with various versions within the same host system without interference.
Let's create a MySQL container:
docker run --name my-first-mysql-container -e MYSQL_ROOT_PASSWORD=my-pass -d mysql:8.0
The MYSQL_ROOT_PASSWORD
is an environment variable for the root password of the container, and my-pass
is the chosen password. Feel free to select any password, but remember it, as we'll need it while logging into the MySQL administration. Running docker ps
will show that the MySQL container is running. What's the difference between docker ps
and docker ps -a
? The former shows only running containers, and the latter shows all containers in the system.
Now, let's enter the MySQL administration to run some SQL commands:
docker exec -it my-first-mysql-container mysql -u root -p
# Enter the password you set in MYSQL_ROOT_PASSWORD environment variable
Enter password:
# Explanation
docker exec -it <container_name or container_id> mysql -u root -p | where exec to enter into a container, mysql is the command, -u as the user -p to prompt password
After running the command, you'll be prompted to enter the password you set while creating the MySQL container, and you'll be logged into the MySQL administration!
ubuntu@ip-172-31-15-44:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ed083df47163 mysql:8.3.0 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp my-first-mysql-container
ubuntu@ip-172-31-15-44:~$ docker exec -it my-first-mysql-container mysql -u root -p
Enter password: (enter the password)
# MySQL Administration
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.3.0 MySQL Community Server - GPL
Copyright (c) 2000, 2024, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
Let's delve into a sample MySQL operation. If you're familiar with SQL, this will be a piece of cake for you. However, if MySQL is new to you, don't worry—it's a straightforward query language designed for interacting with relational databases.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)
mysql> create database mydb;
Query OK, 1 row affected (0.01 sec)
mysql> use mydb;
Database changed
mysql> create table students (
-> id int auto_increment primary key,
-> name varchar(50) not null,
-> roll varchar(3) not null
-> );
Query OK, 0 rows affected (0.03 sec)
mysql> insert into students(name, roll)
-> values("Your Name", "101");
Query OK, 1 row affected (0.00 sec)
mysql> select * from students;
+----+-----------+------+
| id | name | roll |
+----+-----------+------+
| 1 | Your Name | 101 |
+----+-----------+------+
1 row in set (0.00 sec)
mysql> show tables;
+----------------+
| Tables_in_mydb |
+----------------+
| students |
+----------------+
1 row in set (0.00 sec)
mysql>
You've just crafted a MySQL container, established a database, added data to a table, and queried that data—all within the container. The fun part is that your host system remains completely unaware of these operations!
Common Docker Commands
Here are some common Docker commands you will need in this tutorial.
Create Container - docker run
Stop Container - docker stop container_name or container_id
Delete Container - docker rm container_name or container_id
Create Image - docker build
Delete Image - docker rmi image_name or image_id
Host A Django APP with Containers
Now that we've created two Docker containers, let's venture into something more exciting - the Dockerfile.
The two containers we created utilized pre-existing images from the Docker Hub. However, this time, we aim to craft our own image and run a container based on that image! Interesting? Let's dive in!
In this section, we'll host a Django todo app with Nginx and a MySQL database using Dockerfile. For this, we'll require three containers, each for a specific service. While Dockerfile is one way to build custom images, the preferred method is to use docker-compose to build multi-container applications. However, understanding docker-compose requires familiarity with Dockerfile. We'll optimize the containerization process with docker-compose for more simplicity in my next article, but for now, let's stick with Dockerfile!
A. Dockerfile and Create Necessary Files
Dockerfile empowers us to create custom Docker images. We choose a base image, and on top of it, our customization takes place. In this Dockerfile, we'll copy any additional dependencies we might have. Typically, we create a file in the directory where our code resides named Dockerfile
. Inside this file, we specify other dependencies the image might need. To create an image from the Dockerfile, use the command docker build -t my-image .
, where -t
assigns a name to our image, and the dot ( . )
specifies the directory where Docker will search for the Dockerfile to create an image. Since we want to build three custom images, we'll follow this naming style: Dockerfile-serviceName
. To build this styled image, use the -f
flag (as the Dockerfile name is not exactly Dockerfile, we need to use -f
to let Docker know that we are using a custom Dockerfile name) along with the full Dockerfile name to create the image.
For example, if we want to build a custom image of MySQL, we'll name it Dockerfile-mysql
and build the image with this command: docker build -t my-sql-image -f Dockerfile-mysql .
Clone The Django Todo Code
Here is the repository to get the sample Django Todo App code. Fork the repository on your GitHub account, and then clone it in your workspace.
https://github.com/Mahboob-A/docker-django-todo
Now that you've cloned the mentioned repository from GitHub, navigate to docker-django-todo
(using cd) and then into our working directory TodoApp
. Type ls
to ensure you see TodoApp and app
directories.
The File Structure
Create .env File for MySQL to Connect the Database from the Django Application
Type nano .env
, a text editor will open and paste the following environment variables for our MySQL database. You can use Ctrl + Ins
to paste the code since Ctrl + V
won’t work in this case.
To exit the editor, press Ctrl + X
, then press Y
, and finally, press Enter
. Whenever you edit any text using the nano
text editor, this is the way you save the text. If you just want to see what’s inside a text document, you can simply run nano filename
, and if you have not edited anything, then you can simply press Ctrl + X
to exit the editor.
You can always change the password and database name, but in that case, you'll also need to change them in the docker run
command while creating the MySQL container. For simplicity, let's go along with the configuration I have set. I recommend you review all the commands I provide in all the below sections.
DB_NAME=mydatabase
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=my-mysql-container
DB_PORT=3306
Create a Custom Bridge Network
If you carefully observe the DB_HOST
in the env file, the value is a container name. This is the magic of Docker; if some containers are within a custom Docker network, we can communicate with them via the container name along with its IP address. If we do not want to use a custom Docker network, then we have to use the IP address of the MySQL container, which will be a bit complex if you are just starting. We'll discuss Docker Network more in my next article, but for now, let's create a custom bridge network for simplicity:
docker network create blog-network
Create .dockerignore
Similar to how .gitignore serves in the case of Git, .dockerignore serves similarly. In the .dockerignore file, we instruct Docker to skip certain files from being copied into the container.
Run nano .dockerignore
and paste the following:
Dockerfile-django
Dockerfile-nginx
Dockerfile-mysql
.gitignore
README.md
B. Create Dockerfiles for Custom Images
Now, let's create three Dockerfiles, each for a service we want to run. Since we have a Django application and we plan to use MySQL as the database while employing Nginx as a proxy to serve the Django application, let's start by creating these three Dockerfiles.
Create MySQL Dockerfile
Run nano Dockerfile-mysql
, and a text editor will open. Paste the following code.
# Using the official 8.0 MySQL image as the base image
FROM mysql:8.0
# Set environment variables
ENV MYSQL_DATABASE mydatabase
ENV MYSQL_USER myuser
ENV MYSQL_PASSWORD mypassword
ENV MYSQL_ROOT_PASSWORD myrootpassword
# Expose the MySQL port to access it from outside the container
EXPOSE 3306
We are using mysql:8.0
as our base image. We need a minimum of MySQL with 8.0 to run MySQL in our project by the time I am writing this article. Then we set the environment variables to access the database and expose the default port 3306 to access the container. I have added the necessary comments so that you can understand each line.
Create Django Dockerfile
Run nano Dockerfile-django
in the same way and paste the following text.
# Using the official Python 3.10 runtime as a base image
FROM python:3.10
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Set the working directory to /DjangoApp (In this directory, all our code will reside)
WORKDIR /DjangoApp
# Copy the current directory contents into the container's working directory at /DjangoApp
# Make sure you are at the right directory at this time. (run ls and see you can see TodoApp and app directory)
COPY . /DjangoApp
# Install needed packages specified in requirements.txt
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# Copy the .env file into the container
COPY .env /DjangoApp/.env
# Expose port 8000 to the outside world
EXPOSE 8000
# Define the default command to run when the container starts
# We are using the Gunicorn wsgi server to run our Django application
CMD ["gunicorn", "TodoApp.wsgi:application", "--bind", "0.0.0.0:8000"]
We are using the Python 3.10 version as the base image. Then, we create a directory in our container and copy all the content from our host’s current directory into the directory inside the container. Then we are installing all the packages to run the Django application and running the WSGI server.
Create Nginx nginx.conf File
Now, we need to create a file that is needed to run Nginx is nginx.conf
. In this file, we will place all the necessary information Nginx needs to know to forward the request to the correct Django container.
Run nano nginx.conf
and paste the following text.
server {
listen 80;
server_name your_host_ip_address:81;
location / {
proxy_pass http://my-django-container:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Nginx that will run inside the Docker container is listening to your_host_ip and port 81. It maps to port 80 of the Docker container. I am mapping port 81 to port 80 of the Docker container only to show you that you can run as many Nginx instances on your computer using Docker by only changing the host’s port. If we were not using Docker, we would not be able to listen to port 80 with more than one instance. But as we are using Docker containers, we can listen to port 80 to all the containers at the same time with mapping to different ports of the host. In the Location section, see that I am again using a container name with port 8000 to forward the request. Do you remember I told you we can communicate with the container name if we use a custom Docker network?
These are the privileges we get using Docker.
If you are using an AWS or Azure instance, to get the IP address, log into your account and you will find the IP of the instance under the overview or security tab.
Create Nginx Dockerfile
Nginx will serve as a proxy to our server. In simple terms, Nginx will forward any request it receives host_ip_address:81
from the browser, and forward the request to the container that is running the Django application. Let’s create the Nginx Dockerfile to make our custom Nginx container.
Run nano Dockerfile-nginx
, and a text editor will open. Paste the following text using ctrl + ins
and exit.
# Using the official latest Nginx image
FROM nginx:latest
# Remove the default Nginx configuration
RUN rm /etc/nginx/conf.d/default.conf
# Copy your custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/
# Expose ports to map with the host's port.
# For HTTP
EXPOSE 80
# For HTTPS
EXPOSE 443
We are using the latest Nginx image, removing the default default.conf
file, copying our nginx.conf
file to the appropriate directory, and exposing port 80 and port 443 to connect the container from the outside world.
C. Customize Djangosettings.py
Before creating images from the Dockerfiles, we need to customize Django’s Allowed Host so that it can allow our server. Open settings.py
and add your host’s IP address to the ALLOWED_HOST
list (The IP address of your virtual machine or your computer’s IP in case you are using WSL).
You also need to add the CSRF_TRUSTED_ORIGINS
. This line tells Django that any request coming from the address in the list is genuine and should not be flagged as a CSRF failure. If we do not add this line, we won’t be able to fill any form in the TodoApp. We have to add this line because we are mapping the host’s port (81) and the container’s port differently (80).
Update ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS
Make sure you add it like the below example -
ALLOWED_HOSTS = ['host_ip', '127.0.0.1']
CSRF_TRUSTED_ORIGINS = ['http://host_ip:81']
# Example
ALLOWED_HOSTS = ['2.16.10.121', '127.0.0.1']
CSRF_TRUSTED_ORIGINS = ['http://2.16.10.121:81']
The settings.py ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS configuration example
D. Create Images From the Dockerfiles
As we have defined our custom Dockerfiles for all our services and added our host IP to Django’s ALLOWED_HOST, it’s time to create the images.
Create the following images by running the following commands.
Create MySQL Image
docker build -t my-mysql-image -f Dockerfile-mysql .
By this command, we are telling Docker to create an image using the Dockerfile Dockerfile-mysql
(using -f
for the custom Dockerfile name) which is located in the current directory (.
) and name the image as my-mysql-image
(-t
). This same goes for the following commands.
Create Django Image
docker build -t my-django-image -f Dockerfile-django .
Create Nginx Image
docker build -t my-nginx-image -f Dockerfile-nginx .
Now, you have successfully created images for MySQL, Django, and Nginx using the respective Dockerfiles.
E. Create Containers from the Custom Images
Finally, let's create the containers and witness the magic happen!
Create MySQL Container
Running the MySQL container would require a minimum of 400 to 500 MB of RAM. If you are using a cloud instance that provides an instance with only 1 GB RAM for free, you might experience lag in the MobaXterm display and the container might not work properly. This is common. If you are running any additional services on your instance, consider stopping them for this purpose.
To run the MySQL container, execute the following command:
Note: If you have altered the MySQL env values while creating the .env file, then add them in the following command accordingly.
docker run -d -p 3306:3306 --name my-mysql-container --network blog-network -e MYSQL_ROOT_PASSWORD=myrootpassword -e MYSQL_DATABASE=mydatabase -e MYSQL_USER=myuser -e MYSQL_PASSWORD=mypassword my-mysql-image
We are instructing Docker to run the container in the background (-d
), map the host’s 3306 port to the container’s 3306 port (as exposed in Dockerfile-mysql), assign a custom name to our container with --name
, attach our container to the custom Docker network for easy communication between containers (--network
), and set environment variables using -e
to connect to the database. These environment variables are specified in the .env
file created earlier for the Django application to connect to this database. Finally, we specify to use our custom MySQL image (my-mysql-image
) to create the container.
Create Django Container
Remember, after creating the MySQL container, create the Django container before the Nginx container, as the Nginx container depends on Django’s 8000 port. Further details will be covered in my next article, where I'll explore creating the same application using Docker Compose with a more straightforward process.
docker run -d -p 8000:8000 --name my-django-container --network blog-network my-django-image
Migrate the Database
With the Django container running, it's time to migrate the MySQL database. Enter into the Django container using the command:
docker exec -it my-django-container /bin/bash
Once inside the container, run the following commands to migrate the database:
python3 manage.py makemigrations
python3 manage.py migrate
The database configuration
The migration command in the terminal
Create Nginx Container
docker run -d -p 81:80 --name my-nginx-container --network blog-network my-nginx-image
The Three Running Containers
Access the Django Application
Access the Django application using the host_ip:81
port, and you will see the application is up and running.
Congratulations! You have successfully deployed your first Containerized Django application with MySQL as the database and Nginx as the proxy. It's time to treat yourself!
Conclusion
You’ve nailed it! You've officially graduated from the Dockerfile and Docker Container tutorial, and your live Django app is the graduation certificate! If this is your first go at Docker, you've made impressive progress. While I've strived to cover as much ground as possible, some concepts may have been briefly touched upon, but there might be a few bits you'd like to explore more. Take this achievement as a starting point.
To deepen your knowledge, check out the Docker documentation for details on the commands we've used. The best way to really get the hang of it is to try running the application again with fewer references to this tutorial. Dockerfile specifics can be found in this Docker documentation.
In the next blog, we'll make Docker even simpler by using Docker Compose to host the same Django application. We'll also dig into Docker Network. Until then, keep pushing, keep fixing, keep building, god bless.