*ฅ^•ﻌ•^ฅ* ✨✨  HWisnu's blog  ✨✨ о ฅ^•ﻌ•^ฅ

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.

works-on-my-machine

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.

docker-whale

Prerequisite:

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:

Build the image

Run the image using the specified port

Run the image in detached mode

example: docker tag quart-server:latest myrepo/quart-server:latest

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:

Another set of important commands:

initializing the Docker process --> streamlined the setup process for various types of applications.

build and start the services as defined in the docker-compose.yml file.

cleanly stop and remove the resources created by docker compose up --build

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).

FROM nginx:1.27.0

RUN rm -rf /usr/share/nginx/html
COPY static /usr/share/nginx/html
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?!

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.