How To Create A Docker Container: A Complete Step-By-Step Guide

You’re staring at a fresh Ubuntu server or a new MacBook Pro. Your project needs to run—a Node.js API, a Python data pipeline, or maybe a database. But the thought of wrestling with library conflicts, version mismatches, and the classic “it works on my machine” dilemma makes you hesitate. There’s a better way, and it starts with a single, powerful command: docker run.

Docker containers have revolutionized how we build, ship, and run software. They package an application with all its dependencies into a standardized unit that runs reliably anywhere Docker is installed. This guide will walk you through creating a Docker container, from the absolute basics to practical, production-ready patterns.

Understanding the Building Blocks

Before you type your first docker command, it’s helpful to grasp three core concepts. A Dockerfile is a text document containing instructions to build a Docker image. Think of it as the recipe. A Docker image is the immutable snapshot, the executable package built from the Dockerfile. Finally, a Docker container is a running instance of an image. It’s the isolated, live process on your system.

The workflow is straightforward. You write a Dockerfile. You use the docker build command to create an image from that file. Then, you use the docker run command to launch a container from that image. Containers are ephemeral by nature; they can be stopped, deleted, and recreated from the image in seconds.

Prerequisites for Your Docker Journey

To follow along, you’ll need Docker installed. Head to the official Docker website and download Docker Desktop for Mac or Windows, or follow the installation guides for your Linux distribution. After installation, open a terminal and run docker –version to confirm it’s ready. You’ll also need a basic text editor to create your Dockerfile.

Creating Your First Dockerfile

Every container starts with a Dockerfile. Let’s create a simple one for a Node.js application. In your project directory, create a new file named Dockerfile with no extension.

We’ll start with a minimal example. A Dockerfile begins by specifying a base image using the FROM instruction. This is the foundation your image builds upon. For a Node.js app, a good choice is an official Node.js image.

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Let’s break down each line. The FROM instruction sets our base. We’re using node:18-alpine, which provides Node.js 18 on the lightweight Alpine Linux distribution. The WORKDIR instruction sets the working directory inside the container for all subsequent commands.

The COPY package*.json ./ line copies your project’s package.json and package-lock.json files into the container’s working directory. The RUN npm ci –only=production command installs the production dependencies cleanly and efficiently. We then COPY . . to copy the rest of your application source code.

The EXPOSE 3000 instruction informs Docker that the container listens on port 3000. It’s a metadata instruction. Finally, CMD [“node”, “server.js”] defines the default command to run when the container starts.

Building a Docker Image from the Dockerfile

With a Dockerfile written, the next step is to build an image. Navigate to the directory containing your Dockerfile in your terminal. Run the following command.

docker build -t my-node-app .

The -t flag tags your image with a name, in this case, my-node-app. The . at the end tells Docker to use the current directory as the build context. Docker will read the Dockerfile and execute each instruction, creating layers for each step.

You’ll see output as Docker pulls the base image and runs each instruction. When it finishes, run docker images to list all images on your system. You should see my-node-app listed. This image is now a portable artifact you can share or deploy.

Optimizing Your Docker Build

Building images can be slow, especially when copying large directories. Use a .dockerignore file to exclude unnecessary files from the build context, similar to a .gitignore file. This speeds up builds and reduces image size.

how to create a docker container

Another tip is to order your Dockerfile instructions strategically. Place commands that change less frequently (like installing dependencies) higher up. This allows Docker to use its layer caching more effectively, making rebuilds faster.

Running Your First Docker Container

Now for the moment of truth. To create and start a container from your image, use the docker run command.

docker run -p 3000:3000 -d --name my-app-container my-node-app

Let’s dissect this command. The -p 3000:3000 flag publishes the container’s port 3000 to your host machine’s port 3000. The -d flag runs the container in detached mode, in the background. The –name flag gives your running container a memorable name, my-app-container. Finally, my-node-app is the image name we built earlier.

After running the command, your container is live. You can verify it’s running with docker ps. Open your web browser and navigate to http://localhost:3000 to see your application. You’ve just created a Docker container.

Interacting with Your Running Container

You can execute commands inside a running container. Use docker exec to open an interactive shell. This is useful for debugging.

docker exec -it my-app-container sh

The -it flags allocate a pseudo-TTY and keep stdin open, giving you an interactive shell. Once inside, you can run commands like ls, ps aux, or cat a log file. Type exit to leave the shell session.

To view logs from your container, use docker logs. This is often the first step in troubleshooting.

docker logs my-app-container

To stop the container, use docker stop. To remove a stopped container, use docker rm. Remember, you can always create a new container from the same image.

docker stop my-app-container
docker rm my-app-container

Common Docker Run Flags for Practical Use

The docker run command is incredibly flexible. Here are essential flags you’ll use regularly. The -e flag sets environment variables inside the container. This is crucial for configuration.

docker run -d -e DATABASE_URL=postgres://localhost/db -p 3000:3000 my-node-app

The -v flag mounts a host directory into the container, enabling data persistence and live code reloading during development. The first path is the host directory, the second is the container path.

docker run -v $(pwd):/app -p 3000:3000 my-node-app

The –restart flag defines a restart policy. Using –restart unless-stalled tells Docker to always restart the container if it stops, which is vital for production resilience.

Managing Container Lifecycle and Resources

Containers are designed to be stateless. For stateful applications like databases, combine the -v flag with a named volume for persistent storage managed by Docker.

docker volume create pgdata
docker run -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret -d postgres

You can also limit container resources. Use –memory and –cpus to prevent a single container from consuming all host resources.

how to create a docker container
docker run --memory="512m" --cpus="1.0" -d my-node-app

Troubleshooting Container Creation Issues

If your container exits immediately, check the logs. The most common cause is an error in the CMD or ENTRYPOINT instruction in the Dockerfile, or a missing dependency. Running the container interactively can help diagnose this.

docker run -it my-node-app sh

If you get a port conflict error, another process is already using the port on your host. Either stop that process or map your container to a different host port using -p 8080:3000.

If Docker cannot find your image, ensure you tagged it correctly during the build and are using the exact name in the run command. Running docker images will list all available images.

Moving from Single Containers to Multi-Container Apps

Most real applications involve multiple services: a web app, a database, a cache. You can run each in its own container and link them. The modern way to manage this is with Docker Compose. Create a docker-compose.yml file to define and run multi-container applications with a single command.

This allows you to spin up your entire development or testing environment consistently. It abstracts away the complex docker run commands for each service and handles networking between them automatically.

Best Practices for Production-Ready Containers

Always use specific version tags for base images, not the latest tag. This ensures your builds are reproducible and not broken by unexpected upstream changes.

Run containers as a non-root user when possible. You can add a USER instruction in your Dockerfile after installing dependencies to enhance security.

Keep your images small. Smaller images deploy faster and have a smaller attack surface. Use Alpine-based variants, multi-stage builds to discard build tools, and clean up package manager caches in the same RUN instruction.

Final Steps for Mastery

Practice is key. Start by containerizing a simple, existing project. Experiment with different base images. Learn to push your images to a registry like Docker Hub so they can be pulled onto any server.

Explore Docker’s networking features to connect containers. Understand how Docker manages data with volumes and bind mounts. Finally, familiarize yourself with Docker Compose for orchestrating multiple containers, which is the natural next step in your containerization journey.

You now have the foundational knowledge to create Docker containers confidently. From a simple Dockerfile to a running, isolated application environment, you’ve seen the entire workflow. The consistency, isolation, and portability containers provide are game-changers for development and deployment. Start building, and you’ll quickly see why Docker has become an indispensable tool in the modern software lifecycle.

Leave a Comment

close