Skip to main content
BlogDevOps

Learn Docker from Scratch: First Container in 6 Steps

Learn Docker from scratch hands-on: build your first image, run a container, publish a port, read logs and keep data with a volume without guessing at errors.

Matthews Wong

Software Engineer

October 202610 min read
Learn Docker from Scratch: First Container in 6 Steps

Key Takeaway

Learning Docker from zero takes six steps: write a Dockerfile, run docker build to create an image, run docker run with the -p flag to start a container and publish a port, inspect it with docker ps, docker logs and docker exec, then keep data in a volume so it survives when the container is removed.

You installed Docker, typed docker run, and the terminal printed one long line of random characters. You opened the browser and localhost does not answer. You ran the command again and now an error says the container name is already in use. Nearly everyone learning Docker from zero stalls here, not because Docker is hard, but because nobody explained what each command actually does.

This guide builds one small Node.js app and takes it from a Dockerfile to a container whose data survives deletion. I checked every command against the official Docker documentation, and the Dockerfile pattern is the same one this site uses itself. The output shown is only what the code below is certain to print; IDs and timestamps will differ on your machine.

Prefer the animated version? Our free lesson explains layers and the build cache visually: Docker for beginners: image, container and Dockerfile. This article is the hands-on side: you type, run and see the result yourself.

What you need

Not much. Every step below works on Linux, macOS and Windows as long as Docker is running.

  • Docker installed and running (Docker Desktop on macOS or Windows, Docker Engine on Linux).
  • A terminal and any text editor. You do not need Node.js on your own machine, because Node.js lives inside the image.
  • About 30 minutes, and an empty folder named belajar-docker.
docker --version
# Expect one line starting with: Docker version ...
# "command not found" or "cannot connect to the Docker daemon" means
# Docker is not installed or not running yet. Fix that first.

Check that Docker is alive first. If the command above fails, fix that before going on; the steps that follow cannot work without a running Docker daemon.

Step 1: Understand image and container through three commands

An image is a read-only package holding your application and everything it needs. A container is an image that is currently running. One image can become many containers, the same way one installer file can be launched many times. The easiest way to remember it: the command you type tells you which one you are dealing with.

You want toCommandResult
Create an image from a Dockerfiledocker buildAn image named belajar-docker:v1
Run an imagedocker runA running container
See what is runningdocker psA list of containers, not images

If you get lost in the steps ahead, come back to this table. The Dockerfile and the image live on the build side; containers and ports live on the run side.

Step 2: Write a tiny app and a Dockerfile

Create two files in the belajar-docker folder. The app counts how many times it was visited and keeps the number in /data/count.txt. We use that counter in Step 6 to see why container data can vanish.

// belajar-docker/server.js
const http = require("node:http");
const fs = require("node:fs");

const FILE = "/data/count.txt";

http
  .createServer((req, res) => {
    let n = 0;
    try {
      n = Number(fs.readFileSync(FILE, "utf8")) || 0;
    } catch {
      // first visit: the file does not exist yet
    }
    n += 1;
    fs.writeFileSync(FILE, String(n));
    console.log("visit " + n);
    res.end("Kunjungan ke-" + n + "\n");
  })
  // 0.0.0.0, not 127.0.0.1: inside a container, 127.0.0.1 is the container
  // itself, so a port you publish could never reach the server.
  .listen(3000, "0.0.0.0");
# belajar-docker/Dockerfile
FROM node:22-alpine
WORKDIR /app
# /data is where the counter file lives; step 6 mounts a volume here.
RUN mkdir /data
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]

Read the Dockerfile top to bottom: start from the node:22-alpine base image, move to the /app folder, copy server.js, note that the app uses port 3000, then set the command that runs when the container starts. The reference stresses that EXPOSE only documents a port and does not publish it; publishing is the job of docker run in Step 4.

# Wrong: single quotes. The reference requires double quotes in the JSON array.
CMD ['node', 'server.js']

# Right:
CMD ["node", "server.js"]

