Grave Design
Tech

Docker for Beginners: Containers Explained Without the Jargon

By Grave Design 1 min read
Server infrastructure representing container technology

A virtual machine is an entire computer simulated inside your computer. It has its own operating system, its own kernel, its own everything — and it takes minutes to boot and gigabytes of RAM to run. A container is something much lighter: it’s a process running on your existing operating system, but isolated so it thinks it’s alone. It boots in seconds, uses megabytes (not gigabytes) of RAM, and shares the host’s kernel.

Docker didn’t invent containers. Linux had namespaces and cgroups (the underlying technologies) since 2008. What Docker did was make containers usable by normal humans. Before Docker, setting up a container required deep Linux internals knowledge. After Docker, it takes a single command.

Key Takeaways

  • A container is a lightweight, isolated process — not a virtual machine, not a full OS, just your application and its dependencies packaged together
  • Docker images are the blueprints, containers are the running instances — one image can spawn hundreds of containers
  • Docker Compose is where Docker becomes actually useful — defining multi-container applications in a single YAML file
  • Volumes are how data persists — without them, everything inside a container disappears when it stops
  • You don’t need to understand everything to start using Docker — pull an image, run it, learn the rest as you go

Why Docker Matters (Practically)

Forget the DevOps buzzwords. Here’s why Docker is useful for real people:

It eliminates “works on my machine” problems. A Docker image contains the exact operating system, libraries, runtime versions, and configurations that the software needs. If it runs in the container, it runs everywhere the container runs — your laptop, your server, your coworker’s weird Windows setup, a cloud VM.

It makes installing complex software trivial. Want to run Nextcloud? Without Docker, you need to install PHP, Apache/Nginx, a database, configure them all, manage PHP extensions, and troubleshoot version conflicts. With Docker, it’s one command: docker run nextcloud. Everything the application needs is already inside the image.

It keeps your system clean. Every application runs in its own isolated container with its own dependencies. No more Python version conflicts, no more cluttered system libraries, no more “I installed something and now my other thing is broken.” Remove a container and its image, and it’s like the software was never there.

It’s how most self-hosted software is distributed now. If you’re interested in self-hosting your own services, Docker is essentially mandatory. The self-hosting community has standardized on Docker Compose files as the default installation method.

Installing Docker

On Linux (Ubuntu/Debian)

Don’t install Docker from your distro’s default repositories — those versions are typically outdated. Use Docker’s official repository:

