> For the complete documentation index, see [llms.txt](https://fastapi-docs.duonghuuphuc.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fastapi-docs.duonghuuphuc.com/unit-21-containerizing-and-deploying-microservices.md).

# Unit 21: Containerizing and Deploying Microservices

Containerize FastAPI microservices and deploy them together using Docker Compose, container networking, and environment-based configuration.

### 1. Scenario

In Unit 19, we built two independently running microservices:

* **Product Service**
* **Order Service**

They communicated through REST:

```
Order Service
     │
     │ GET /products/{product_id}
     ▼
Product Service
```

Until now, both services have run directly on the local machine using Uvicorn.

In this unit, we will package each service into a **Docker container** and deploy the complete system using **Docker Compose**.

The final structure is:

```
unit21-containers/
│
├── product_service/
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
│
├── order_service/
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
│
└── docker-compose.yml
```

***

## 2. Containerize Product Service

Use the Product Service developed in Unit 19.

Inside `product_service`, create:

```
requirements.txt
```

with:

```
fastapi
uvicorn
```

Create:

```
Dockerfile
```

with:

```dockerfile
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

The main instructions are:

| Instruction | Purpose                                        |
| ----------- | ---------------------------------------------- |
| `FROM`      | Select the base image                          |
| `WORKDIR`   | Set the working directory                      |
| `COPY`      | Copy files into the image                      |
| `RUN`       | Execute commands while building                |
| `CMD`       | Define the command used to start the container |

From the `product_service` directory, build the image:

```bash
docker build -t product-service .
```

Run the container:

```bash
docker run --name product-service -p 8000:8000 product-service
```

The port mapping:

```
8000:8000
  │    │
  │    └── Container port
  └─────── Host port
```

Test the Product Service:

```
http://localhost:8000/products
```

Stop and remove the container before continuing:

```bash
docker stop product-service
docker rm product-service
```

***

## 3. Containerize Order Service

Inside `order_service`, create:

```
requirements.txt
```

with:

```
fastapi
uvicorn
httpx
```

Create the same `Dockerfile`:

```dockerfile
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

Notice that both containers can use port `8000` internally.

Later, Docker will map them to different host ports.

***

## 4. Container Networking

In Unit 19, Order Service used:

```python
PRODUCT_SERVICE_URL = "http://127.0.0.1:8000"
```

This will not work correctly between separate containers.

Inside a container:

```
localhost
```

refers to **that container itself**, not another container.

With Docker Compose, services can communicate using their **service names**.

For example:

```
http://product-service:8000
```

Therefore:

```
Order Container
      │
      │ http://product-service:8000
      ▼
Product Container
```

This is one of the most important differences between running services directly on a computer and running them inside containers.

***

## 5. Use Environment Variables

We should not hard-code the deployment address into Order Service.

Modify `order_service/main.py`:

```python
import os
```

Replace the previous Product Service URL with:

```python
PRODUCT_SERVICE_URL = os.getenv(
    "PRODUCT_SERVICE_URL",
    "http://127.0.0.1:8000"
)
```

The default value allows Order Service to continue working when run locally.

When deployed with Docker, we will provide another value:

```
http://product-service:8000
```

This separates:

```
Application Code
      │
      └── Business logic

Deployment Configuration
      │
      └── Service addresses, ports, credentials, etc.
```

The same source code can therefore run in different environments without modification.

***

## 6. Create Docker Compose

At the root of the project, create:

```
docker-compose.yml
```

Add:

```yaml
services:

  product-service:
    build: ./product_service
    ports:
      - "8000:8000"

  order-service:
    build: ./order_service
    ports:
      - "8001:8000"
    environment:
      PRODUCT_SERVICE_URL: http://product-service:8000
    depends_on:
      - product-service
```

Docker Compose now defines the complete application.

The port mappings are:

```
Host                     Containers

localhost:8000 ───────► Product Service :8000

localhost:8001 ───────► Order Service   :8000
```

Internally, Order Service communicates with Product Service using:

```
http://product-service:8000
```

The service name `product-service` acts as the network hostname.

***

## 7. Run the Complete System

From the project root, run:

```bash
docker compose up --build
```

Docker Compose will:

1. Build the Product Service image.
2. Build the Order Service image.
3. Create the containers.
4. Create a network for the services.
5. Start the services.

Check the running containers:

```bash
docker compose ps
```

The system is now:

```
Client
   │
   │ localhost:8001
   ▼
Order Container
   │
   │ product-service:8000
   ▼
Product Container
```

***

## 8. Test Service Communication

First, test Product Service:

```
http://localhost:8000/products
```

Then open the Order Service API and create an order:

```
POST http://localhost:8001/orders
```

Example request:

```json
{
    "customer_name": "Nguyen Van An",
    "product_id": 1,
    "quantity": 2
}
```

The communication flow is:

```
Client
   │
   │ POST /orders
   ▼
Order Service Container
   │
   │ GET http://product-service:8000/products/1
   ▼
Product Service Container
   │
   │ Product information
   ▼
Order Service Container
   │
   │ Create order
   ▼
Client
```

The two microservices are now communicating through the Docker network.

***

## 9. Manage the Deployment

Several Docker Compose commands are useful during development.

View running services:

```bash
docker compose ps
```

View logs:

```bash
docker compose logs
```

Follow logs continuously:

```bash
docker compose logs -f
```

View logs for one service:

```bash
docker compose logs order-service
```

Stop and remove the application:

```bash
docker compose down
```

After changing source code, rebuild and start:

```bash
docker compose up --build
```

Run in the background:

```bash
docker compose up -d
```

Then stop the application with:

```bash
docker compose down
```

***

## 10. Volumes

Containers are designed to be replaceable.

Data stored only inside a container can disappear when the container is removed.

Docker **volumes** allow data to persist outside the lifecycle of an individual container.

Conceptually:

```
Container
    │
    ▼
Docker Volume
    │
    ▼
Persistent Data
```

For example:

```yaml
volumes:
  - ./data:/app/data
```

Volumes are commonly used for:

* Database data
* Uploaded files
* Persistent application data

Our current Product and Order Services use in-memory data, so volumes are not required for this tutorial.

***

## 11. From Development to Deployment

Docker packages an application and its runtime dependencies into an image:

```
Source Code
     ↓
Dockerfile
     ↓
Docker Image
     ↓
Container
```

Docker Compose allows multiple containers to be deployed together:

```
Product Image ──► Product Container
                         ▲
                         │
                    REST │
                         │
Order Image ─────► Order Container
```

In larger production environments, the same general container images may be stored in a **container registry** and deployed to cloud or container orchestration platforms.

```
Source Code
     ↓
Docker Image
     ↓
Container Registry
     ↓
Deployment Platform
```

Cloud deployment and container orchestration are outside the scope of this unit.

***

## 12. Practice Tasks

### Task 1 — Change Port Mapping

Change Order Service from:

```yaml
ports:
  - "8001:8000"
```

to:

```yaml
ports:
  - "8080:8000"
```

Restart the application and determine the new URL used by the client.

***

### Task 2 — Add an Environment Variable

Add:

```
APP_ENV=development
```

to Order Service through `docker-compose.yml`.

Read it in Python using:

```python
os.getenv("APP_ENV")
```

Return the current environment from a new endpoint:

```
GET /environment
```

***

### Task 3 — Observe Container Failure

Start the complete application.

Stop Product Service:

```bash
docker compose stop product-service
```

Then try to create an order.

Observe how Order Service behaves when its dependent service is unavailable.

Restart Product Service:

```bash
docker compose start product-service
```

Test the request again.

***

### Task 4 — Rebuild a Service

Modify one product in Product Service.

Then run:

```bash
docker compose up --build
```

Verify that the updated application is deployed.

***

## 13. Key Takeaways

* A Docker image packages an application and its runtime dependencies.
* Each microservice can run independently inside its own container.
* Containers communicate using Docker networking.
* `localhost` inside a container refers to that container itself.
* Docker Compose service names can be used as network hostnames.
* Environment variables separate deployment configuration from application code.
* Docker Compose simplifies building, starting, stopping, and managing multiple services.
* Volumes provide persistent storage when container data must survive container replacement.
