9/18 Docker Compose

Commit Hash: 0c23cdf

In this lab, we’ll be working with Docker Compose. Let’s create a Docker compose for our backend. First, let’s review how we currently run the system locally. Pull the most recent changes to the repository, you should essentially see something similar to what you did after the lab last week. To run our service locally, we have to currently run 3 processes in 3 different terminals (or manage backgrounding them): the auth service, notes service, and frontend. This has become quite a mess! It’s only going to get worse when we add more services (in our case, the chat service is coming in a few weeks).

Luckily, this is exactly what Docker Compose is meant to help with. Get started by installing Docker Compose (if you installed Docker Desktop you should already have it). You can check if you have it installed properly by simply running

docker compose

which should output the help text.

Docker Compose

Let’s start simple for our Docker Compose by just setting up the auth service first. Create a new file compose.yaml at the very root of the repository and add the following to it:

services:
  auth-service:
    build:
      context: ./backend
      dockerfile: ./services/auth/Dockerfile
    ports:
      - "8000:8000"
    expose:
      - "8000"
    command: ["uv", "run", "fastapi", "dev", "main.py", "--host", "0.0.0.0", "--port", "8000", "--reload"]
    restart: unless-stopped

This defines the auth-service service that builds from the specified Dockerfile. If you recall, building a Dockerfile also requires a path, which in this case is the context. We need to open our host machine’s ports to the container, which we do with the ports: value (this is the same as the -p flag when we ran docker run). The expose: value is declared for communication with any internal services. Let’s give this a run:

docker compose up

You should be able to visit localhost:8000/docs now. Try creating a user and signing in, it should work. Great! Now let’s see how things hold up if we stop the container, press ctrl+c to stop the docker compose command, then run it again. Try signing in again, you’ll likely be able to succeed. Let’s now try removing the container and restarting it. Run

docker compose down

to stop and remove all containers. Then, run docker compose up and try signing in with the user you created. You’ll get a 401 error, specifically, the user no longer exists in our database! Note that we currently have a very rudimentary database setup; it’s just a file that sits on our computer, namely backend/jarvis.db. When we started the auth service container, that file was created. You can actually inspect the container to see this. Run docker compose ps and copy the name of the container. Then run

docker container exec -it <container_name> /bin/bash

which should open a shell to your currently running auth service container. You can poke around here, but particularly note that there’s a file /jarvis.db on the container! This isn’t ideal, our system assumes on database which all services have access to, so this won’t work. Instead, in production, the setup will likely have an external database running that our services will connect to. We can mimic this using Docker Compose: modify your compose.yaml to be:

services:
  auth-service:
    build:
      context: ./backend
      dockerfile: ./services/auth/Dockerfile
    ports:
      - "8000:8000"
    expose:
      - "8000"
    command: ["uv", "run", "fastapi", "dev", "main.py", "--host", "0.0.0.0", "--port", "8000", "--reload"]
    restart: unless-stopped
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: jarvis
      POSTGRES_USER: jarvis
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U jarvis -d jarvis"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data  # Persist data with a volume

volumes:
  db_data:

What we’ve done here is add a new service called db that is running the Postgres image, which is a popular SQL database choice in industry. We’ve set some environment variables that are managed by Postgres itself to configure it and exposed it to port 5432 (the standard postgres port). The healthcheck value is used to determine if the service is healthy or not by running a command, in this case pg_isready -U jarvis -d jarvis. This is important because our backend services require the database to be healthy before they can be spun up. This is codified in our compose as the depends_on: value. Finally, we created a volume named db_data. This look a bit funny, but it’s essentially just using all the default values. This volume is attached to the db service to ensure that our data persists even when we remove containers.

We’re not done yet! We still need to hook up the database to our service. Remember that the database url is a configuration. If you take a look at our Dockerfile for auth, you’ll see that we’re copying everything, which includes our .env.local (which is also why the service is starting without errors). We need a different set of configs for the docker compose environment. Once again, docker compose has a nice way to deal with this. Create a new file backend/services/auth/docker.env and put the following inside:

DATABASE_URL=postgresql://jarvis:password@db:5432/jarvis
FRONTEND_URL=http://localhost:3000
PORT=8000
JWT_SECRET_KEY=a_super_secret_key_for_jwt
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=True
COOKIE_SECURE=False

As an aside, the DATABASE_URL can be read use the postgresql:// protocol, username and password jarvis:password, the actual URL is at just db port 5432, and /jarvis is the path to our database named jarvis. If you recall from lecture, there’s a default network all services are included in, which allows them to connect to each other by name.

To actually inject these are environment variables, we need to add the following value to auth-service in our compose.yaml:

env_file:
  - ./backend/services/auth/docker.env

Now, finally, we can do the same test to make sure that our database persists. Take some time to try to understand what we currently have defined so far in compose.yaml, as you’ll need to get familiar with both reading YAML and Docker compose syntax for the rest of this lab. You may find referencing the Docker Compose documentation useful.

Tasks

Overall goal: get docker compose working for all backend services.

  • Auth service working and connected to database (what we just did)
  • Notes service working and connected to database. This includes adding a docker.env for notes. Think about what the AUTH_SERVICE_URL needs to be.

At this point, you should have a fully functioning backend. You can test by doing a request to /notes, which is authenticated. Next, let’s add a QOL change:

  • Mount your local files to the auth and notes containers. Specifically, mount app/ and main.py where they need to be in the container. This will allow FastAPI to automatically restart the service when we change any files locally, which is ideal for development. To test this, you can modify the read_root endpoint in main.py to return something else (this endpoint is not used in anyway). You should see logs from docker compose stating that the service is restarting, or you can manually hit the root endpoint to verify your change.

When you are finished, commit and push the changes to your repository. Then, open a pull request titled with your Pennkey.


How do we make local development easy?

2025-09-18