Docker - Compose
Introduction
Ever heard the phrase: "It works on my machine!"? This phrase encapsulates a common frustration in modern application deployment, where developers often encounter inconsistencies between their local environments and those of their colleagues or production servers.
For instance, Dev A might run the application flawlessly on their PC, while Dev B struggles to replicate the same success, even with the exact same workflow and dependencies. These discrepancies, due to OS differences, software versions, or configurations, cause delays and increased troubleshooting.
Docker
Docker resolves this issue by offering a consistent environment via containers. By bundling the app and its dependencies, Docker ensures uniform execution across machines, eliminating the "it works on my machine" problem. This consistency simplifies deployment and enhances team collaboration with a reproducible environment.
Prerequisite:
- Installed Docker Desktop
- Create an account in Docker website
- Dockerfile inside the project folder.
Important commands
The entirety of this post I'm going to jump straight to Docker Compose which is the more interesting use case of Docker. For beginner / introduction to Docker you can find plenty of tutorials out there. Before jumping ahead to Compose, there are some commands you need to know:
- docker build -t app-name .
Build the image
- docker run -p 8000:8000 app-name
Run the image using the specified port
- docker run -p 8000:8000 -d app-name
Run the image in detached mode
- docker tag app-name:tagname new-repo:tagname
example: docker tag quart-server:latest myrepo/quart-server:latest
- docker push new-repo:tagname
example: docker push myrepo/quart-server:latest
Docker Compose
Now let's get down to business, Compose is much more useful if you're building applications with real functionality ~ more often than not you'll have several components of a project --> the part where Compose shines!
For example you're building a web app, at minimum there are 2 main components: backend and frontend. Compose will help you orchestrate the build process and deployment of your app.
Prerequisite:
- docker-compose.yml
Another set of important commands:
- docker init
initializing the Docker process --> streamlined the setup process for various types of applications.
- docker compose up --build
build and start the services as defined in the
docker-compose.yml
file.
- docker compose down
cleanly stop and remove the resources created by
docker compose up --build
- docker image prune
- docker container prune
These two commands are my favorite to cleanup unused images and containers. Do these often and you won't be hoarding useless unused images and containers which could take sizeable amount in storage.
Example project: FastAPI+MongoDB ToDo app
Consists of two components: backend (fastapi + mongodb) and frontend (nginx).
- Frontend Dockerfile:
FROM nginx:1.27.0
RUN rm -rf /usr/share/nginx/html
COPY static /usr/share/nginx/html
- Backend Dockerfile:
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 /wheels/* && rm -rf /wheels
EXPOSE 8000
CMD ["uvicorn", "mysite.main:app", "--host", "0.0.0.0", "--port", "8000"]
Note: instead of using standard version of a runtime, often your app works fine with the slim or alpine version. One of the benefit of using the slim and alpine versions: much lower build size --> slim is usually 50% lower while alpine can be 80-90% lower size --> you're saving massive amount of storage!
The other day I saw someone on X/twitter said Docker is stupid: "why a simple HelloWorld app need to be 1 GB in size?". Using alpine version for the HelloWorld app would reduce the size to < 80 MB, well guess who's stupid now?!
- docker-compose.yml
services:
backend:
image: fastapi-mongo-todos-backend
pull_policy: never
container_name: fastapi-mongo-todos-backend
build:
context: ./backend
dockerfile: Dockerfile
target: runner
ports:
- 8000:8000
env_file:
- ./backend/.env
frontend:
image: fastapi-mongo-todos-frontend
pull_policy: never
container_name: fastapi-mongo-todos-frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- 80:80
mongodb:
image: mongo:7.0.12
container_name: fastapi-mongo-todos-mongodb
volumes:
- mongodb-data:/data/db
env_file:
- ./mongodb/.env
mongo-express:
image: mongo-express:1.0.2
ports:
- 8081:8081
container_name: fastapi-mongo-todos--mongo-express
env_file:
- ./mongo-express/.env
volumes:
mongodb-data:
Summary
Docker and containers are essential in modern software development, solving the "it works on my machine" dilemma. Their consistent, portable, and efficient approach to packaging and deploying apps ensures smoother operations, improved collaboration, and more reliable software delivery.