
Getting Started with Docker Compose
Docker Fundamentals: Containers, Images, Compose, Persistence, and Publishing
What is Docker and Why Use It?
Docker is a platform that enables applications to be packaged into containers.
A container is a lightweight, portable, and isolated execution unit that includes everything required to run an application:
- Source code
- Runtime
- System tools
- Libraries
- Dependencies
Docker is widely used in modern software engineering for building, shipping, and running applications consistently across different environments.
Without Docker: The Problem Docker Solves
Before containerization, software deployment often suffered from environment inconsistency.
Common issues included:
- Applications behaving differently across development, testing, and production
- Dependency conflicts, such as different library versions installed on the same machine
- “It works on my machine” problems
- Complex setup procedures for new developers
- Harder scaling and replication of environments
In practice, this creates fragile deployments where reproducibility is not guaranteed.
With Docker: Why It Exists
Docker solves these issues by packaging the application and its dependencies into a container image.
Benefits of Docker:
- Same environment runs everywhere
- Dependencies are isolated per application
- Fast project setup using a single command
- Lightweight compared to virtual machines
- Easy to scale and deploy across systems and cloud platforms
Container vs Virtual Machine
| Feature | Virtual Machine | Container |
|---|---|---|
| What it is | Full computer inside your computer | Lightweight package for an app |
| OS | Each VM has its own OS | Shares the host OS |
| Size | Heavy, usually GBs | Light, usually MBs |
| Startup speed | Slow to start | Starts very fast |
| Resource usage | High | Low |
| Isolation | Strong | Medium |
| Best for | Running full systems or different OS environments | Running apps consistently everywhere |
Docker Desktop Installation on Fedora
This is a simplified practical version of Docker Desktop installation for Fedora Linux.
1. Requirements
Before installing Docker Desktop, make sure the following requirements are satisfied:
- Fedora 42 or 43, 64-bit
- GNOME Desktop is recommended
If using GNOME, install terminal support:
sudo dnf install gnome-terminal
2. Download Docker Desktop RPM
Download the latest Docker Desktop RPM package for Fedora.
Package name:
docker-desktop-x86_64.rpm
3. Install Docker Desktop
Run this command in the directory where the .rpm file is located:
sudo dnf install ./docker-desktop-x86_64.rpm
Docker Desktop will be installed into:
/opt/docker-desktop
4. Start Docker Desktop
Option A: GUI
Open Docker Desktop from the application menu.
Steps:
- Open the Applications Menu
- Launch Docker Desktop
- Accept the license agreement
Option B: Terminal
systemctl --user start docker-desktop
5. Enable Auto-Start
This step is optional.
systemctl --user enable docker-desktop
6. Verify Installation
Check Docker CLI:
docker --version
Check Docker Compose:
docker compose version
Check Docker Engine:
docker version
Expected result:
- Docker Engine client is installed
- Docker Compose V2 is available
- Docker Desktop is running properly
7. Stop Docker Desktop
systemctl --user stop docker-desktop
8. Upgrade Docker Desktop
Docker Desktop does not automatically upgrade through the system package manager.
Manual upgrade process:
sudo dnf remove docker-desktop
sudo dnf install ./docker-desktop-x86_64.rpm
Running Hello World Using Docker
Run the Docker hello-world container:
docker run hello-world
Pull the hello-world image manually:
docker pull hello-world
Docker Containers and Images
Images
Images are templates for containers.
An image specifies:
- File system
- Users
- Default command
- Runtime configuration
- Application dependencies
Images are what you download, build, and share.
List Docker images:
docker image ls
Containers
Containers are running instances of images.
A container is based on an image.
You can have many containers running from the same image.
List all containers:
docker ps -a
Port Mapping
Pull Nginx Image
docker pull nginx
To interact with the Nginx server from the host machine, publish the container port.
Example:
docker run -p 80:80 nginx
Alternative:
docker run -p 8080:80 nginx
Explanation:
8080:80
| Part | Meaning |
|---|---|
| 8080 | Host machine port |
| 80 | Container port |
Run Container in the Background
Use detached mode:
docker run -p 8080:80 -d nginx
Check running containers:
docker ps
Specify a Container Name
docker run -p 8080:80 -d --name my-nginx nginx
Stop the container:
docker stop my-nginx
Delete all stopped containers:
docker container prune
Automatically Remove Container After Stop
Use --rm:
docker run -p 8080:80 -d --name my-nginx --rm nginx
Docker Image Tags
Pull or Run a Specific Image Version
docker run nginx:1.27
Warning:
Tags are mutable.
This means the same tag can point to a different image in the future.
A more specific tag is safer:
docker run nginx:1.27.0-bookworm
Tagging by Digest
Tagging by digest is more reproducible because the digest points to an exact image.
Example:
docker run nginx@sha256:5aca99593157f4ae539a5dec1092a0ad8762f8e2eb1789085a13a0f5622369f6
Runtime Environment Variables and Arguments
Example using Python:
docker run -e ABC=123 -e DEF=456 python:3.12 python -c "import os; print(os.environ)"
Explanation:
| Option | Meaning |
|---|---|
-e ABC=123 | Sets environment variable ABC |
-e DEF=456 | Sets environment variable DEF |
python:3.12 | Image name |
python -c | Command executed inside the container |
Slim and Alpine Images
Slim and Alpine images are smaller versions of full images.
Pull Python slim image:
docker pull python:3.12-slim
Pull Python Alpine image:
docker pull python:3.12-alpine
Use slim or Alpine images when you want smaller image sizes.
Debugging Containers
Execute a shell inside a running container:
docker exec -it <container-id-or-name> /bin/bash
Explanation:
| Option | Meaning |
|---|---|
-i | Interactive mode |
-t | Allocates a terminal |
/bin/bash | Shell to execute inside the container |
Docker Persistence and Mount Types
Persistence
Docker containers are ephemeral by default.
That means when a container stops or is removed, internal files can disappear unless persistence is configured.
Persistence allows data to remain even after:
- Container restart
- Container removal
- Image rebuild
- System reboot
Docker provides different mount types for persistence and storage management.
Mount Types
Docker supports three common mount types:
- Volumes
- Bind mounts
- Tmpfs mounts
1. Volumes
A volume is Docker-managed storage.
Example:
docker run -v mydata:/data python:3.12
Explanation:
| Part | Meaning |
|---|---|
mydata | Docker volume |
/data | Folder inside the container |
Best for:
- Databases
- Production applications
- Persistent application data
Docker manages the storage automatically.
2. Bind Mounts
A bind mount directly connects a host folder to a container folder.
Example:
docker run -v $(pwd):/data python:3.12
Explanation:
| Part | Meaning |
|---|---|
$(pwd) | Current host directory |
/data | Folder inside the container |
Best for:
- Development
- Source code syncing
- Live editing
Files are directly visible on the host machine.
3. Tmpfs Mounts
A tmpfs mount is a temporary in-memory filesystem.
Example:
docker run --tmpfs /data python:3.12
Characteristics:
- Stored only in RAM
- Removed when the container stops
- Never written to disk
Best for:
- Temporary cache
- Sensitive temporary data
- High-speed temporary storage
Mount Type Comparison
| Feature | Volume | Bind Mount | Tmpfs Mount |
|---|---|---|---|
| Managed by | Docker | Host OS | Memory / RAM |
| Persistent | Yes | Yes | No |
| Stored on disk | Yes | Yes | No |
| Best for | Production data | Development | Temporary data |
| Host access | Limited | Full access | None |
| Performance | High | Depends on host filesystem | Very fast |
| Security | Safer | Less isolated | High |
| Common usage | Databases | Source code | Cache / secrets |
Persistence Example
Without Persistence
docker run python:3.12 python -c 'f="data.txt";open(f,"a").write(f"Ran!
");print(open(f).read())'
Output:
Ran!
The file disappears after the container exits.
Using Docker Volume
docker run -v mydata:/data python:3.12 python -c 'f="/data/data.txt";open(f,"a").write(f"Ran!
");print(open(f).read())'
First run:
Ran!
Second run:
Ran!
Ran!
The file persists because /data is connected to the Docker volume mydata.
Custom Images
Building Your Own Dockerfile
Example project:
MySite
└── frontend
├── static
│ └── index.html
└── Dockerfile
Build Image from Root Path
docker build -t mysite .
Specify Dockerfile Path
docker build -t mysite -f ./Dockerfile .
Nginx Frontend Example
Dockerfile
FROM nginx:1.27.0
RUN rm -rf /usr/share/nginx/html
COPY static /usr/share/nginx/html
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>
<input type="text" id="newItem" placeholder="Add a new todo item" />
<button onclick="addItem()">Add</button>
<ul id="todoList"></ul>
<script>
function addItem() {
const input = document.getElementById('newItem');
const list = document.getElementById('todoList');
const li = document.createElement('li');
li.textContent = input.value;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = function () {
list.removeChild(li);
};
li.appendChild(deleteBtn);
list.appendChild(li);
input.value = '';
}
</script>
</body>
</html>
Docker Layers, Immutability, and Caching
Layers
Docker images are made of layers.
Each command in a Dockerfile creates a new layer.
Example:
FROM nginx:1.27.0
COPY . /app
RUN npm install
| Command | Layer Type |
|---|---|
FROM | Base layer |
COPY | File layer |
RUN | Execution layer |
Layers work like image diffs and only store filesystem changes.
Immutability
Docker layers are immutable.
That means:
- Existing layers cannot be modified
- Changing or deleting files creates a new layer
- Old layers remain unchanged
Example:
RUN rm -rf /tmp/files
Docker creates a new layer that marks the files as deleted.
Caching
Docker caches layers by default.
If a command has not changed, Docker reuses the cached layer instead of rebuilding it.
Example:
FROM node:22
COPY package.json .
RUN npm install
COPY . .
If only the source code changes:
FROMis cachedCOPY package.jsonis cachedRUN npm installis cached- Only
COPY . .rebuilds
Cache Invalidation
Changing an earlier layer invalidates the layers after it.
Example change:
FROM nginx:1.27.0
to:
FROM nginx:1.28.0
Docker will:
- Pull the new image
- Rebuild all following layers
- Rebuild because the base layer changed
Build Without Cache
docker build --no-cache .
Docker Layer Summary
| Concept | Description |
|---|---|
| Layer | Filesystem change created by a command |
| Immutable | Layers cannot be modified |
| Cache | Reuses unchanged layers |
| Cache invalidation | Rebuilds layers after a change |
| Image | Collection of stacked layers |
Custom Backend Image
Example file tree:
MySite
├── backend
│ ├── src
│ │ └── mysite
│ │ └── main.py
│ ├── Dockerfile
│ └── requirements.txt
└── frontend
├── static
│ └── index.html
└── Dockerfile
Backend Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY src/mysite mysite
EXPOSE 8000
CMD ["uvicorn", "mysite.main:app", "--host", "0.0.0.0", "--port", "8000"]
Multi-Stage Builds
A multi-stage build separates the build stage from the runtime stage.
Flow:
Build Stage
↓
Run Stage
↓
Packaged Application
This helps make the final image smaller and cleaner.
Multi-Stage Dockerfile Example
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml requirements.txt ./
RUN pip wheel --no-cache-dir --no-deps --wheel-dir wheels -r requirements.txt
COPY src src
RUN pip wheel --no-cache-dir --no-deps --wheel-dir wheels .
FROM python:3.12-slim AS runner
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
EXPOSE 8000
CMD ["uvicorn", "mysite.main:app", "--host", "0.0.0.0", "--port", "8000"]
pyproject.toml
Create a pyproject.toml file so the application can be installed as a Python package.
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "mysite"
requires-python = ">=3.12"
version = "0.0.1"
dependencies = [
"fastapi"
]
[tool.setuptools.packages.find]
where = ["src"]
Updated File Tree
MySite
├── backend
│ ├── src
│ │ └── mysite
│ │ ├── __init__.py
│ │ └── main.py
│ ├── Dockerfile
│ ├── pyproject.toml
│ └── requirements.txt
└── frontend
├── static
│ └── index.html
└── Dockerfile
Docker Compose
Orchestrating Many Containers
Managing multiple containers manually can become cumbersome.
Real applications may include:
- Frontend
- Backend
- Database
- Load balancer
- Cache
- Task queue
Docker Compose is a tool used to manage these container compositions.
To use Docker Compose, create a file named:
docker-compose.yml
Docker Compose Example
services:
backend:
image: mysite-backend
container_name: mysite-backend
build:
context: ./backend
dockerfile: Dockerfile
target: runner
ports:
- "8000:8000"
frontend:
image: mysite-frontend
pull_policy: never
container_name: mysite-frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
Docker Compose Commands
Build all images:
docker compose build
Start all services:
docker compose up
Stop services:
CTRL + C
Stop and delete containers:
docker compose down
Adding a Database: MongoDB
Why Add a Database?
Without a database, the application stores data only while the application is running.
A database allows the application to:
- Persist data between application restarts
- Store application records permanently
- Support multiple users accessing the same data
- Scale application data management
For this project, MongoDB is used as the database.
MongoDB Container with Docker Compose
services:
backend:
image: mysite-backend
container_name: mysite-backend
build:
context: ./backend
dockerfile: Dockerfile
target: runner
ports:
- "8000:8000"
env_file:
- ./backend/.env
frontend:
image: mysite-frontend
pull_policy: never
container_name: mysite-frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
mongodb:
image: mongo:7.0.12
container_name: mysite-mongodb
volumes:
- mongodb_data:/data/db
env_file:
- ./mongodb/.env
volumes:
mongodb_data:
Why Use Volumes?
Containers are temporary by nature.
Without a volume:
- Data is lost when the container is removed
With a volume:
- Data remains available after container recreation
- Database files are stored outside the container lifecycle
- The database becomes persistent
Security
Avoid Hardcoded Secrets
Sensitive information should never be stored directly in source code.
Bad example:
MONGODB_CONNECTION_STRING = "mongodb://localhost:27017"
Better example:
import os
MONGODB_CONNECTION_STRING = os.environ["MONGODB_CONNECTION_STRING"]
Benefits:
- Keeps secrets out of source control
- Supports multiple environments
- Improves deployment security
Environment Variables with .env
Create a .env file for application configuration.
Example:
MONGODB_CONNECTION_STRING=mongodb://mongodb:27017
Load the file using Docker Compose:
services:
backend:
env_file:
- ./backend/.env
Benefits:
- Centralized configuration
- Easier environment management
- Improved security practices
- Simplified deployment process
Startup Order
Applications often depend on other services.
For this project:
Frontend
↓
Backend
↓
MongoDB
The backend requires MongoDB to be available before database operations can be performed.
Configure service dependencies:
services:
backend:
depends_on:
- mongodb
frontend:
depends_on:
- backend
This ensures Docker Compose starts services in the correct order.
Important note:
depends_oncontrols startup order, but it does not always guarantee that the dependent service is fully ready to accept connections.
For production-level reliability, use health checks or retry logic in the application.
Building and Running the Application
Build all services:
docker compose build
Build and start all services:
docker compose up
Start services in detached mode:
docker compose up -d
View running containers:
docker compose ps
View logs:
docker compose logs
Stop services:
CTRL + C
Stop and remove containers:
docker compose down
Stop and remove containers, networks, and volumes:
docker compose down -v
Docker Compose depends_on
Using depends_on, Docker Compose can start services in dependency order.
Example:
services:
backend:
image: mysite-backend
container_name: mysite-backend
build:
context: ./backend
dockerfile: Dockerfile
target: runner
ports:
- "8000:8000"
env_file:
- ./backend/.env
depends_on:
- mongodb
frontend:
image: mysite-frontend
pull_policy: never
container_name: mysite-frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
mongodb:
image: mongo:7.0.12
container_name: mysite-mongodb
volumes:
- mongodb_data:/data/db
env_file:
- ./mongodb/.env
mongo-express:
image: mongo-express:1.0.2
container_name: mysite-mongo-express
ports:
- "8081:8081"
env_file:
- ./mongo-express/.env
depends_on:
- mongodb
volumes:
mongodb_data:
Publishing Images
Building images locally is useful for development, but in real-world projects we often publish images to a container registry.
A container registry allows images to be downloaded and deployed from anywhere.
Common container registries include:
- Docker Hub
- GitHub Container Registry
- Amazon Elastic Container Registry
- Google Artifact Registry
- Azure Container Registry
For this project, Docker Hub is used as the image registry.
Why Publish Images?
Without a registry:
Developer Machine
└── Docker Image
The image only exists on the local machine.
With a registry:
Developer
↓
Docker Hub
↓
Server
Any machine can pull and run the image.
Create Docker Hub Repositories
Create repositories for each application component:
nightdev19/mysite-backend
nightdev19/mysite-frontend
Build Images Locally
Backend:
docker build -t nightdev19/mysite-backend:latest ./backend
Frontend:
docker build -t nightdev19/mysite-frontend:latest ./frontend
Verify images:
docker images
Example output:
REPOSITORY TAG IMAGE ID
nightdev19/mysite-backend latest abc123
nightdev19/mysite-frontend latest def456
Login to Docker Hub
docker login
Enter your Docker Hub credentials.
Push Images Manually
Push backend image:
docker push nightdev19/mysite-backend:latest
Push frontend image:
docker push nightdev19/mysite-frontend:latest
The images are now available in Docker Hub.
Pull Images
On another machine:
docker pull nightdev19/mysite-backend:latest
docker pull nightdev19/mysite-frontend:latest
Automating Image Publishing with GitHub Actions
Instead of manually building and pushing images, GitHub Actions can automate the process whenever code is pushed to the repository.
Workflow:
Git Push
↓
GitHub Actions
↓
Build Docker Images
↓
Push Images to Docker Hub
Docker Hub Secrets
Store Docker Hub credentials securely in GitHub.
Location:
Repository
└── Settings
└── Secrets and variables
└── Actions
Create these secrets:
DOCKERHUB_USERNAME
DOCKERHUB_TOKEN
Generate the Docker Hub token from:
Docker Hub
└── Account Settings
└── Personal Access Tokens
GitHub Actions Workflow
File:
.github/workflows/docker-push.yml
Workflow:
name: Docker Push
on:
push:
branches: [main]
jobs:
docker-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Build backend image
run: |
docker build \
-t nightdev19/mysite-backend:latest \
./backend
- name: Build frontend image
run: |
docker build \
-t nightdev19/mysite-frontend:latest \
./frontend
- name: Push backend image
run: docker push nightdev19/mysite-backend:latest
- name: Push frontend image
run: docker push nightdev19/mysite-frontend:latest
Publishing Workflow
Developer
↓
git push origin main
↓
GitHub Actions
↓
Build Images
↓
Push to Docker Hub
↓
Docker Hub Registry
After the workflow is configured, manual image building and pushing is no longer required.
Development vs Production
Development Compose
In development, Docker Compose can build images directly from the local source code.
Example:
build:
context: ./backend
This is useful because local development is fast and flexible.
Production Compose
In production, Docker Compose should use published images from the registry.
Example:
image: nightdev19/mysite-backend:latest
Docker pulls the published image from Docker Hub.
This separation allows:
- Local development to remain fast
- Production deployments to use tested images
- Servers to pull images without needing the source code
Benefits of Publishing Images
Publishing images provides:
- Consistent deployments
- Reproducible environments
- Centralized image storage
- Automated publishing
- Easier production deployments
- Foundation for CI/CD pipelines
Publishing images is a key step toward a complete CI/CD workflow and allows applications to be deployed reliably across multiple environments.
Deployment to the Cloud
Deploying to the cloud is optional.
You can run Docker directly on your own server, but many real-world projects use managed cloud services.
Common deployment options:
AWS
- ECS
- EKS
- Lambda
- App Runner
- Lightsail
Google Cloud Platform
- Compute Engine
- Google Kubernetes Engine
- Cloud Run
Azure
- Azure Container Instances
- Azure Kubernetes Service
- Azure Functions
- Azure App Service
There are also many Platform-as-a-Service providers that can run containerized applications.
Final Notes
Docker helps developers build, run, and deploy applications consistently.
The core workflow is:
Write application code
↓
Create Dockerfile
↓
Build image
↓
Run container
↓
Use Docker Compose for multiple services
↓
Publish images to a registry
↓
Deploy to server or cloud
Docker becomes more powerful when combined with:
- Docker Compose
- Volumes
- Environment variables
- GitHub Actions
- Container registries
- Cloud deployment platforms