Docker basics: images, containers and a first Dockerfile

Docker packages an app with everything it needs so it runs the same everywhere. Learn the core ideas and the commands you will use first.

A shipping container holding an app

Docker packages an application together with its runtime, libraries and settings into a unit that runs the same way on a laptop, a server or the cloud. It ends the "it works on my machine" problem.

Image vs container

  • An image is a read-only template: the app, its files and its dependencies, built in layers.
  • A container is a running instance of an image: an isolated process with its own filesystem view, network and limits.

You can start many containers from one image, just as many programs can run from one installed copy.

Containers are not virtual machines. They share the host's kernel, so they start in seconds and use little memory, but their isolation is lighter than a VM's.

A first Dockerfile

A Dockerfile lists the steps to build an image.

FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
  • FROM: the base image to start from.
  • WORKDIR: the working folder inside the image.
  • COPY and RUN: add files and run build commands. Each creates a layer. Copying package.json first lets Docker cache the dependency install when only your code changes.
  • EXPOSE: documents the port the app uses.
  • USER node: run as a non-root user.
  • CMD: the command to run when the container starts.

Add a .dockerignore file (for node_modules, .git, .env) so you do not copy junk or secrets into the image.

The commands you will use first

docker build -t myapp:1.0 .                     # build an image
docker run -d --name myapp -p 8080:3000 myapp:1.0   # run it, map port 8080 to 3000
docker ps                                       # running containers
docker logs -f myapp                            # follow logs
docker exec -it myapp sh                        # open a shell inside
docker stop myapp && docker rm myapp            # stop and remove
docker images                                   # list images

Ports, environment and volumes

  • Ports: -p host:container publishes a port. Bind to localhost for private services: -p 127.0.0.1:5432:5432.
  • Environment: -e NAME=value or --env-file .env passes settings. Do not bake secrets into the image.
  • Volumes: a container's own filesystem is lost when it is removed. To keep data (a database, uploads), mount a volume: -v mydata:/var/lib/data.

Docker Compose

For several containers (app, database, cache), docker compose describes them in one compose.yaml file and starts them with docker compose up -d. It also creates a private network where containers reach each other by service name.

Safety habits

  • Use small, official, maintained base images and pin a version tag rather than latest.
  • Run as non-root and do not use --privileged unless you really must.
  • Do not publish ports you do not need. A database port open to 0.0.0.0 is reachable from the internet unless a firewall blocks it. Docker's published ports can bypass simple host firewall rules, so bind to 127.0.0.1 when the service is local.
  • Keep images updated and scan them for known vulnerabilities.
  • Never put secrets in the Dockerfile or image layers. Pass them at run time.
  • Set memory and CPU limits for containers that could misbehave.

Takeaway: build an image from a Dockerfile, run containers from it, keep data in volumes, bind private ports to localhost, run as non-root and keep secrets out of the image.


Linux & DevOps series, Part 1 (servers and containers): 4 of 5. Previous: Nginx as a reverse proxy with free HTTPS (Let's Encrypt).

Keep reading