ap.Abhishek Patel

Search

Search notes, essays, projects, the library and the AI course. Use arrow keys to choose and Enter to open.

Lesson 4 of 5 30 min Beyond the syllabus

Docker: shipping your laptop in a box

Run services like Redis and Ollama in containers, keep their data in volumes, build an image for your own app, and run several containers together with Compose.

Words in this lessonDocker · Docker image · Container · Dockerfile · Docker Compose · Port · Volume
Docker
A tool that packages software with everything it needs so it runs the same way on any machine. Like shipping a sealed, ready-to-plug-in kitchen instead of a recipe and hoping the other kitchen matches.
Docker image
A sealed, read-only package of an operating system's files, a runtime, libraries, code and a start command. Like a batch of frozen dough: fully prepared, ready to be baked as many times as you like.
Container
One isolated, running copy of a Docker image. Like one loaf baked from the frozen dough: identical to the others at the start, then its own thing.
Dockerfile
A text file of step-by-step instructions Docker follows to build an image for your app. Like the recipe card for making the frozen dough.
Docker Compose
A way to describe and start several containers together from one file. Like a floor plan for a whole bakery: ovens, fridges and counters placed and connected in one go.
Port
A numbered door on a computer that a network service listens on, such as 6379 for Redis. Like numbered service hatches along one building: the same address, different doors for different departments.
Volume
Storage that lives outside any container, so data survives when containers are replaced. Like a walk-in fridge shared between shifts: the ovens change, the stock stays.
On this page
  1. Send the whole kitchen, not the recipe
  2. Running Redis in one command
  3. Where the data goes: volumes
  4. The real code: putting your own bot in a box
  5. Several containers together: Docker Compose
  6. Where it breaks
  7. Try it yourself
  8. Common mistakes
  9. Recap
  10. Go deeper

Saturday, 7 a.m. Arjun is deploying the support bot to Crumb’s small cloud server for the first time. On his laptop it has worked all week. On the server, it crashes on the first line that touches the queue:

ModuleNotFoundError: No module named 'redis'

He installs the library. The next error says the Redis server itself is not running. He installs Redis with the server’s package manager, which gives him an older major version than the one on his Mac, with a different default configuration. Then Python complains: the server has Python 3.10, and his code uses a feature from 3.12. By the time the bot is running it is 10:15, the support team has been without drafts for the whole morning rush, and Arjun has a fourteen-step SETUP.md file he is not sure is complete.

On Monday, Priya reads the file. “Step 9 says ‘install Redis’. Which version? With what settings? And when Module 20 needs Ollama and Module 08 needs Milvus, will you add thirty more steps, and run them by hand on every machine?”

“It worked on my machine,” Arjun says, and hears how it sounds.

“Every developer has said that sentence,” says Priya. “Docker exists so you never have to say it again.”

Important

The idea in one sentence: Docker packages a program together with everything it needs (the operating system files, the right language version, the libraries and the settings) into an image, and runs that image as an isolated container that behaves the same on your laptop, a teammate’s laptop and a server.

Send the whole kitchen, not the recipe

Priya asks a question. “When Crumb opened the Wakad store, how did we make sure the croissants tasted the same as Baner’s?”

“We sent the recipe,” Arjun says.

“And the first week?”

He remembers. “They were wrong. Different oven, different flour supplier, the butter was too warm in their kitchen. We had to send Sunil over for a week.”

“Your SETUP.md is a recipe. It assumes the other kitchen has the same oven, the same flour and the same temperature. Now imagine that instead of a recipe, you could send a sealed, portable kitchen: oven, flour, butter, temperature, all already set, that only needs plugging in. Same croissants everywhere, guaranteed.”

That is the shipping container idea, and Docker borrowed the name from it. Before standard containers, cargo was loaded onto ships piece by piece, and every port handled it differently. A standard steel box changed that: whatever is inside, every crane, ship and truck in the world knows how to move it.

