Docker packages an application and its runtime dependencies into an image that can be run as a container. It helps developers create more consistent environments across laptops, CI systems and servers. Instead of manually installing every dependency on every machine, a team can describe the environment and build an image from that definition.
Containers are useful, but they are not magic virtual machines. Understanding images, containers, ports, volumes and environment variables will help you use Docker safely and avoid common development problems.
1. What is a container?
A container is an isolated process environment that uses the host operating system's kernel. It has its own filesystem view, process and network configuration, subject to the container runtime and operating system.
A virtual machine usually includes a guest operating system and virtualized hardware. Containers generally share the host kernel, which makes them lighter to start and run in many workloads. The isolation model is different, so containers should not be treated as an automatic security boundary for hostile code.
2. Image versus container
A Docker image is a packaged, read-only template containing application files, dependencies and metadata. A container is a running or stopped instance created from an image.
You can create multiple containers from the same image. Each container has its own writable layer and runtime configuration, while the image provides the common base.
Useful commands:
docker images
docker ps
docker ps -a
The first lists local images. The second lists running containers. The third includes stopped containers.
3. Write a simple Dockerfile
A Dockerfile describes how to build an image. For a basic Python application:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
This example chooses a base image, sets a working directory, installs dependencies, copies application files and defines the default command. For a real service, pin and update base images deliberately, use a non-root user where practical, and avoid copying secrets into the image.
Build the image:
docker build -t task-api:dev .
Run it:
docker run --rm task-api:dev
The exact command depends on the application. A web server also needs a port mapping and possibly configuration.
4. Understand port mapping
A process inside a container listens on a container port. To access it from the host, publish a port:
docker run --rm -p 8000:8000 task-api:dev
The left side is the host port and the right side is the container port. The application must listen on an address reachable from outside the container, commonly 0.0.0.0, rather than only 127.0.0.1 inside the container.
Publishing a port makes a service reachable through the host's network interfaces according to the runtime and firewall configuration. Do not publish databases or admin interfaces publicly without authentication and network controls.
5. Understand volumes and persistent data
A container's writable layer is not the right place for important data that must survive container replacement. Volumes provide persistent storage managed by Docker, while bind mounts connect a host path to a container path.
Example named volume:
docker volume create database-data
docker run -d --name db \
-v database-data:/var/lib/postgresql/data \
postgres:16
The path shown is the standard data directory for that example image; verify the correct path for the exact image and version you use.
A bind mount can be useful during local development:
docker run --rm -it \
-v "$PWD:/app" \
-w /app \
python:3.12-slim python app.py
Mounting source code is convenient, but mounting over a directory can hide files that were already present in the image. Understand which side owns the data and what happens when the container is removed.
6. Configure environment variables
Applications often need configuration such as a port, environment name or database URL. Environment variables can pass configuration at runtime:
docker run --rm \
-e APP_ENV=development \
-e PORT=8000 \
task-api:dev
Do not bake passwords or API keys into a Dockerfile or image layer. Use a secret manager or the deployment platform's secret facility for sensitive values. A local .env file should be excluded from version control.
Environment variables are configuration, not a complete secret-management system. Restrict who can inspect container configuration and logs.
7. Use Docker Compose for multi-service projects
A web application may need an API and a database. Docker Compose describes multiple services in one YAML file and starts them together.
services:
api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://app:example@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- database-data:/var/lib/postgresql/data
volumes:
database-data:
This is a learning example, not a production secret configuration. Compose service names can be used for service-to-service communication on the project network. A depends_on relationship does not necessarily mean the database is ready to accept connections; applications should retry safely or use health checks and readiness logic.
Start the stack with docker compose up --build and stop it with docker compose down. Removing named volumes requires an additional explicit option; understand that data-loss effect before using it.
8. Make images smaller and safer
Use a small, maintained base image where appropriate. Add a .dockerignore file to exclude unnecessary files such as local environments, Git metadata, caches and secrets.
For example:
.git
.venv
__pycache__
.env
node_modules
dist
Use multi-stage builds when build tools are not needed at runtime. Copy only the artifacts required by the final image. Run as a non-root user when the application supports it, scan images, update dependencies and avoid running unnecessary services inside one container.
9. Debug common problems
| Symptom | Possible cause | What to inspect |
|---|---|---|
| Container exits immediately | Main process completed or crashed | docker logs and container exit code |
| Host cannot reach service | Port not published or app bound to localhost | Port mapping and listen address |
| Data disappears | Data stored only in container layer | Volume or bind mount configuration |
| Permission denied | Host/container user IDs differ | File ownership and container user |
| Image build is slow | Large build context or uncached steps | .dockerignore and Dockerfile order |
| API starts before database | Dependency is not ready | Health checks and retry logic |
Useful commands include docker logs, docker inspect, docker exec -it CONTAINER sh, and docker compose ps. Avoid placing secrets in commands that may be stored in shell history.
10. A practical learning project
Containerize a small API with one endpoint and a database. First build and run the API alone. Then publish its port, add a persistent database volume, and configure the connection through environment variables. Rebuild the image after changing dependencies and verify that database data survives container replacement.
Document the exact commands in the README. Test the project from a clean clone to confirm that another developer can reproduce it.
Frequently asked questions
Is a Docker container the same as a virtual machine?
No. Containers share the host kernel and isolate processes and resources differently from a VM, which typically runs a guest operating system.
Do containers keep data after they are removed?
Not reliably in their writable layer. Use a volume or bind mount for data that must persist.
Should I put secrets in environment variables?
Environment variables are common for configuration, but secrets should be supplied through a suitable secret-management mechanism and protected from logs and unauthorized inspection.
Final checklist
- Understand the difference between image and container.
- Use a Dockerfile to describe reproducible builds.
- Publish only the ports that need to be reachable.
- Use volumes for persistent data.
- Supply configuration at runtime.
- Keep secrets out of images and Git.
- Add .dockerignore and use multi-stage builds where useful.
- Add health checks, retries and monitoring for real services.
Docker becomes much easier when each concept has a clear job: images package software, containers run it, ports expose services, volumes persist data, and environment variables configure behaviour.