Docker can seem intimidating at first , but it’s actually a powerful tool that simplifies application deployment and management. This tutorial will walk you through creating a Dockerfile from scratch, demonstrating how easily you can containerize an application. We’ll build on the concepts of Docker Compose (How to use docker compose and why use it) to show how this streamlines your workflow.

The steps outlined below were performed on Ubuntu 24.04.2 LTS

What is a Dockerfile?

A Dockerfile is simply a text file containing instructions for building a Docker image. Think of it as a list of steps that tells Docker exactly how to create your containerized environment. This ensures consistency and repeatability, eliminating the “it works on my machine” problem. Once an environment is built, it should be runnable on any machine.

Containerizing a Simple Application

For this tutorial, we’ll build an image based on Ubuntu, install SSH for remote access, and then install net-tools – a networking management tool. This tutorial assumes you already have docker installed, otherwise, see the following pages for installation instructions based on your OS:

Step 1: Setting up the Dockerfile

Create a new file named Dockerfile (no extension) in your preferred text editor
(e.g. “nano Dockerfile“). Then let’s start with the base image:

# Choose the "ubuntu:latest" docker image as our base image
FROM ubuntu:latest
Dockerfile

This line tells Docker to use the latest Ubuntu image as our starting point. Docker will automatically pull this image from Docker Hub if you don’t have it locally. Important: Regularly update your base images (e.g., ubuntu:latest) to benefit from security patches and bug fixes. Outdated base images can introduce vulnerabilities into your containerized applications.

You can update your base image by building the container (explained later) or by pulling the image manually using the command “docker pull ubuntu:latest

Step 2: Installing SSH

We want to be able to connect to our container, so let’s install and configure SSH. Add the following lines to your Dockerfile:

# Updates the package list and installs the SSH server
RUN apt-get update && apt-get install -y openssh-server

# Set a password for the root user (for testing purposes only – avoid in production!), allow login through SSH using the root account by modifying the sshd_config file and creates the /var/run/sshd where the SSH service will create its PID file at launch
RUN echo 'root:password' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN mkdir /var/run/sshd

# Allow SSH connections
EXPOSE 22
Dockerfile

Explanation:

  • RUN: Executes commands inside the container during image build.
  • apt-get update: Updates the package lists.
  • apt-get install -y openssh-server: Installs the OpenSSH server. The -y flag automatically answers “yes” to any prompts.
  • echo 'root:password' | chpasswd: Sets a password for the root user (for testing only!). Never use hardcoded passwords in production environments.
  • sed -i…: Change the line “PermitRootLogin prohibit-password” to “PermitRootLogin yes” in the “sshd_config file”, which allows the “root” account to login with a password
  • mkdir /var/run/sshd: Creates the /var/run/sshd where the SSH service will create its PID file at launch
  • EXPOSE 22: Informs Docker that the container listens on port 22 (SSH). This is the container’s internal port, any port can be exposed on our local machine, which will be done later on port 2222

Step 3: Installing net-tools

Let’s install net-tools. Add the following string to the Dockerfile, specifically to the end of the first “RUN” line:

net-tools
Dockerfile

This line tells the “docker build” command to install the latest version of net-toolsalong with its dependencies.

The first “RUN” line should now look like this:

# Updates the package list and installs the SSH server and net-tools
RUN apt-get update && apt-get install -y openssh-server net-tools
Dockerfile

Step 4: Defining the Entrypoint

Finally, let’s define what happens when we run the container. Add the following line to the end of the Dockerfile:

# Set the CMD to the command "/usr/sbin/sshd -D" as the default command when the container starts to start SSH and keep the container running as long as that process is available
CMD ["/usr/sbin/sshd", "-D"]
Dockerfile

This command defines the entrypoint of your container – what happens when you run it.
CMD ["/usr/sbin/sshd", "-D"] sets /usr/sbin/sshd -D as the default command to execute.

What it does: It starts the ssh service and keeps it running for as long as the container is up

  • -D: This flag allows the SSHD server to run in non-detached mode, which means it does not become a Daemon (a service), which keeps the ubuntu container alive

Why we do it: Without a CMD, the container would start but immediately exit. It gives us access to the container’s environment for testing and debugging. You can override this command when running the container if needed (e.g., to run a different application). This can be explored in more details after getting some more experience with the concepts around docker and Dockerfiles

As a general rule of thumb, every docker container should run a single application. In our case, runing that “sshd -D” command is what allows the container to keep running even though no specific service has been configured to run on it. Instead of a CMD, we could also configure an “ENTRYPOINT“. The differences are explained in this external blog article by CloudBees: Understanding Docker’s CMD and ENTRYPOINT Instructions

Step 5: Final result