# Add Docker's official GPG key and repository
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine and Docker Compose
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Add your user to the docker group (so you don't need sudo every time)
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

On macOS and Windows

Install Docker Desktop from docker.com. It runs a lightweight Linux VM in the background (since containers are a Linux technology). Docker Desktop is free for personal use, education, and small businesses (under $10M revenue and under 250 employees). Larger organizations need a paid subscription ($5-24/user/month).

On macOS, Docker Desktop uses Apple’s Virtualization framework on Apple Silicon Macs, which is significantly faster than the old HyperKit backend. Performance is good for development but noticeably slower than native Linux for I/O-heavy workloads.

Core Concepts: Images, Containers, and Registries

An image is a read-only template. Think of it as a snapshot of a filesystem plus some metadata (what command to run, what ports to expose, what environment variables to set). Images are built in layers — a base Ubuntu layer, then a Python installation layer, then your application code layer. Layers are cached and shared, so if ten images all start from ubuntu:24.04, that base layer only exists once on disk.

A container is a running instance of an image. You can run multiple containers from the same image, and each one is isolated from the others. Containers are ephemeral by default — when you stop and remove a container, any data written inside it is gone.

A registry is where images are stored and shared. Docker Hub (hub.docker.com) is the default public registry with millions of images. GitHub Container Registry (ghcr.io), Google Container Registry, and others also exist. When you docker pull nginx, Docker downloads the image from Docker Hub.

Your First Container

Let’s run something:

docker run -d -p 8080:80 --name my-nginx nginx

Breaking this down:

  • docker run — create and start a container
  • -d — detached mode (runs in the background)
  • -p 8080:80 — map port 8080 on your machine to port 80 inside the container
  • --name my-nginx — give it a human-readable name
  • nginx — the image to use (pulled from Docker Hub if not already local)

Open http://localhost:8080 in your browser. You’ll see the Nginx welcome page. Congratulations — you’re running a web server in a container. That server is completely isolated from your system. It can’t see your files, your other processes, or anything else.

Useful commands for managing this container:

docker ps                    # List running containers
docker ps -a                 # List all containers (including stopped)
docker stop my-nginx         # Stop the container
docker start my-nginx        # Start it again
docker logs my-nginx         # View container logs
docker exec -it my-nginx bash  # Open a shell inside the container
docker rm my-nginx           # Remove the container (must be stopped first)

Volumes: Making Data Persist

By default, everything inside a container is temporary. Restart the container and your data might survive (it stays in the container’s writable layer). Remove the container and it’s gone. For any application that stores data — a database, a file server, a notes app — you need volumes.

A volume maps a directory on your host machine to a directory inside the container:

docker run -d -p 8080:80 -v /home/user/website:/usr/share/nginx/html --name my-nginx nginx

The -v /home/user/website:/usr/share/nginx/html part means: whatever is in /home/user/website on your machine appears inside the container at /usr/share/nginx/html. Files you put in that host directory are immediately visible inside the container, and vice versa.

There are two types of volumes:

Bind mounts (shown above) map a specific host path to a container path. You manage the directory yourself. Good for development and when you want direct access to the files.

Named volumes are managed by Docker:

docker volume create my-data
docker run -d -v my-data:/data --name my-app some-image

Docker stores named volumes in /var/lib/docker/volumes/ (on Linux). They’re cleaner for production use and easier to back up with Docker commands, but you can’t just browse them from your file manager.

Docker Compose: Where It Gets Good

Running individual docker run commands gets tedious fast, especially when you have multiple containers that need to work together. A typical web application might need an app server, a database, and a reverse proxy — that’s three containers with specific network connections, volume mounts, and environment variables.

Docker Compose lets you define all of this in a single YAML file. Here’s a real-world example — running Nextcloud (a self-hosted Google Drive alternative) with a MariaDB database:

services:
  nextcloud:
    image: nextcloud:29
    ports:
      - "8080:80"
    volumes:
      - nextcloud-data:/var/www/html
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=your-secure-password-here
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: mariadb:11
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=your-root-password-here
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=your-secure-password-here
    restart: unless-stopped

volumes:
  nextcloud-data:
  db-data:

Save this as docker-compose.yml (or compose.yml — both work), then:

docker compose up -d

That single command pulls both images, creates both containers, connects them on a shared network (so Nextcloud can reach the database at hostname db), creates the named volumes, and starts everything. Visit http://localhost:8080 and finish the Nextcloud setup wizard.

To manage the stack:

docker compose ps        # Status of all containers in the stack
docker compose logs -f   # Follow logs from all containers
docker compose down      # Stop and remove all containers (volumes are preserved)
docker compose pull      # Pull newer versions of all images
docker compose up -d     # Recreate containers with updated images

Networking Basics

Docker creates an isolated network for each Compose project. Containers within the same Compose file can reach each other by service name — in the Nextcloud example, the nextcloud container connects to db:3306 because Docker’s internal DNS resolves db to the database container’s IP.

Ports are only accessible from outside Docker if you explicitly publish them with -p or ports: in Compose. This is a security feature: your database doesn’t need to be reachable from the outside world, so don’t publish its port. Only publish ports for services that need external access (like your web frontend).

If you’re running multiple web services and want them all accessible on port 80/443, use a reverse proxy like Traefik, Caddy, or Nginx Proxy Manager. The reverse proxy is the only container that publishes ports 80 and 443. It routes incoming requests to the correct backend container based on the domain name. This is how most self-hosting setups work in practice.

Building Your Own Images

Eventually you’ll want to containerize your own application. You do this with a Dockerfile — a text file containing instructions for building an image:

FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["python", "app.py"]

Each instruction creates a layer. Docker caches layers, so if your requirements.txt hasn’t changed, pip install won’t re-run when you rebuild — only the COPY . . layer (your application code) gets rebuilt. This makes builds fast during development.

Build and run:

docker build -t my-app .
docker run -d -p 8000:8000 my-app

A few Dockerfile best practices that actually matter:

  • Use specific image tags (python:3.13-slim, not python:latest). latest can break your build without warning when the upstream image changes.
  • Order instructions from least-changing to most-changing. Dependencies before application code, so layer caching works effectively.
  • Use -slim or -alpine base images. python:3.13 is 1.0GB. python:3.13-slim is 150MB. python:3.13-alpine is 50MB. Smaller images pull faster, start faster, and have less attack surface.
  • Don’t run as root. Add USER nonroot near the end of your Dockerfile. If the container is compromised, the attacker has limited privileges.

Common Mistakes and How to Fix Them

Not using restart: unless-stopped or restart: always. Without a restart policy, your containers don’t come back after a reboot or a crash. Add restart: unless-stopped to every service in your Compose files. This restarts containers automatically unless you explicitly stopped them.

Storing passwords in the Compose file. The example above has passwords in plain text for clarity, but in practice you should use a .env file (which Docker Compose reads automatically) or Docker secrets. At minimum, put a .env file next to your compose.yml with your passwords, reference them as ${MYSQL_PASSWORD} in the Compose file, and keep .env out of version control. For securing passwords properly, a password manager is a good place to generate and store them.

Forgetting that container data is ephemeral. This one bites everyone exactly once. You spend an hour configuring an application inside a container, then docker compose down and docker compose up -d to apply a change, and all your configuration is gone because you didn’t define a volume for the config directory. Always check which directories an application uses for persistent data and mount them as volumes.

Using docker compose down -v casually. The -v flag removes volumes — meaning your databases, files, and configuration. docker compose down without -v stops and removes containers but preserves volumes. Muscle memory with the wrong flag will cost you data.

Never cleaning up old images. Over time, Docker accumulates unused images, stopped containers, and orphaned volumes. Run docker system prune periodically to clean up. Add --volumes to also remove unused named volumes (be careful — make sure nothing important is in them).

Docker vs Podman vs LXC

Podman is Docker’s main alternative, developed by Red Hat. It’s daemonless (no background service needed), rootless by default (runs as your user, not root), and compatible with Dockerfiles and most Docker commands. If you’re on Fedora or RHEL, Podman is pre-installed. For most use cases, you can alias docker to podman and everything works. The main gap: Docker Compose support in Podman exists via podman-compose but isn’t as polished as native Docker Compose.

LXC/LXD (now Incus, after Canonical forked it) provides system containers — closer to lightweight VMs than application containers. An LXC container runs a full init system and can host multiple services, like a traditional server. Docker containers typically run one process each. LXC is great for replacing VMs; Docker is great for packaging and running applications. Different tools for different jobs.

For beginners: start with Docker. It has the largest community, the most documentation, and the most available images. Switch to Podman if you have specific reasons (rootless requirements, philosophical preference for daemonless architecture, Fedora/RHEL environments). Ignore LXC unless you specifically need system containers.

Frequently Asked Questions

Is Docker free?

Docker Engine (the core technology) is free and open-source. Docker Desktop (the GUI application for macOS and Windows) is free for personal use, education, open source projects, and small businesses. Companies with more than 250 employees or more than $10M annual revenue need a paid Docker Desktop subscription ($5-24/user/month). On Linux, you don’t need Docker Desktop at all — Docker Engine runs natively.

How much disk space does Docker use?

It depends on how many images you have. A typical image is 50MB-1GB. Running 10-15 containers with their images might use 5-15GB of disk space, plus whatever data your volumes contain. Run docker system df to see exactly how much space Docker is using. Regular docker system prune keeps things manageable.

Can I run Docker inside a virtual machine?

Yes, and it’s common. Many production environments run Docker inside VMs for isolation. Performance is good — Docker adds negligible overhead since it shares the host kernel. The main constraint is nested virtualization if you’re trying to run Docker Desktop (which uses a VM) inside another VM. On a Linux VM, Docker Engine runs natively without this issue.

Is Docker secure?

Docker containers provide isolation, not sandboxing. A container running as root has limited access to the host by default, but container escapes are a known class of vulnerabilities. For better security: run containers as non-root users, keep Docker and your images updated, don’t run untrusted images, use read-only filesystems where possible, and don’t give containers unnecessary capabilities (avoid --privileged mode). Docker is secure enough for home use and most production workloads when configured properly.

Should I use Docker for my personal projects?

If your project has any dependencies beyond the standard library of your language, Docker makes your life easier. It’s especially valuable when you’re working across machines, collaborating with others, or deploying to a server. For a simple script with no dependencies, Docker is unnecessary overhead. For a web application with a database, Docker Compose is almost certainly simpler than installing everything manually.

Related Articles

Docker containers DevOps virtualization