This site's own Dockerfile uses the same pattern: WORKDIR /app, port 3000 inside the container, and CMD in JSON array form with double quotes. The difference is that this site's Dockerfile has several stages so the final image stays small. That is a more advanced topic, covered in the best practices article linked at the end.

Step 3: Build your first image

Run docker build from inside the belajar-docker folder. The dot at the end of the command is not decoration: it is the build context, the folder sent to Docker. Forgetting the dot is one of the first errors beginners hit.

cd belajar-docker

# -t gives the image a name:tag. The trailing "." is the build context:
# the current folder, which is sent to Docker.
docker build -t belajar-docker:v1 .

docker image ls belajar-docker
# Expect one row: REPOSITORY belajar-docker, TAG v1

While it builds, you will see one progress step for each Dockerfile instruction. The first time, Docker downloads the node:22-alpine image, so it takes longer. Run the same command again and Docker reuses its cache for the unchanged steps, which is why the second build is much faster.

Step 4: Run the container and publish its port

docker run turns the image into a container. The three flags below are the ones you will use most. -d runs the container in the background and prints its ID, --name gives you a name that is easy to type, and -p connects a port on your machine to a port inside the container.

# -d  run in the background and print the container ID
# --name  a name you can type instead of the random ID
# -p HOST:CONTAINER  traffic to host port 8080 goes to container port 3000
docker run -d --name belajar-app -p 8080:3000 belajar-docker:v1

curl http://localhost:8080
# Kunjungan ke-1
curl http://localhost:8080
# Kunjungan ke-2
# Wrong: host and container swapped. Nothing listens on 8080 inside the container.
docker run -d --name belajar-app -p 3000:8080 belajar-docker:v1

# Right: host side first, container side second.
docker run -d --name belajar-app -p 8080:3000 belajar-docker:v1

# Same thing, but only this machine can reach it (what this site does on its VPS).
docker run -d --name belajar-app -p 127.0.0.1:8080:3000 belajar-docker:v1

The order is always host port first, then container port. Without an IP address, Docker publishes the port on all interfaces, meaning it can be reached from outside your machine. That is why this site on its VPS uses the 127.0.0.1 form in front of the port, so only nginx on the same machine can reach it.

If your app listens on 127.0.0.1 inside the container, the port you publish will never reach it, because 127.0.0.1 there means the container itself. Listen on 0.0.0.0 inside, then restrict outside access with -p 127.0.0.1:8080:3000. This site's Dockerfile also sets HOSTNAME to 0.0.0.0 for the same reason.

Step 5: Look inside with ps, logs and exec

These three commands are your everyday debugging tools. docker ps shows running containers, docker logs shows what the app printed, and docker exec runs a new command inside a live container. The -it flags on exec give you an interactive terminal; the docker exec documentation uses sh as its example, and sh is available in Alpine images.

docker ps
# Columns: CONTAINER ID, IMAGE, COMMAND, CREATED, STATUS, PORTS, NAMES
# One row: belajar-app, STATUS Up ...

docker ps -a        # -a also lists stopped containers

docker logs belajar-app
# visit 1
# visit 2

docker logs -f --tail 20 belajar-app   # -f follows, Ctrl+C to stop following

docker exec -it belajar-app sh
cat /data/count.txt
# 2
exit

Two habits matter. First, if a container is missing from docker ps, add -a before concluding it vanished; it may simply have stopped. Second, docker exec only works while the container's main process is running, so for a crashed container, read docker logs instead.

Step 6: Keep data with a volume

Everything the app writes inside a container lands in that container's own writable layer. Remove the container and the data goes with it. Try it yourself: the sequence below removes the container and starts it again, and the counter resets to 1.

# 1. Without a volume: the counter dies with the container.
docker rm -f belajar-app
docker run -d --name belajar-app -p 8080:3000 belajar-docker:v1
curl http://localhost:8080
# Kunjungan ke-1      <- the count started over

# 2. With a named volume mounted at /data.
docker rm -f belajar-app
docker run -d --name belajar-app -p 8080:3000 -v belajar-data:/data belajar-docker:v1
curl http://localhost:8080
# Kunjungan ke-1
curl http://localhost:8080
# Kunjungan ke-2