In Docker:

  • An image is the sealed, portable kitchen: a read-only package of a minimal operating system’s files, the language runtime, your libraries, your code and the command to start it. Think of it as a batch of frozen dough: fully prepared, ready to be baked any number of times.
  • A container is one running copy of an image: the loaf actually in the oven. You can run many containers from one image, and each one is isolated from the others and from your laptop’s own setup.
  • A registry is a warehouse of images. The biggest public one is Docker Hub, where projects like Redis, Postgres and Ollama publish official images.
  • Docker Engine is the program that builds and runs containers. On macOS and Windows you install Docker Desktop, which runs the engine inside a small hidden Linux virtual machine (on Windows it uses WSL 2 for this). On Linux you install Docker Engine directly.

Note

Pause and think. An image is often compared to a class in programming, and a container to an object (an instance). In bakery terms, what would be the equivalent, and what does it predict about running three containers from one image?

Show the answer

The image is the recipe-plus-ingredients (or the frozen dough); each container is one loaf baked from it. Three containers from one image are three independent loaves: they start identical, but each has its own running state, and changes inside one (a file written, data stored) do not appear in the others or in the image. When a container is deleted, anything written inside it is gone, unless you stored it somewhere outside, which is what volumes are for (below).

Running Redis in one command

Install Docker Desktop from the Docker documentation (on Linux, Docker Engine), start it, and wait for it to say it is running. If you skip the “start it” part, every command fails with an error like this one, which simply means the engine is not running:

failed to connect to the docker API at unix:///Users/arjun/.docker/run/docker.sock ...

Now, Priya’s first demo: the Redis server that took Arjun an hour on Saturday. Redis is a very fast in-memory data store you will use for queues and caching in Modules 10 and 21.

docker run -d --name crumb-redis -p 6379:6379 redis:7

Here is every piece of that command, and what happened when it ran:

  • docker run: create a container from an image and start it.
  • redis:7: the image, written name:tag. The tag picks the version: here, Redis major version 7. Docker did not have this image yet, so it downloaded it from Docker Hub first, printing lines like Pulling from library/redis and a few Pull complete lines, one per layer (images are built in stacked layers, and layers shared between images are downloaded only once).
  • -d: detached, run in the background and give me my terminal back. Docker printed the new container’s long ID.
  • --name crumb-redis: a friendly name, so you can refer to it later instead of the ID.
  • -p 6379:6379: publish a port. Read it as laptop:container. Redis listens on port 6379 inside the container; this connects your laptop’s port 6379 to it, so programs on your laptop can reach Redis at localhost:6379.

Check it is running:

docker ps
CONTAINER ID   IMAGE     COMMAND                  STATUS         PORTS                    NAMES
3f2a91c0d7e4   redis:7   "docker-entrypoint.s…"   Up 8 seconds   0.0.0.0:6379->6379/tcp   crumb-redis

And talk to it, by running a command inside the container:

docker exec -it crumb-redis redis-cli ping
PONG

docker exec runs a command in an already-running container; -it makes it interactive in your terminal. Redis’s own command-line tool answered PONG. From nothing to a running, correctly versioned Redis in about twenty seconds, with nothing installed on your laptop except Docker.

The everyday commands to manage it:

docker logs crumb-redis      # what has it printed?
docker stop crumb-redis      # stop it (it still exists)
docker start crumb-redis     # start it again
docker rm crumb-redis        # delete the container (must be stopped first)
docker images                # list downloaded images

The same pattern runs Ollama, the local model runtime from Module 20, in one command (this is the command from Ollama’s own documentation):

docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

Notice the new piece, -v. That is where your data lives.

Where the data goes: volumes

Arjun stores a test key in Redis, stops and deletes the container, starts a fresh one from the same image, and looks for his key. It is gone.

Of course it is. The container was the loaf; he threw it away and baked a new one from the same frozen dough. Anything written inside a container lives only as long as that container.

A volume is storage that lives outside any container and is mounted into it at a folder path. Delete the container, start a new one with the same volume, and the data is still there.

docker run -d --name crumb-redis -p 6379:6379 \
  -v redis-data:/data \
  redis:7 redis-server --appendonly yes

-v redis-data:/data means “mount the volume called redis-data at /data inside the container” (Docker creates the volume if it does not exist). The words after the image name replace the image’s default start command: here, Redis’s own option to write every change to disk in /data. For Ollama, the volume holds the downloaded models, often several gigabytes each; without it, you would re-download them every time the container is replaced.

The real code: putting your own bot in a box

