Dockerize Anything
Dockerizing has become the de facto standard for shipping code. Learn how to containerize your application to prevent any future surprises while deploying or distributing for development.
Docker helps you package and ship your application in a way that anyone with Docker installed on their system can run it.
FAQ
- How is Docker different than a Virtual Machine?
The major difference is how both solutions isolate your application. A virtual machine creates its own OS where it simulates both the kernel and the OS, however, Docker just virtualizes the application layer and is mainly dependent on the Host to talk to the kernel.
- What is the difference between
Dockerfile
anddocker-compose
.?
A Dockerfile is a bunch of instructions on how to create an image for the application you're building. docker-compose
is used to run containers in the form of services in their own isolated environment.
To further clarify, think of your production and development environment. In your production environment, your database is most likely a service you're opting for from one of the cloud providers, and in this case, your docker image needs only to be set up with your actual application, maybe an Nginx to act as a web server. In this case, a Dockerfile is most useful as it will generate an image for only the essential components for going to production.
In your development environment, however, you'd be better off hosting a database instance locally to minimize the latency and obviously the cost. In this case, you should rely on docker-compose to spin up both your database and application containers.
Common Terminologies
- What is an Image?
- An image is a snapshot of a packaged application. Think of it as an
.exe
. - What is a Container?
- A container is currently an image that is running/executing. Think of this as running the aforementioned
.exe
.
Installing Docker and Docker Compose
To install Docker on Linux, a simple sudo apt install docker.io
should work. For Windows you just have to download the binary and double-click on it. Hope you can manage that.
Also, do install docker-compose while you're at it. For that navigate to their official repository's release page, select the latest version, and choose the correct binary for your OS. If you're on Linux, it should be docker-compose-linux-x86_64
. For Windows, it's going to be the one with the .exe
extension.
Overview of the application to be packaged
I'll be using a Flask application written in Python to demonstrate how to set up Docker. Please clone it from my Github repository if you'd like to tinker with it yourself.
Note - Don't be alarmed if you don't know Python or Flask, they're just tools to explain how Docker works.
Take this for example, this is the code for app.py
which serves as the entry point for our application. It creates API endpoints.
This may look familiar to my friends using Express js. The function def index
returns JSON {"hello": "world"}
if you hit the root of the application. Keep in mind the actual app.py is a little bit more complex where it connects with a Database and returns entries from the database in the form of JSON. All of this will be explained later.
Start Packaging
The 5 lines of code above are all that is required to start packaging the application. You'll need to create a few things before we start. Create a docker-compose.yml
file at the root of your project. Create a directory called docker and place the following files in it.
Dockerfile.local
- Instruction to create an image.
nginx.default
- Configuration for the nginx server to act as a reverse proxy.
start_server.sh
- The script will actually start the application server, create database migrations, and whatever else that's required by your application to run smoothly.
After doing all this, this is how your project directory will look like.
docker-compose.yml
Here's a step-by-step explanation of everything that's going on here.
Database Section
- We use the
postgres
version 14 image as our database instance. - Name the container "mtmm-postgres"
volumes
part is important, here we linkdata/postgres
directory inside ourdocker
folder so that all the data saved in the DB doesn't get reset every time you start and stop the container.environment
is where we specify the username and passwords we want to set when the database image boots up for the first time.
Web App Section
- We specify the version of docker-compose we want to use
Services
is the umbrella that holds all the containers together. Here you'll define anything your application might need, currently, it's just instructions to build your web app, but it can include a Database (which is demoed below), a Redis server, an Elastic Search whatever else you can think of.web_app
is the first service we want to create, you can name it whatever you like and this is the meta of things.container_name
is pretty self-explanatory, it's going to be the name of the container once it runs.context
- You specify the base directory and the point of reference from where the config will start looking if any specific file is mentioned. This is the root of your project.docker file
- This specifies where the Dockerfile is located that compose will use to create the image, and later run it.working_dir
- Sets the directory where your application gets copied too. More on this later.volumes
- This acts as a persistent data store for your container. You're telling it to use the "volume" as its hard drive.ports
- Connecting a host port with a container port.HOST_PORT
:CONTAINER_PORT
Keep in mind that under the services section, we have set the database
as our key, which will also serve as the host of the application. For example, when connecting to the DB, you will specify database
your host, and since your application is running as a container too, it'll be resolved. However, if you try to connect from your host machine to this database
named host, it'll fail because it doesn't have Docker's context.
Dockerfile.local
- You use the python3.10 image built on Debian as the base.
- You can pass a bunch of arguments when building your image, to parse the argument, you use the ARG keyword then the name of the argument.
- ENV helps you set the environment variable in the docker image you'll build. Here we store the argument
DEVELOPMENT_ENVIRONMENT
as an environment variable with the same name. RUN
is an image-building command, you can use it to install packages, copy files, and much more. It alters the state of the image.CMD
is the command that is used to start the container. Have it at the end to run whatever script is required to start your project.
nginx.default
Here we have nginx that's set up to listen to port 5000, as you'll recall above, it's the same port we're opening in our docker image.
server_name
- can be anything, doesn't matter.access_log
andserver_log
- where all the logs for any incoming requests will be stored.location
is a directive used to specify which configuration block will take over for any particular URL, and URIs. For example, here, for any request that comes in we pass it to the local server running at port 5010 which is where our actual project will be running.proxy_pass
is just passing incoming requests to another server.
start_server.sh
It's a simple bash script that runs a gunicorn server, it's a WSGI (Web Server Gateway Interface) that you use in production to handle incoming traffic more efficiently. The nginx command tells the server to stay in the foreground. This is usually used in docker images as one container usually runs one service. However, a bare metal server may be running a bunch of them where you'd need to send Nginx to the background. For more details about this, please have a look at this link.
We're finally ready to run the server.
Running the Docker container
To start the Docker container you enter the following command
That's all that's needed. In case you're already in the docker group you won't need to use sudo
.
Building Docker Image Manually
You'll be building an image when deploying to production. Ideally you should have a CI / CD pipeline setup for this, but that's beyond the scope of this blog post.
Here's how you can build an image for yourself.
This will generate an image for your project.
Running an image as a container
Pretty simple to run any existing image.
Debugging Docker
There's a good chance you'll have to get a shell inside the docker container that is running for some debugging purpose, so here's how to do that.
- List all the docker containers currently running.
- Getting a shell on one of the containers.
And that's all folks! I know the first time you look at all this setup it seems a little daunting but keep in mind all of this is laying a solid foundation for your project to prepare for the future. This will help you when you have to distribute the software to a bunch of developers or deploy it rapidly to production. Happy dockerizing!