Lecture Overview
Now that we understand Docker's architecture and have Docker Desktop installed, it's time to run our first container! This session will guide you through the process of running containers, exploring their inner workings, and understanding the fundamental concepts in action. By the end of this session, you'll be comfortable with the basic container lifecycle and ready to start building your own containerized applications.
What Happens When You Run a Container?
Before diving into commands, let's understand what actually happens when you run a container. This background will help you make sense of what Docker is doing behind the scenes.
The Container Lifecycle
When you run a Docker container, several steps happen in sequence:
- Image Lookup: Docker checks if the requested image exists locally
- Image Pull: If not found locally, Docker downloads the image from a registry (usually Docker Hub)
- Container Creation: Docker creates a new container instance from the image
- Filesystem Setup: A writable layer is added on top of the image's read-only layers
- Network Setup: Docker configures networking for the container
- Process Start: The container's main process (defined in the image) is started
- Output Stream: The container's output is connected to your terminal (unless detached mode is used)
Analogy: Running a container is like constructing a modular building. The image is the blueprint and pre-fabricated components. When you decide to create a building (container), Docker first checks if it has the blueprint and materials (image) locally. If not, it orders them from the warehouse (registry). It then assembles the structure according to the blueprint, adds utilities like plumbing and electricity (networking), creates a space for new furnishings (writable layer), and finally turns on the power (starts the main process).
Understanding this process helps demystify what Docker is doing and makes troubleshooting easier when things don't work as expected.
Your First Container: Hello World
Let's start with the simplest possible container - the Docker "Hello World" image. This tiny container does just one thing: prints a message to the console and exits.
Running Hello World
Open your terminal or command prompt and type:
docker run hello-world
You should see output similar to this:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:fc6cf906cbfa013e80938cdf0bb199fbdbb86d6e3e013783e5a766f50f5dbce0
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
...
Let's break down what just happened:
- You told Docker to run a container based on the "hello-world" image
- Docker couldn't find the image locally, so it pulled it from Docker Hub
- Docker created a container from this image
- The container ran its programmed task (printing a message)
- The container exited after completing its task
This simple example demonstrates the basic workflow of running a container - from image retrieval to execution.
Note: The hello-world container exits immediately after printing its message. This is because containers run only as long as their main process is running. When that process completes, the container stops - but it still exists on your system in a stopped state.
Running an Interactive Container
The hello-world container is useful for testing, but it doesn't demonstrate the interactive nature of containers. Let's run a more useful container that gives us a shell to interact with.
Running Ubuntu
Type the following command:
docker run -it ubuntu bash
You should see a prompt change, indicating you're now inside the container:
root@f8d76af99df3:/#
Let's break down the command:
docker run: The command to create and start a container-it: Two flags combined:-ior--interactive: Keep STDIN open (allows input)-tor--tty: Allocate a pseudo-TTY (provides a terminal)
ubuntu: The image to use (an Ubuntu Linux distribution)bash: The command to run inside the container (the Bash shell)
Exploring Inside the Container
Now that you have a shell inside the container, try running some commands:
ls
cat /etc/os-release
apt update
apt install -y python3
python3 --version
exit
Notice a few important things:
- The container has its own filesystem, separate from your host computer
- You can install software inside the container without affecting your host
- When you exit the shell, the container stops because its main process (bash) has terminated
Real-world application: This ability to quickly spawn a clean Linux environment is incredibly useful for testing, troubleshooting, or isolating work. Need to test something on Ubuntu but you're running Windows or macOS? No problem - just spin up an Ubuntu container!
Running a Background Service Container
Many times, you'll want to run containers as background services rather than interactive processes. Let's run a web server container as a detached service.
Running NGINX Web Server
Type the following command:
docker run -d -p 8080:80 --name my-nginx nginx
Let's break down this command:
docker run: Create and start a container-dor--detach: Run the container in the background-p 8080:80or--publish 8080:80: Map port 8080 on your host to port 80 in the container--name my-nginx: Assign a name to the containernginx: The image to use (the NGINX web server)
After running this command, Docker will return a long string of characters. This is the container ID:
7f16d9e28b2a920a48eeb3e9f676949ee6b9be5d23a2e8ca84c6f93c50be62e3
Verifying the Container is Running
To check if your container is running, use:
docker ps
You should see output like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7f16d9e28b2a nginx "/docker-entrypoint.…" 10 seconds ago Up 9 seconds 0.0.0.0:8080->80/tcp my-nginx
Accessing the Web Server
Now open your web browser and navigate to:
http://localhost:8080
You should see the NGINX welcome page. Congratulations! You're running a web server in a container.
What's happening: The NGINX container is running the NGINX web server, which listens for HTTP requests on port 80 inside the container. We mapped port 8080 on our host machine to port 80 in the container with the -p 8080:80 flag. This means requests to localhost:8080 on our computer are forwarded to port 80 in the container.
Analogy: Port mapping is like setting up mail forwarding. When someone sends mail to your temporary address (host port 8080), the postal service (Docker) automatically forwards it to your actual residence (container port 80). The sender doesn't need to know about this forwarding - they just know the mail got delivered.
Managing Running Containers
Now that we have a container running, let's learn how to manage it.
Listing Running Containers
To see all running containers:
docker ps
To see all containers (including stopped ones):
docker ps -a
Viewing Container Logs
To see the logs from your NGINX container:
docker logs my-nginx
To follow the logs in real-time (like tail -f):
docker logs -f my-nginx
Press Ctrl+C to stop following the logs.
Stopping a Container
To stop the NGINX container:
docker stop my-nginx
This sends a SIGTERM signal to the main process in the container, allowing it to shut down gracefully. After a timeout, if the container hasn't stopped, Docker sends a SIGKILL signal.
Starting a Stopped Container
To start the container again:
docker start my-nginx
Your web server should be accessible again at http://localhost:8080.
Restarting a Container
To restart a container (stop and then start):
docker restart my-nginx
Removing a Container
First, stop the container if it's running:
docker stop my-nginx
Then remove it:
docker rm my-nginx
To force removal of a running container:
docker rm -f my-nginx
Warning: Removing a container permanently deletes its writable layer, including any changes you made inside the container after it was created. If you need to preserve data, use volumes (which we'll cover later).
Executing Commands in Running Containers
Often, you'll need to run commands inside an already-running container. Let's explore how to do this.
Starting a New NGINX Container
First, let's start a new NGINX container:
docker run -d -p 8080:80 --name my-nginx nginx
Running a Command Inside the Container
To execute a command in a running container:
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
For example, to see what processes are running inside the NGINX container:
docker exec my-nginx ps aux
You'll see something like:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 10628 5512 ? Ss 00:42 0:00 nginx: master process nginx -g daemon off;
nginx 31 0.0 0.0 11276 3240 ? S 00:42 0:00 nginx: worker process
...
Getting an Interactive Shell
For more complex interactions, you can get a shell inside the container:
docker exec -it my-nginx bash
Now you're inside the container with an interactive shell. You can run commands like:
ls -la /etc/nginx
cat /etc/nginx/nginx.conf
echo "Hello from inside the container" > /usr/share/nginx/html/hello.html
exit
After creating the hello.html file, you can access it in your browser at:
http://localhost:8080/hello.html
When to use exec: The exec command is extremely useful for:
- Debugging issues inside a container
- Modifying configuration files
- Running database migrations or management commands
- Inspecting the container environment
Understanding Container Isolation
One of Docker's key features is isolation. Let's explore what this means in practice.
Process Isolation
Each container has its own isolated process space. Let's demonstrate this by running two NGINX containers on different ports:
docker run -d -p 8080:80 --name nginx1 nginx
docker run -d -p 8081:80 --name nginx2 nginx
Now, let's get the process ID of the NGINX process in each container:
docker exec nginx1 ps aux | grep master
docker exec nginx2 ps aux | grep master
You'll notice that in both containers, the NGINX master process has PID 1. This is because each container has its own isolated process namespace.
Filesystem Isolation
Each container also has its own filesystem. Let's demonstrate this:
docker exec -it nginx1 bash -c "echo 'This is container 1' > /usr/share/nginx/html/index.html"
docker exec -it nginx2 bash -c "echo 'This is container 2' > /usr/share/nginx/html/index.html"
Now, if you visit http://localhost:8080, you'll see "This is container 1", and if you visit http://localhost:8081, you'll see "This is container 2". Each container has its own version of the file, unaffected by changes in the other container.
Network Isolation
Containers have their own network stacks as well. Let's check the IP addresses in each container:
docker exec nginx1 hostname -i
docker exec nginx2 hostname -i
You'll see different IP addresses, showing that each container has its own network identity.
Analogy: Container isolation is like having several different apartments in the same building. Each apartment has its own furniture, utilities, and address. The residents of one apartment can't see into or affect another apartment. They might live in the same building (host), but they have their own separate living spaces (containers).
Running a Python Application Container
Let's try something more relevant to web development - running a Python application in a container.
Creating a Simple Python App
First, let's create a simple Python application on your host machine. Create a new directory called python_docker_demo and add a file named app.py with the following content:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from Python inside Docker!"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
Also, create a requirements.txt file with:
flask==2.0.1
Running the Python App in a Container
Now, let's run this application in a Python container using volume mounting to share our code:
cd python_docker_demo
docker run -it --rm -p 5000:5000 -v "$(pwd):/app" -w /app python:3.9-slim bash -c "pip install -r requirements.txt && python app.py"
Let's break down this command:
-it: Interactive mode with a terminal--rm: Remove the container when it exits-p 5000:5000: Map port 5000 on the host to port 5000 in the container-v "$(pwd):/app": Mount the current directory on the host to /app in the container-w /app: Set the working directory inside the container to /apppython:3.9-slim: Use the Python 3.9 slim imagebash -c "pip install -r requirements.txt && python app.py": The command to run inside the container
After running this command, you should see Flask starting up. Open your browser and navigate to http://localhost:5000 to see the "Hello from Python inside Docker!" message.
Volume mounting: The -v "$(pwd):/app" option creates a bind mount that maps your current directory on the host to the /app directory in the container. This means any changes you make to your code on the host will be immediately visible inside the container, and vice versa.
Making Live Changes
With the container still running, edit the app.py file on your host and change the hello function to:
@app.route('/')
def hello():
return "Hello from Python inside Docker! I updated this without restarting the container!"
Save the file, refresh your browser, and you'll see the updated message. This is a powerful development workflow - your code runs in a container with a consistent environment, but you can edit it normally with your favorite editor on your host machine.
Development workflow: This pattern of mounting your code directory into a container is a common development workflow. It gives you the benefits of containerization (isolation, consistency) while still allowing for a fast feedback loop during development.
Understanding Container Lifecycle
Now that we've run several containers, let's understand the container lifecycle more fully.
Container States
A container can be in one of these states:
- Created: Container is created but not started
- Running: Container is running with its processes active
- Paused: Container processes are temporarily suspended
- Stopped/Exited: Container processes have terminated
- Removed: Container has been deleted
Let's see the container lifecycle in action:
# Create a container without starting it
docker create --name lifecycle-demo nginx
docker ps -a # See the created container with "Created" status
# Start the container
docker start lifecycle-demo
docker ps # See the container with "Up" status
# Pause the container
docker pause lifecycle-demo
docker ps # See the container with "Paused" status
# Unpause the container
docker unpause lifecycle-demo
docker ps # See the container with "Up" status again
# Stop the container
docker stop lifecycle-demo
docker ps -a # See the container with "Exited" status
# Remove the container
docker rm lifecycle-demo
docker ps -a # Container is gone
Container Exit Codes
When a container stops, it returns an exit code that indicates why it stopped:
- 0: Normal exit, process completed successfully
- 1-128: Error exit codes, indicating various problems
- 137: Container was killed (received SIGKILL)
- 143: Container was terminated (received SIGTERM)
You can see a container's exit code with:
docker inspect -f '{{.State.ExitCode}}' container_name
Analogy: The container lifecycle is like a light bulb. It can be installed (created), turned on (started), dimmed (paused), turned off (stopped), or removed entirely. The exit code is like the reason the light went out - did someone turn it off normally, did it burn out, or was there a power outage?
Container Resource Constraints
By default, containers can use as much of the host's CPU, memory, and I/O resources as they need. However, you can limit these resources to ensure one container doesn't starve others.
Setting Memory Limits
To limit a container to using a maximum of 512MB of memory:
docker run -d --name memory-limited --memory=512m nginx
Setting CPU Limits
To limit a container to using at most 0.5 CPU cores:
docker run -d --name cpu-limited --cpus=0.5 nginx
Viewing Resource Usage
To see how much resources your containers are using:
docker stats
This displays a live stream of resource usage statistics for all running containers.
Why limit resources: In production environments, resource limits are crucial for several reasons:
- Preventing one container from consuming all host resources
- Ensuring predictable performance
- Aligning with resource allocation in orchestration systems like Kubernetes
- Capacity planning and cost management in cloud environments
Container Networking Basics
We've already used port publishing with -p, but let's explore container networking a bit more.
Network Types
Docker provides several network types:
- bridge: The default network. Containers can talk to each other if they're on the same bridge.
- host: Container shares the host's network stack (no isolation).
- none: Container has no external network connectivity.
- user-defined networks: Custom networks you create.
Listing Networks
docker network ls
Inspecting a Network
docker network inspect bridge
Running a Container on the Host Network
To run a container that shares the host's network stack:
docker run -d --network=host --name host-nginx nginx
With host networking, you don't need to publish ports with -p because the container uses the host's ports directly. You can access NGINX at http://localhost:80 (note the port is 80, not 8080).
Container to Container Communication
Let's demonstrate how containers can communicate with each other. First, create a custom network:
docker network create my-network
Run two containers on this network:
docker run -d --network=my-network --name container1 nginx
docker run -d --network=my-network --name container2 alpine sleep 1000
Now, container2 can reach container1 by its name:
docker exec container2 wget -qO- container1
This command uses wget inside container2 to make an HTTP request to container1, which is running NGINX. The containers can communicate using their names as hostnames, which Docker resolves to the appropriate IP addresses.
Analogy: Docker networks are like different neighborhoods in a city. Containers in the same network (neighborhood) can easily talk to each other. The bridge network is like a gated community with controlled access to the outside world. The host network is like living in the same house as the host - no separation at all. And containers with no network are like isolation cells with no external communication.
Practical Exercises
Now let's put everything together with some practical exercises to reinforce what we've learned.
Exercise 1: Web Server with Custom Content
- Create a directory called
nginx_demoon your host machine - Inside this directory, create a file named
index.htmlwith some custom HTML content - Run an NGINX container that serves this custom content by mounting the directory
- Verify you can access the custom content in your browser
Solution:
mkdir nginx_demo
echo "<h1>My Custom NGINX Content</h1><p>This is being served from a Docker container!</p>" > nginx_demo/index.html
docker run -d -p 8080:80 -v "$(pwd)/nginx_demo:/usr/share/nginx/html" --name custom-nginx nginx
# Open browser to http://localhost:8080
Exercise 2: Redis Database Container
- Run a Redis container in the background
- Connect to the Redis CLI inside the container
- Set a key-value pair in Redis
- Retrieve the value to verify it was stored
Solution:
docker run -d --name my-redis redis
docker exec -it my-redis redis-cli
> set mykey "Hello from Redis"
OK
> get mykey
"Hello from Redis"
> exit
Exercise 3: Container Resource Monitoring
- Run an NGINX container with 256MB memory limit
- Run another NGINX container with no memory limit
- Use
docker statsto compare their resource usage
Solution:
docker run -d --name limited-nginx --memory=256m nginx
docker run -d --name unlimited-nginx nginx
docker stats
Troubleshooting Container Issues
Even with simple containers, things can go wrong. Let's explore some common issues and how to troubleshoot them.
Container Exits Immediately
If a container exits immediately after starting, check:
- The exit code:
docker ps -a - The container logs:
docker logs container_name - Try running the container in interactive mode to see the error:
docker run -it image_name command
Can't Access Container via Published Port
If you can't access a container via a published port, check:
- If the container is running:
docker ps - If the port is correctly published:
docker port container_name - If the application inside the container is actually listening on the expected port:
docker exec container_name netstat -tulpn - If there are any firewall issues on your host
Container Has No Internet Access
If a container can't access the internet, check:
- DNS resolution:
docker exec container_name ping 8.8.8.8 docker exec container_name ping google.com - If the first ping works but the second doesn't, it's a DNS issue. Check the container's DNS configuration:
docker exec container_name cat /etc/resolv.conf - Your host's networking and firewall settings
Volume Mount Issues
If volume mounts aren't working as expected, check:
- If the paths are correct and absolute:
Look for the "Mounts" section in the output.docker inspect container_name - Permissions on the host directory
- If you're on Windows, path format issues (use forward slashes)
Best Practices for Running Containers
Let's wrap up with some best practices for running containers:
Naming Containers
Always use meaningful names for containers with the --name flag. This makes it much easier to manage them later.
Using the --rm Flag
For short-lived containers, especially those used for development or testing, use the --rm flag to automatically remove the container when it exits. This prevents accumulating stopped containers.
Setting Resource Limits
Especially in production, always set appropriate resource limits to prevent one container from starving others.
Handling Container Logs
For containers that generate a lot of logs, use log rotation or a logging driver to prevent disk space issues:
docker run -d --log-opt max-size=10m --log-opt max-file=3 nginx
Security Considerations
Run containers with the principle of least privilege:
- Don't run containers as root if possible:
docker run -d --user 1000:1000 nginx - Use read-only file systems where appropriate:
docker run -d --read-only nginx - Only publish the ports you need
Using Environment Variables
Use environment variables for configuration:
docker run -d -e DATABASE_URL=postgres://user:pass@db:5432/mydb my-app
Checking Container Health
For important containers, add health checks:
docker run -d --health-cmd="curl -f http://localhost/ || exit 1" --health-interval=5s nginx
Key Takeaways
- Docker containers are isolated, lightweight environments for running applications
- Basic container lifecycle: create, start, stop, remove
- Containers can be run interactively (-it) or in the background (-d)
- Port publishing (-p) maps container ports to host ports
- Volume mounts (-v) allow sharing files between host and container
- The exec command lets you run commands in a running container
- Containers have isolated process spaces, filesystems, and networks
- Resource constraints help manage container resource usage
- Proper container management includes naming, cleanup, and security practices
With these fundamentals, you're well on your way to mastering Docker containers!
Looking Ahead
In our next session, we'll explore Docker Hub and public images, which will expand your container toolkit significantly. We'll learn how to find, evaluate, and use pre-built images for a wide variety of applications and services.
Discussion Questions
- How might containers improve your development workflow compared to traditional local development?
- What challenges do you anticipate when working with containers in your projects?
- How could you use the container isolation properties we explored to improve application security?
- What types of applications do you think are best suited for containerization? Are there any that might not be good candidates?
- How would you explain the value of containerization to a non-technical team member?
Additional Resources
- Docker Run Reference - Comprehensive documentation on the docker run command
- Docker Exec Reference - Details on executing commands in running containers
- Docker Storage Guide - In-depth information on volumes and bind mounts
- Docker Resource Constraints - How to limit container resources
- Play with Docker - Interactive environment for experimenting with Docker