1/28 Configurations & Docker

GitHub Classroom: https://classroom.github.com/a/bruhogj6

In this lab, we’ll be learning about dotenv, a package that implements the configuration setup we discussed in lecture, as well as creating Docker images for our backend services.

First, before you get started, install Docker if you don’t have it already.

Configurations in Practice

Pull the latest commit and take a look at services/auth/app/config.py and utilities/shared/config.py. You’ll see a new settings class that inherits from a shared BaseServiceSettings class in the shared utilities folder. The base class inherits from BaseSettings in the pydantic_settings package. Specifically, our settings will first look for a settings file called .env.local to load from, then for any environment variables set in the shell environment. This means that the .env.local file is overridden by any variables set in the shell environment. You can see the configuration for this in BaseServiceSettings as the model_config attribute. What happens here is that the settings class will look for any variables that match the name of attributes we have defined in our settings class.

model_config = SettingsConfigDict(
    # look for a .env.local file to load settings from
    env_file=".env.local",
    env_file_encoding="utf-8",
    case_sensitive=False,
    # ignore any settings that we don't have defined in our class
    extra="ignore"
)

Try running the auth service. From services/auth/, run uv run fastapi dev main.py. You’ll get an error that should look similar to the output below:

ValidationError: 3 validation errors for AuthServiceSettings
frontend_url
  Field required [type=missing, input_value={}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.11/v/missing
...

This behavior of failing when we’re missing important configurations is actually ideal! You wouldn’t want a missing config to surface as an application level error, instead, you would want it to tell you immediately. The issue here is that we don’t actually have a settings file created. Create a new file services/auth/.env.local and put the following in:

DATABASE_URL=sqlite:///../../jarvis.db
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

Try running the auth service again, this time it should spin up without errors!

Docker in Practice

Let’s now create a Dockerfile, or image, for the auth service. Think about what we would have to do if we started on a brand new linux machine. We would first need to install uv, copy our code over, then sync dependencies. Let’s create a file services/auth/Dockerfile and put the following inside:

FROM python:3.10-slim

# Set work directory
WORKDIR /service/app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Copy all files into container
COPY services/auth/. .

# Copy the utilities folder into a similar structure on the container
# Note the /, this is an absolute path
COPY utilities /utilities

# Install dependencies
RUN uv sync --no-cache --link-mode=copy

# Set the command to run the service
CMD ["uv", "run", "fastapi", "dev", "main.py"]

When we build an image, two things are required: a Dockerfile and a path. This path is used as the root for things like COPY. In the Dockerfile for auth, we wrote things such that we assume we’re running the docker build from backend/. Build the image for our auth service with the following command:

# from backend/
docker build -f services/auth/Dockerfile . -t jarvis-auth

The -f flag here points to the Dockerfile to point, the . is the path, and -t refers to a “tag” for the image, which is essentially just a name for it. Now, let’s try to run our image as a container!

docker run jarvis-auth

You should see the logs for a successful startup. Try visiting the url it says the service is located at. It says the site can’t be reached, why is this the case? When you’re running a web server inside a Docker container locally, two key concepts often trip up beginners: port exposure and host binding.

Port Exposure

By default, Docker containers run in their own isolated network namespace. Think of it like each container living in its own private apartment building - they can’t communicate with the outside world (your host machine) unless you explicitly create doorways.

When you expose a port using the flag -p 8000:8000, you’re essentially telling Docker: “Take traffic coming to port 8000 on my host machine and forward it to port 8000 inside the container.” Without this mapping, requests to localhost:8000 on your machine have no way to reach the web server running inside the container.

Host Binding

Here’s where it gets tricky. Inside the container, your web server might be configured to listen on 127.0.0.1:8000 (localhost). But 127.0.0.1 inside the container refers to the container’s own loopback interface - not your host machine’s localhost.

When you set your server to listen on 0.0.0.0:8000 instead, you’re telling it to accept connections from any network interface, including the Docker bridge network that connects your container to the host (your laptop).

The bottom line: Port exposure gets traffic to your container, but your application needs to listen on 0.0.0.0 to actually receive that forwarded traffic. Miss either piece, and your locally running container won’t be accessible from your browser. Let’s modify our docker command to do this properly, in services/auth/Dockerfile, change the CMD to:

CMD ["uv", "run", "fastapi", "dev", "main.py", "--port", "8000", "--host", "0.0.0.0"]

Then we can expose the port when we run the container like so:

docker run -p 8000:8000 jarvis-auth

Alternatively, we don’t need to modify the Dockerfile since docker run can take in the command as an argument:

docker run -p 8000:8000 jarvis-auth uv run fastapi dev main.py --port 8000 --host 0.0.0.0

Try visiting the website now, you should be now properly see the correct responses/pages!

Tasks

[Errata 1/28]: backend/services/notes/main.py is missing an import: from app.config import settings

  1. Add an .env.local for the notes service. You’ll need to determine what fields to populate based on what you see in the settings class in service/notes/app/config.py. You should be able to run the notes service locally without errors afterwards.
# from services/notes/
uv run fastapi dev main.py --port 8001
  1. Create a Dockerfile for the notes service. Verify that it builds and runs properly:
docker build -f services/notes/Dockerfile . -t jarvis-notes
docker run -p 8001:8001 jarvis-notes

For now, don’t worry if you can’t get a request to notes to work, just make sure there aren’t any errors on startup. You actually need to create a Docker network in order for these two separate docker containers to talk to each other, which is a bit unwieldy for our use case. We’ll be addressing this problem next class!

  1. Commit and push your changes to your GitHub Classroom repository.

How do we easily manage multiple environments?

2026-01-28