Available for Hire
ZB

Memuat...

Back to Blog
Tutorial 7 min read · 1405 words

Docker for Beginners: From Zero to Confident

A Docker walkthrough covering installation, the image and container model, writing an efficient Dockerfile, and running multi-service applications with Docker Compose.

#docker #containerization #devops #tutorial

Docker is a containerization platform that runs an application together with all of its dependencies in an isolated environment. Unlike a virtual machine, a container does not carry its own operating system. It shares the kernel with the host machine and packages only what the application needs. That is why a container starts in seconds while a VM takes tens of seconds to minutes.

Why Docker?

Consistency. The phrase “it works on my machine” exists because the Node version, system library versions, or environment variables on a developer’s laptop differ from the server. A container locks all of that into an image, so what runs on the laptop is exactly what runs in production.

Portability. The same image runs on a laptop, an on-premise server, or any cloud service that supports containers.

Efficiency. With no guest OS, a single server can host far more containers than it could VMs of the same specification.

Isolation. Two applications that need different PHP versions can live side by side without interfering with each other.

Docker is not the answer to everything. For a small application that needs a single process on a single server, Docker adds a layer of complexity that may not pay for itself.

Installation

On Ubuntu or Debian, the docker.io package from the distribution repository usually lags several versions behind. For the latest release, use Docker’s official repository. If you just want to try it quickly:

sudo apt update
sudo apt install docker.io
sudo systemctl enable --now docker

# So you do not need sudo every time
sudo usermod -aG docker $USER

After adding yourself to the docker group, log out and back in so the membership takes effect. Worth noting: members of the docker group effectively hold root access on that machine, because they can mount any directory into a container. Do not add users you do not trust.

Verify the installation:

docker run --rm hello-world

The --rm flag removes the container as soon as it finishes so they do not pile up.

Core Concepts

Image is a read-only template containing your application’s filesystem. An image is built from stacked layers. Each instruction in a Dockerfile produces one layer.

Container is a running instance of an image. On top of the read-only image layers, Docker adds a single writable layer. When the container is deleted, that writable layer goes with it. This is why data inside a container is not permanent.

Volume is the mechanism for storing data outside the container so it survives the container being deleted or replaced.

Network connects containers. Containers on the same network can reach each other by service name without knowing IP addresses.

Dockerfile is the recipe for building an image.

Docker Compose runs several containers together from a single configuration file.

Commands You Will Use Constantly

docker pull nginx:1.27          # Pull an image, name the tag
docker run -d -p 8080:80 nginx  # Run it, map host:container ports
docker ps                       # Running containers
docker ps -a                    # Including stopped ones
docker logs -f <id>             # Follow logs live
docker exec -it <id> sh         # Open a shell inside the container
docker stop <id>                # Stop gracefully
docker rm <id>                  # Remove the container
docker images                   # Local images
docker system df                # Disk usage
docker system prune             # Clean up what is unused

Two habits save a lot of time. First, always name the version tag. Writing just nginx quietly means nginx:latest, and what latest points at changes over time, so a build that worked yesterday can fail today. Second, run docker system prune regularly. Accumulated images and build cache can eat tens of gigabytes without you noticing.

Writing an Efficient Dockerfile

The simplest version for a Node application:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

The ordering here is deliberate. Docker caches each layer, and a layer is rebuilt only when it or a layer above it changes. Because package*.json is copied first, npm ci will not rerun as long as the dependency list is unchanged. If you write COPY . . first instead, every single-line code change triggers a full dependency reinstall.

Use npm ci rather than npm install inside an image. It reads package-lock.json strictly, so the result is reproducible.

Create a .dockerignore so your local node_modules, the .git folder, and .env files do not get copied in:

node_modules
.git
.env
dist
*.log

Without .dockerignore, the build context can balloon to hundreds of megabytes, and node_modules from the host can overwrite what was installed inside the image.

Multi-stage builds

For applications that need a build step, separate building from running. The final image then contains only the build output, with no toolchain:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

The difference is large. An image carrying the full node_modules plus toolchain can run to hundreds of megabytes, while a result like the one above is often under fifty.

Do not run as root

By default, processes inside a container run as root. If your application has a flaw, an attacker immediately has root inside the container, which is a good starting point for trying to break out. Drop the privileges:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

The official Node image already provides a user named node, so you do not need to create one.

Persisting Data with Volumes

Data inside a container disappears when the container is deleted. For a database that is obviously unacceptable:

docker run -d \
  --name db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

pgdata is a named volume managed by Docker. Its contents survive the db container being removed and recreated.

There are also bind mounts, which map a host folder into the container. These are useful during development so code changes show up immediately:

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

Bind mounts are handy in development but avoid them in production, because they tie the container to the host machine’s folder structure.

Docker Compose for Multiple Services

Real applications rarely stand alone. There is usually an application, a database, and perhaps a cache. Compose describes all of it in one place:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Notice that DATABASE_URL uses the host db, not localhost. Inside a Compose network, a service name automatically becomes a hostname.

Notice as well that depends_on is paired with condition: service_healthy. Without a healthcheck, depends_on only waits for the database container to start, not for Postgres to be ready to accept connections. The result is an application that fails in its first few seconds trying to reach a database that is not listening yet.

Running it:

docker compose up -d       # Start everything in the background
docker compose logs -f app # Follow one service's logs
docker compose down        # Stop and remove containers
docker compose down -v     # Remove volumes too, be careful

Mistakes That Come Up Often

Baking secrets into the image. Anything copied into an image is stored in a layer and readable by anyone who has that image, even if the file is deleted in a later layer. Pass secrets as environment variables at runtime or through a secret manager.

Storing important data without a volume. One docker compose down -v and your database is empty.

Not limiting resources. A single container leaking memory can take down an entire server. Constrain it with --memory and --cpus.

Relying on the latest tag. Non-deterministic builds are a source of problems that are hard to trace.

Where to Go Next

Once you are comfortable with Docker, the usual next step is orchestration: running containers across multiple machines with self-healing and scaling. Kubernetes is the most common choice, though for simpler needs Docker Swarm or a managed container service from a cloud provider is often enough and far lighter to maintain.

Share this article:

Enjoyed this article?

0 reactions