Docker Compose

9/11 Lab Errata

  • When testing the authenticated /notes endpoint, you actually probably got the error {"detail":"Service unavailable"}
  • This is because in order for separate docker containers to talk to each other, they need to be on the same network
    • You could have set the IP address assigned to the auth container by docker as AUTH_SERVICE_URL on the notes service, but this is unwieldy and manual
  • We will solve this problem today!

Agenda

  1. Docker Compose Introduction
  2. YAML Overview
  3. Docker Compose Specification
  4. Lab

Recap

  • So far Jarvis is a mono-repo where each service is a separate uv project
    • This setup is common for large companies where each service is owned by a team
  • We created configurations for our services using environment variables or a .env file
    • This paradigm of loading a file then environment variables is called dotenv
  • We created Dockerfiles for each service to containerize them
  • Goal for today: finish creating a robust local development environment that mirrors the components we need in production

What could be better about the local setup?

Recap

We have a problem, running Jarvis locally is a pain!

  • We need to open a terminal for each service and run its container
    • If we add a service (chat), this is only going to get worse
  • We need to make sure we use the right commands for running each service, i.e. picking the right port
  • If we're actively developing on a service, we need to make code changes, build the Docker image, then run it again

How could we solve this ourselves?

How could we solve this ourselves?

We don't have to!

Enter: Docker Compose

Docker Compose is a tool that allows us to define and run multi-container applications.

  • An application is defined as a collection of containers using a YAML file compose.yml
  • Run all containers using a single command docker-compose up
  • Provides configurations for almost anything you need within a container
  • Mainly used locally, typically not recommended for production

docker compose

https://builder.aws.com/content/2qi9qQstGnCWguDzgLg1NgP8lBF/file-structure-of-docker-composeyml-file

YAML: Overview

YAML stands for "YAML Ain't Markup Language"

  • Meant to be a human readable serialization format
  • Used very commonly for various DevOps tools
    • Docker, Kubernetes, Terraform, CI/CD, etc
  • File extension is either .yaml or .yml
  1. # Key-value pairs (scalars)
  2. name: "John Doe"
  3. age: 30
  4. is_active: true
  5. salary: 75000.50
  6. department: null
  7. # Strings (quotes optional for simple strings)
  8. simple_string: Hello World
  9. quoted_string: "Special chars: @#$%"
  10. multiline_string: |
  11. This is a multiline string
  12. that preserves line breaks
  13. and formatting.
  14. folded_string: >
  15. This is a folded string that
  16. will be converted to a single
  17. line with spaces replacing newlines.
  1. # NOTE: indentation defines scope in YAML
  2. # Lists/Arrays (two styles)
  3. fruits:
  4. - apple
  5. - banana
  6. - orange
  7. colors: [red, green, blue]
  8. random_things: [123, "123", true]
  9. # Nested objects/maps
  10. person:
  11. name: Alice Smith
  12. contact:
  13. email: alice@example.com
  14. phone: "555-1234"
  15. skills:
  16. - Python
  17. - JavaScript
  18. - YAML
  1. # Lists can contain objects
  2. employees:
  3. - name: Bob Johnson
  4. position: Developer
  5. years_experience: 5
  6. - name: Carol Wilson
  7. position: Designer
  8. years_experience: 3

Docker Compose Specification

  • Docker Compose introduces a few new concepts: services, networks, and volumes
  • Each of these are defined as top level objects in the compose.yaml file
  • Services are effectively a container running a program, e.g. a FastAPI application, Next.js frontend, Postgres database, etc
  • Networks allow services to communicate with each other
  • Volumes store data outside containers so it survives container restarts and updates

Services: Defining Your Containers

Services are the containers that make up your application. Each service runs one part of your system.

  1. services:
  2. web:
  3. image: nginx:alpine
  4. ports:
  5. - "80:80"
  6. database:
  7. image: postgres:15
  8. environment:
  9. POSTGRES_DB: myapp
  • One service = one container
  • Define image, ports, environment, and more

More Service Syntax

  1. services:
  2. api:
  3. build: ./backend # Build from a Dockerfile
  4. ports:
  5. - "3000:3000"
  6. env_file: # Inject env variables from local file
  7. - ./api/.env
  8. depends_on: # Start only after database service is ready
  9. - database
  10. restart: unless-stopped # Restart policy
  11. database:
  12. image: postgres:15
  13. volumes:
  14. - db_data:/var/lib/postgresql/data # Use volume for persistence

Networks: Connecting Services

  • Default network: Unless specified, all services are included in the default network. This allows them to connect to each other using service names as hostnames, e.g. http://api connects to the api service.
  • If you have a need to split network connections from each service, you can explicitly add services to networks

Networks Syntax

  1. services:
  2. api:
  3. ...
  4. networks:
  5. - web-tier # include api service in web-tier and db-tier networks
  6. - db-tier
  7. networks:
  8. web-tier:
  9. driver: bridge # The network driver to use, typically bridge
  10. db-tier:
  11. driver: bridge
  12. internal: true # Prevent external access, false by default

Volumes: Persisting Data

  • We can create volumes to store persistent data
    • Think of these as folders
  • We mount these volumes in our containers to use them
  • We can also mount local files to our Docker containers
    • Useful for local development, e.g. mount the app/ directory so that FastAPI can restart automatically within the container when it sees files have changed!

Volumes Syntax

  1. services:
  2. database:
  3. image: postgres:15
  4. volumes:
  5. # Mount db_data volume at /var/lib/postgresql/data
  6. - db_data:/var/lib/postgresql/data
  7. api:
  8. volumes:
  9. # Mount local folder ./api/app as /app on container
  10. - ./api/app:/app
  11. volumes:
  12. db_data: # Managed by Docker
  13. static_files: # Shared between services

Docker Compose CLI

Common commands:

  • docker compose up - start services defined in compose.yaml
    • adding the -d flag runs in detached mode, i.e. the background
  • docker compose down - stop and remove services
  • docker compose ps - get status of containers
  • docker compose logs - get logs of all containers
    • Only really needed if you started in detached mode

Docker Compose Specification

There's a lot more you can do with Docker Compose, we've only just gone over the basics we need. If you're curious, check out the specification on the docker documentation.

https://docs.docker.com/reference/compose-file/

Lab: Docker Compose