Commit Hash: c92e624
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, pull the latest commit from jarvis-monorepo
and install Docker if you don’t have it already.
You will be submitting your work in the following classroom: https://classroom.github.com/a/AFeur2YN.
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.
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
- 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 inservice/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
-
Errata 9/12: the Dockerfile was included in the repo, you can keep it or modify it to match auth. Create a Dockerfile for the notes service. Use the auth Dockerfile as a starting point. Then, build an image for the notes service and run it. To test if this is working, try making an authenticated request to the notes service while both the auth and the notes service are running as Docker containers. Refer back to the previous lab if you forgot the specifics of how our authentication works.
-
When you’ve finished the above, commit your changes and push them to your GitHub classroom repository. You’ll need to create a remote to the repository, then push your changes to that remote.