Running other people’s images is half of Docker. The other half is building your own, so the support bot ships with its exact Python and libraries. You describe the image in a text file called a Dockerfile: a recipe Docker follows, step by step, to build it.

In your crumb-lab project from the setup lab, create a file named exactly Dockerfile:

# 1. Start from an official image with Python 3.12 on a slim Linux.
FROM python:3.12-slim

# 2. Copy the uv tool in from uv's official image.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# 3. Work inside /app in the image.
WORKDIR /app

# 4. Install the exact locked dependencies first (this layer is cached).
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-install-project

# 5. Copy the rest of the code.
COPY . .

# 6. What to run when a container starts.
CMD ["uv", "run", "main.py"]

Line by line:

  1. FROM picks the starting image. Every Dockerfile builds on top of another image. This one gives you Debian Linux with Python 3.12 already installed, so the server’s Python 3.10 no longer matters.
  2. COPY --from=... borrows the uv program from uv’s published image. (In real projects, replace latest with a specific uv version so builds are repeatable; uv’s docs show how.)
  3. WORKDIR sets the folder inside the image where the next commands run.
  4. Dependencies before code. Docker caches each step as a layer and reuses it if its inputs have not changed. Copying only pyproject.toml and uv.lock first means the slow “install libraries” step reruns only when your dependencies change, not every time you edit one line of code.
  5. COPY . . copies your project into the image.
  6. CMD is the command each container runs on start.

Next to it, create .dockerignore, which lists files that must never be copied into the image:

.env
.venv
.git
__pycache__

Without that first line, COPY . . would bake your API key into the image, and anyone you ever share the image with could read it.

Build the image and run it:

docker build -t crumb-bot .
docker run --rm --env-file .env crumb-bot

-t crumb-bot names (tags) the image; the . means “build from the Dockerfile in this folder”. The first build downloads the Python image and installs your libraries, which can take a minute; later builds that only change code take a few seconds. --rm deletes the container when it exits, and --env-file .env passes your secrets in as environment variables at run time, so they are never inside the image. You should see the same output as uv run main.py gave you in the setup lab, now running from inside a Linux container.

Several containers together: Docker Compose

The real bot needs Redis and itself, and later a database too. Typing long docker run commands for each, in the right order, is the SETUP.md problem again. Docker Compose describes a whole set of containers in one file, compose.yaml, and starts them together:

services:
  bot:
    build: .                    # build from the Dockerfile in this folder
    env_file: .env              # secrets at run time
    environment:
      REDIS_URL: redis://redis:6379
    depends_on:
      - redis

  redis:
    image: redis:7
    command: redis-server --appendonly yes
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data

volumes:
  redis-data:
docker compose up -d      # build if needed, then start everything in the background
docker compose ps         # what is running
docker compose logs bot   # the bot's output
docker compose down       # stop and remove the containers (volumes are kept)

Look at REDIS_URL: redis://redis:6379. Inside Compose, every service can reach every other service by its service name. The bot finds Redis at the hostname redis, not localhost. That one line is the most common Docker bug in this whole course, and it bit Arjun within the hour.

Milvus, the vector database in Module 08, is a good example of why Compose matters: it runs as several cooperating containers, and its documentation provides a ready-made Compose file you start with docker compose up -d.

Note

Pause and think. Arjun’s bot, running in a container, tries to connect to Redis at localhost:6379 and gets “connection refused”, even though docker ps shows Redis running and his laptop can reach localhost:6379 fine. Why?

Show the answer

Inside a container, localhost means the container itself, not your laptop or other containers. Each container has its own isolated network identity. The bot’s container has no Redis inside it, so localhost:6379 refuses. From your laptop, localhost:6379 works because -p 6379:6379 connected your laptop’s port to the Redis container. The fix in Compose is to use the service name, redis:6379. (To reach a service running directly on your laptop from inside a container, Docker Desktop provides the special hostname host.docker.internal.)

Where it breaks

“Port is already allocated”. If something on your laptop already uses port 6379 (say, a Redis you installed by hand earlier), -p 6379:6379 fails. Stop the other program, or map a different laptop port: -p 6380:6379 makes the container’s 6379 available as localhost:6380.

Lost data. Containers are disposable by design. If a database container has no volume, docker compose down followed by up gives you an empty database. Every stateful service (Redis, Postgres, Milvus, Ollama’s model store) needs a volume.