docker rm -f belajar-app
docker run -d --name belajar-app -p 8080:3000 -v belajar-data:/data belajar-docker:v1
curl http://localhost:8080
# Kunjungan ke-3      <- the number survived the container

The fix is a volume. The Docker documentation says a volume's contents exist outside the lifecycle of a given container, so the data stays even when the container is removed. The form -v volume-name:/path mounts a named volume at a folder inside the container; Docker creates the volume automatically if it does not exist yet.

# Clean up when you are done
docker rm -f belajar-app
docker volume rm belajar-data

This is exactly the pattern this site uses in production: its compose file mounts a named volume called next-image-cache so the optimised image cache is not lost every time the container is replaced on deploy. Note also that docker rm does not delete a named volume; you have to run docker volume rm yourself.

Common errors and how to fix them

These six symptoms eat most of a beginner's time. Match the message in your terminal to the first column.

SymptomCauseFix
Message bind failed: port is already allocatedHost port 8080 is already used by another container, including an old one you never removed.Check docker ps -a, remove the old one with docker rm -f name, or use another host port such as -p 8081:3000.
Message Conflict: the container name is already in useAn old container, even a stopped one, still holds the name belajar-app until it is removed.docker rm -f belajar-app, then run docker run again.
curl refuses the connection or it dropsPort mapping swapped (-p 3000:8080), or the app listens on 127.0.0.1 inside the container.Write -p host-port:container-port, and make the app listen on 0.0.0.0.
The container shows Exited right away in docker ps -aThe main process finished or crashed. A container lives only as long as its main process.Read docker logs name and fix the error; also check the CMD in the Dockerfile.
Cannot connect to the Docker daemonDocker Desktop or the Docker service is not running.Open Docker Desktop, or on Linux start the Docker service, then try docker --version and docker ps again.
Data or the counter resets after the container is removedThe data was written to the container's writable layer, not to a volume.Add -v volume-name:/path to docker run, as in Step 6.

The general rule: when a container misbehaves, run docker ps -a and then docker logs name before changing anything. Nearly every answer is in those two commands.

Checklist before moving on

  • I can explain that docker build creates an image and docker run creates a container.
  • I know the order of -p is host port first, then container port.
  • I can use docker ps -a, docker logs and docker exec -it to look inside a container.
  • I understand data without a volume is lost when the container is removed.
  • I cleaned up with docker rm -f belajar-app and docker volume rm belajar-data.

Go deeper

You now have a first container. To understand why the order of Dockerfile instructions decides build speed, open the free lesson Docker for beginners: image, container and Dockerfile. After that, read Docker containerization best practicesto tidy the image before it goes to a server, and Docker Compose Watch for local developmentonce you are tired of rebuilding the image every time you change one line of code.

One rule to carry with you: an image is what you build, a container is what you run, and anything that matters must live in a volume, not inside the container. From here, Docker Compose is the natural next step.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

How long does it take to learn Docker from zero?

You can run your first container in about 30 minutes by following the six steps in this guide. Understanding layers, caching and Compose takes longer, and comes from building small projects. Start with one tiny app rather than a large one.

What is the difference between a Docker image and a container?

An image is a read-only package built by docker build from a Dockerfile. A container is a running instance of an image, created by docker run. One image can start many containers.

Why does Docker say the port is already allocated?

Another container is already using that host port, and an old stopped container can still hold it until you remove it. Run docker ps -a to find it and docker rm -f to remove it, or publish on a different host port such as 8081.

Does my data disappear when I delete a Docker container?

Yes, if it was written to the container's own writable layer. Data in a volume survives, because a volume's contents live outside the lifecycle of any single container. Mount one with -v volume-name:/path when you run the container.

Why does a container exit immediately after I start it?

A container only lives as long as its main process. If that process finishes or crashes, the container shows Exited in docker ps -a. Read docker logs with the container name to see why, and check the CMD in your Dockerfile.