Your final Dockerfile should now look like this:

# Choose the "ubuntu:latest" docker image as our base image
FROM ubuntu:latest

# Updates the package list and installs the SSH server and net-tools
RUN apt-get update && apt-get install -y openssh-server net-tools

# Set a password for the root user (for testing purposes only – avoid in production!), allow login through SSH using the root account by modifying the sshd_config file and creates the /var/run/sshd where the SSH service will create its PID file at launch
RUN echo 'root:password' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN mkdir /var/run/sshd

# Allow SSH connections
EXPOSE 22

# Set the CMD to the command "/usr/sbin/sshd -D" as the default command when the container starts to start SSH and keep the container running as long as that process is available
CMD ["/usr/sbin/sshd", "-D"]
Dockerfile

Building the Image

Open your terminal and navigate to the directory containing your Dockerfile. Then, build the image using:

docker build -t my-ubuntu-app .
Bash

This command builds a Docker image tagged as my-ubuntu-app.

  • -t: This tells docker what our image’s tag is, which is what we will use when building docker containers
  • The final "." tells Docker to use the current directory where the Dockerfile is located as the build context. Some files can be referenced inside Dockerfile files, they would need to be in this current directory

Running the Container

Now, let’s run the container:

docker run -d -p 2222:22 my-ubuntu-app
Bash
  • -d: Runs the container in detached mode (background).
  • -p 2222:22: Maps port 22 inside the container to port 2222 on your host machine. This allows you to connect via SSH by connecting to <Your machine’s IP>:2222
  • my-ubuntu-app: This is the image’s name that was defined using “-t” in the previous step

Testing the Container

You can now connect to the container using SSH:

ssh root@localhost -p 2222
Bash

Enter the password “password” (remember, this is for testing only!). You should now have a shell inside your containerized Ubuntu environment. Run net-tools to verify it’s installed and working correctly, you can do so by running the following command, which should give you the Ubuntu container’s internal IP:

ifconfig -a
Bash

Upgrading the Application

Let’s say we want to upgrade net-tools to the latest version. We can simply rebuild our Docker image by launching the build command, which will automatically recreate the image with the latest version of net-tools. There are two ways of doing this:

  1. You can simply run the exact same command as before, which will replace your current, existing image, with the new version containing the most up-to-date net-tools and SSH utilities
docker build -t my-ubuntu-app .
Bash
  1. You can also run the image again by giving it a specific version number, which means your old image will still be available in case there are issues with the new version, for example, if the new version of net-tools has a new feature that breaks something within your container
docker build -t my-ubuntu-app:v2 .
Bash

You can then run a new container using the “docker run” command seen earlier. This demonstrates how easily you can update your applications by simply rebuilding the image from the Dockerfile.

# 1. If the original image was overwritten, so no specific version tag was used
docker run my-ubuntu-app -d

# 2. If a version tag was used, which means the old image is still available 
docker run my-ubuntu-app:v2 -d
Bash

Beyond Ubuntu: Exploring Alpine Linux

While Ubuntu is a great starting point, consider exploring Alpine Linux as an alternative base image once you’re more comfortable with Dockerfiles. Alpine is a very lightweight distribution (around 5MB in size) that focuses on security and resource efficiency. This can result in smaller images and faster build times. However, it uses musl libc instead of glibc, which might require some adjustments for certain applications which may not always support that distribution of linux out of the box.

Why Dockerize?

This simple example highlights the advantages of Docker. Any application that can run on Ubuntu (or any other supported base image) can be dockerized. This means:

  • Consistency: Your application will behave the same way regardless of where it’s deployed and, in some cases, it means an application made for Ubuntu can run on Windows, Linux or MacOS.
  • Portability: Easily move your application between different environments (development, testing, production).
  • Isolation: Containers isolate applications from each other and the host system, improving security and stability.
    • Do note that it is still possible to deploy docker applications with wide-open networking, so security is not guaranteed, it simply gives you one more way to secure your applications and your systems
  • No Need for Separate VMs: Instead of dedicating entire virtual machines, hardware and IP addresses to each application, you can run multiple containers on a single VM or even directly on your host operating system. This significantly reduces resource consumption and simplifies management.

Further Exploration

  • Docker Compose: For multi-container applications, explore Docker Compose to orchestrate the different services.
  • Volumes: Learn about volumes for persistent data storage.
  • Networking: Understand how containers communicate with each other and the outside world.
  • Security Best Practices: Implement security measures like using non-root users and regularly scanning for vulnerabilities.

Docker is great for application development and especially deployment. By mastering the basics of Dockerfile, you can change the way your work and streamline your workflow.

By Don

Leave a Reply

Your email address will not be published. Required fields are marked *