Secrets baked into images. Forgetting .dockerignore copies .env into the image; using ENV ANTHROPIC_API_KEY=... in a Dockerfile writes it into a layer. Both leak the key to anyone with the image. Pass secrets at run time (--env-file, or your hosting platform’s secret settings).

Disk and memory. Images take space: even the slim Python base image is over a hundred megabytes, and model images and volumes can be many gigabytes. docker system df shows usage; docker system prune cleans up stopped containers and unused images (read its warning before confirming). On macOS and Windows, Docker Desktop also has a memory limit in its settings; local models in Module 20 may need you to raise it.

Different chips. Images are built for a processor type. Apple Silicon Macs use ARM chips; most cloud servers use x86. Official images usually include both, but an image built on your Mac may not run on an x86 server without building for that platform. You will meet this when deploying in Module 26.

Licensing. Docker Engine is open source. Docker Desktop is free for personal use, education and small businesses, but larger companies need a paid subscription. Check Docker’s terms before using it at work.

Back at Crumb, the Saturday deploy now takes three commands: git pull, docker compose build, docker compose up -d. The server needs only Docker installed. Its Python version no longer matters, because the bot brings its own.

Try it yourself

  1. Run Redis with a volume, as shown above. Store a value with docker exec -it crumb-redis redis-cli set greeting "namaste", and read it back with get greeting.
  2. Delete the container (docker stop crumb-redis then docker rm crumb-redis) and start a new one with the same volume. Is greeting still there?
  3. Now try it once without -v. What changes?
Show the answer
  1. set replies OK; get greeting replies "namaste".
  2. Yes. The data lived in the redis-data volume, which outlived the container. The new container mounted the same volume at /data, and Redis reloaded it on start.
  3. Without a volume, the value disappears when the container is removed: the data lived only inside that one container. (Clean up afterwards with docker rm -f crumb-redis and, if you like, docker volume rm redis-data.)

Common mistakes

  • Using localhost between containers. Use the Compose service name.
  • No volume on a database. Your data dies with the container.
  • Copying .env into the image. Always have a .dockerignore; pass secrets at run time.
  • Forgetting Docker Desktop is not running. “Cannot connect to the Docker daemon” or “failed to connect to the docker API” means start Docker Desktop and wait.
  • Using latest everywhere. redis:latest can change major version under you. Pin a tag like redis:7 so every machine runs the same thing.

Recap

  • An image is a sealed package of an OS’s files, a runtime, libraries, code and a start command; a container is one isolated running copy of it.
  • docker run starts containers; -p laptop:container publishes ports; -v name:/path mounts volumes so data survives.
  • A Dockerfile is the recipe for your own image; .dockerignore keeps secrets and junk out; secrets go in at run time.
  • Docker Compose starts several containers together from one file, and containers find each other by service name.
  • Later modules use exactly this: Redis (Module 10), Milvus (Module 08) and Ollama (Module 20) in containers.

Meera wants to know why Saturday’s deploy was a disaster and Monday’s took five minutes.

“On Saturday I sent the server a recipe and hoped its kitchen matched mine,” Arjun says. “It did not: different Python, different Redis. Now I send a sealed kitchen. The box contains the exact Python, the exact libraries and the bot, and a second box contains the exact Redis. The server only needs to know how to plug boxes in.”

“Like sending Sunil and the oven together,” Meera says.

“Exactly. And when the AI work needs more servers later, it is one more box.”

Priya has been waiting for this. “Speaking of boxes: your bot stores conversations in a text file. When two customers write at the same moment, what happens to that file? Next lesson: a real database, and why every AI app still needs one.”

Go deeper

  • Docker documentation: installation, the getting-started guide, Dockerfile and Compose references.
  • uv documentation: its Docker integration guide explains the Dockerfile pattern above and how to pin uv’s version.
  • Ollama and Milvus documentation: the official Docker instructions you will follow in Modules 20 and 08.

Library and tool options change quickly. If a name or flag has moved, check the docs linked above.

Check your understanding

3 quick questions
1. What is the difference between an image and a container?
2. In Docker Compose, the bot container cannot reach Redis at localhost:6379. What is the fix?
3. You delete and recreate a Postgres container and all its data is gone. What was missing?