Dockerize a FastAPI App
Containerize a Python API with Docker, from a basic Dockerfile to production.
Introduction
30 Second Summary
You write code that works perfectly on your computer. Then you share it with someone else and it breaks. Different Python version, missing packages, wrong settings. Docker solves this by packaging your code and everything it needs into a single container that runs the same way everywhere.
In this project, you will build a Python web API from scratch and containerize it with Docker. You will progress from a beginner Dockerfile to a production-grade multi-stage setup that professional teams use in the real world.
What You'll Build
A simple FastAPI web API that returns JSON responses, fully containerized with Docker using separate development and production Dockerfiles.
By the end of this project, you'll have:
- A working FastAPI application running inside a Docker container, accessible at http://localhost:8000.
- Both a beginner Dockerfile and a production multi-stage Dockerfile you understand deeply.
- Hands-on experience with development and production container workflows, including image layering and caching strategies.
- Secret Mission: Orchestrate dev and prod environments with a single Docker Compose command.
Are there any prerequisites?
You need basic Python knowledge (variables, functions). No Docker experience is required. This project starts from zero and builds up.
Install Docker Desktop
Before you can containerize anything, you need Docker installed on your machine. Docker packages your application and its dependencies into a container that runs the same way everywhere.
In this step, you'll install Docker Desktop, verify it can build and run containers, and create the project folder for your FastAPI app.
In this step, get ready to:
- Download and install Docker Desktop.
- Verify Docker is running.
- Create the project folder and open it in your editor.
Download and install Docker Desktop
Docker Desktop is the application that provides the Docker engine, CLI tools, and a graphical dashboard on your machine.
- Download Docker Desktop for Windows from the official install page.
- Run Docker Desktop Installer.exe once the download completes.
- Accept the defaults in the installer (the WSL 2 backend is recommended).
- Restart your computer if the installer prompts you to.
- After restarting, Docker Desktop should launch automatically. If it does not, press the Windows key to open your search bar, type Docker Desktop, and press Enter.
What is Docker Desktop?
Docker Desktop bundles the Docker engine (which actually runs containers), the docker CLI, and a visual dashboard into one installer. It also handles the Linux virtual machine that containers need to run on Windows.
Installer asking about WSL 2?
If the installer says WSL 2 is not enabled, follow the prompt to install it. WSL 2 is a lightweight Linux layer that Docker uses under the hood on Windows. You may need to restart your computer a second time after enabling it.
help me enable WSL 2 for Docker Desktop on Windows
Verify Docker is running
With Docker Desktop open, you can confirm the CLI works from your terminal.
- Press the Windows key (Windows) or Cmd+Space (macOS) to open your search bar.
- Type Terminal and press Enter to open it.
- Check Docker is installed by running this command:
docker --version
✔️ I see a version number
Docker is installed and ready. You should see output similar to Docker version 27.x.x. Continue to the next command below.
ⓧ Command not found
This means the Docker CLI is not available in your terminal. Try these fixes:
- Make sure Docker Desktop is running (check your system tray for the Docker whale icon).
- Close your terminal completely and open a new one. The PATH update from the installer only takes effect in new terminal sessions.
- If neither works, restart your computer and try again.
help me fix docker command not found on Windows after installing Docker Desktop
- Verify Docker can pull images and run containers by running this command:
docker run hello-world
You should see a welcome message from Docker confirming that your installation is working correctly. Docker pulls the hello-world image from Docker Hub, creates a container from it, and runs it.
Seeing a connection error?
If you see an error about the Docker daemon not running, make sure Docker Desktop is fully started. Look for the whale icon in your system tray. It may take a minute after launch before the engine is ready.
If you see a network-related error, check that your internet connection is active. Docker needs to download the hello-world image the first time you run this command.
help me fix docker run hello-world failing on Windows
Create your project folder
Now you need a dedicated folder for all the project files. You will create it on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir fastapi-docker
cd fastapi-docker
- Confirm the folder exists by running:
ls
You should see an empty directory (no output from ls) because you have not created any files yet.
- Open the folder in VS Code (or Cursor) by running:
code .
What does this command do?
The code . command opens VS Code with the current folder as the workspace. If you use Cursor instead, run cursor . instead.
code command not recognized?
If your terminal does not recognize code, you can open VS Code manually. Press the Windows key (Windows) or Cmd+Space (macOS), type VS Code, and press Enter. Then click File in the top menu bar, click Open Folder, and select the fastapi-docker folder on your Desktop.
If you do not have VS Code installed yet, download it from code.visualstudio.com.
help me open my project folder in VS Code
What will this folder contain?
By the end of this project, your fastapi-docker folder will contain these files:
- main.py - your FastAPI application.
- requirements.txt - pinned Python dependencies.
- Dockerfile - production multi-stage build instructions.
- Dockerfile.dev - development build with hot reload.
- .dockerignore - files to exclude from the Docker image.
Your environment is ready. Next up, you'll build the FastAPI application that will run inside your Docker container.
Build the FastAPI Application
With Docker installed and your project folder open, it's time to write the application that will live inside your container.
In this step, you'll create a small FastAPI web API that returns JSON responses. Two endpoints are all you need to prove the container works and demonstrate a health check pattern that production systems rely on.
In this step, get ready to:
- Create a FastAPI application with two endpoints.
- Pin your project dependencies in a requirements file.
Create the FastAPI application
FastAPI is a Python web framework that makes it easy to build APIs. You define endpoints as Python functions, and FastAPI handles routing incoming requests to the right function.
- In VS Code, hover over the fastapi-docker folder in the Explorer sidebar and click the New File icon.
- Name the file main.py and press Enter.
- Paste the following code into main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from Docker!", "environment": "production"}
What does this code do?
from fastapi import FastAPI brings in the FastAPI class from the library.
app = FastAPI() creates your application instance. Every request to your API flows through this object.
@app.get("/") tells FastAPI to call the function below whenever someone visits the root URL. The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
You won't see a change yet. The next chunk adds the health endpoint to complete the file.
- Add the health endpoint below the existing code in main.py:
@app.get("/health")
def health_check():
return {"status": "healthy"}
Why a health endpoint?
Production systems use health endpoints to check if a service is alive. Load balancers, container orchestrators, and monitoring tools all ping endpoints like /health to decide whether to route traffic to your app.
- Save main.py by pressing Ctrl+S.
Seeing a red underline under the import?
VS Code may show a warning because fastapi is not installed on your local machine yet. That's expected. You'll install it inside the Docker container later, not on your local system.
help me understand why VS Code shows import errors for packages I haven't installed locally
✔️ Awesome, I've got everything!
Great. Make sure you've saved main.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.py file:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from Docker!", "environment": "production"}
@app.get("/health")
def health_check():
return {"status": "healthy"}
Create the requirements file
Every Python project lists its dependencies in a requirements.txt file. When Docker builds your container, it reads this file to install exactly the right packages.
- Create a new file in the fastapi-docker folder called requirements.txt.
- Paste the following into requirements.txt:
fastapi==0.136.3
uvicorn==0.48.0
Why pin exact versions?
The == syntax pins each package to a specific version. Without pinning, pip install grabs whatever the latest version is at build time.
Pinning versions means your container builds are reproducible. Every time Docker builds this image, it installs the same versions regardless of what's been released since.
What is uvicorn?
Uvicorn is an ASGI web server that runs your FastAPI application. FastAPI defines your endpoints, and uvicorn handles the actual network traffic. You need both.
- Save requirements.txt.
- Confirm both files exist by checking the Explorer sidebar in VS Code. You should see main.py and requirements.txt listed inside fastapi-docker.
Don't see both files?
Make sure you created both files inside the fastapi-docker folder, not in a subfolder or your home directory. If a file ended up in the wrong place, right-click it in the Explorer sidebar and select Delete, then create it again inside the correct folder.
help me find where my files were created in VS Code
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete requirements.txt file:
fastapi==0.136.3
uvicorn==0.48.0
Test locally (optional)
Already have Python installed?
If you have Python installed on your machine, you can verify the app works before containerizing it. Run pip install -r requirements.txt then uvicorn main:app --reload from inside the fastapi-docker folder.
Visit http://127.0.0.1:8000 to see your JSON response and http://127.0.0.1:8000/docs to see the auto-generated Swagger UI.
This is completely optional. The Docker container will handle installing and running everything in the next step.
Your FastAPI application is ready. Next up, you'll write a Dockerfile that teaches Docker how to package this app into a container image.
Write the Beginner Dockerfile
Your FastAPI application is written and ready to go. Now you need to give Docker a set of instructions for packaging it into a container image.
Those instructions live in a file called a Dockerfile. In this step, you will write a Dockerfile that builds a lean, production-ready image using a multi-stage approach. You will also create a .dockerignore file to keep unnecessary files out of the image.
In this step, get ready to:
- Create a Dockerfile with properly ordered layers for efficient builds.
- Create a .dockerignore to exclude unnecessary files from the image.
- Understand Docker's layer caching strategy and why instruction order matters.
Create the Dockerfile
A Dockerfile is a text file with no file extension. Each line is an instruction that tells Docker how to assemble your image, layer by layer.
Your Dockerfile will use a multi-stage build. The first stage installs dependencies into a virtual environment. The second stage copies only what the app needs into a fresh, minimal image.
- In VS Code, create a new file called Dockerfile (no file extension) inside your fastapi-docker folder.
- Add the builder stage by pasting this code into the file:
# ============================================
# Stage 1: Builder - install dependencies
# ============================================
FROM python:3.11-slim AS builder
WORKDIR /app
# Create a virtual environment for clean dependency isolation
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Copy and install dependencies (cached layer if requirements unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
What does this code do?
This is Stage 1, called the "builder." Its only job is to install your Python dependencies cleanly.
- FROM python:3.11-slim AS builder starts from a lightweight Python image and names this stage builder so the next stage can reference it.
- WORKDIR /app sets the working directory inside the container. All following commands run from here.
- RUN python -m venv /app/venv creates a virtual environment so dependencies are isolated in one folder.
- COPY requirements.txt . copies only the requirements file first. This lets Docker cache the install layer separately from your code.
- RUN pip install --no-cache-dir -r requirements.txt installs your dependencies without storing pip's download cache, keeping the image smaller.
You won't see a change yet. The next chunk completes the Dockerfile with the production stage.
- Add the final production stage below the builder stage in the same file:
# ============================================
# Stage 2: Final - lean production image
# ============================================
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"
WORKDIR /app
# Copy only the virtual environment from the builder stage
COPY --from=builder /app/venv /app/venv
# Copy application code
COPY main.py .
# Document the port the app listens on
EXPOSE 8000
# Start the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What does this code do?
This is Stage 2, the final image that actually runs in production. It starts completely fresh from python:3.11-slim and copies only what it needs from the builder.
- ENV PYTHONDONTWRITEBYTECODE=1 prevents Python from writing .pyc files, which are useless in a container that gets discarded.
- ENV PYTHONUNBUFFERED=1 ensures logs appear immediately instead of being buffered. This is critical for container logging.
- COPY --from=builder /app/venv /app/venv pulls only the virtual environment from Stage 1. Build tools, pip cache, and temporary files are left behind.
- EXPOSE 8000 documents which port the app listens on. It does not actually publish the port.
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] starts your FastAPI server using exec form, which is the recommended format for production containers.
- Save Dockerfile.
- Confirm the file exists by running:
ls
You should see Dockerfile listed alongside main.py and requirements.txt.
Don't see the Dockerfile?
- Make sure the file has no extension. If VS Code added .txt automatically, rename it to just Dockerfile.
- Confirm you saved it inside the fastapi-docker folder, not a subfolder.
- help me troubleshoot my Dockerfile not appearing in my project folder
Why multi-stage instead of single-stage?
A single-stage Dockerfile would leave pip's build artifacts and download cache in your final image. Multi-stage builds separate the "workshop" (where you install things) from the "suitcase" (what you ship).
The result is a smaller, more secure production image with no leftover build tools. You get the right habits from day one.
✔️ Awesome, I've got everything!
Great. Double check you have saved your Dockerfile before moving on.
ⓧ I'd like to double check the full code
# ============================================
# Stage 1: Builder - install dependencies
# ============================================
FROM python:3.11-slim AS builder
WORKDIR /app
# Create a virtual environment for clean dependency isolation
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Copy and install dependencies (cached layer if requirements unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ============================================
# Stage 2: Final - lean production image
# ============================================
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"
WORKDIR /app
# Copy only the virtual environment from the builder stage
COPY --from=builder /app/venv /app/venv
# Copy application code
COPY main.py .
# Document the port the app listens on
EXPOSE 8000
# Start the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Create the .dockerignore file
When Docker builds an image, it sends every file in your project folder to the Docker daemon as "build context." A .dockerignore file tells Docker which files to skip, keeping your image small and avoiding leaking secrets.
- Create a new file called .dockerignore in your fastapi-docker folder.
- Add these exclusion patterns:
__pycache__
*.pyc
.venv
.env
.git
.gitignore
Dockerfile
Dockerfile.dev
.dockerignore
README.md
docker-compose.yml
What does this file do?
- __pycache__ and *.pyc exclude compiled Python bytecode files that get regenerated inside the container anyway.
- .env prevents secrets from being copied into the image.
- .git excludes your entire Git history, which can be large and is never needed at runtime.
- The Dockerfiles themselves and docker-compose.yml are build configuration, not application code.
- Save .dockerignore.
- Confirm both new files exist by running:
ls -a
You should see .dockerignore and Dockerfile listed alongside your existing files.
Don't see the .dockerignore file?
- Files starting with a dot are hidden by default. Use ls -a (not plain ls) to reveal them.
- If VS Code's Explorer sidebar hides the file, check **File** then **Preferences** then **Settings** and search for "files.exclude" to make sure dotfiles are visible.
- help me find my .dockerignore file in VS Code
✔️ Awesome, I've got everything!
Great. Double check you have saved .dockerignore before moving on.
ⓧ I'd like to double check the full code
__pycache__
*.pyc
.venv
.env
.git
.gitignore
Dockerfile
Dockerfile.dev
.dockerignore
README.md
docker-compose.yml
Understand Docker image layering
Each instruction in a Dockerfile creates a new "layer" that Docker caches. If a layer has not changed since the last build, Docker reuses the cached version instead of rebuilding it.
Why does instruction order matter?
Look at Stage 1 of your Dockerfile. You copied requirements.txt and ran pip install BEFORE copying your application code. This is deliberate.
Dependencies change rarely. Your code changes constantly. By putting the dependency install earlier in the file, Docker caches that layer and reuses it every time you rebuild after a code change. Without this ordering, every small code edit would trigger a full reinstall of all packages.
What is layer caching in practice?
Imagine you fix a typo in main.py and rebuild. Docker sees that requirements.txt has not changed, so it skips the pip install step entirely and reuses the cached layer. Your rebuild finishes in seconds instead of waiting for packages to download.
Your Dockerfile and .dockerignore are ready. Next up, you will build a Docker image from this Dockerfile and run your FastAPI app inside a container.
Build and Run the Container
Your Dockerfile and application code are written and ready to go. Now comes the exciting part: turning those files into a running container that serves your API.
In this step, you will build a Docker image from your Dockerfile, run it as a container, and learn the essential commands for managing containers.
In this step, get ready to:
- Build a Docker image from your Dockerfile.
- Run a container and verify your API is accessible.
- Manage containers with stop, detach, and inspect commands.
Build the Docker image
The docker build command reads your Dockerfile and executes each instruction to create an image. The image bundles your application code, dependencies, and runtime into a single portable package.
- Build your Docker image by running this command in Windows Terminal (make sure you're in the fastapi-docker folder):
docker build -t myapp .
What does this command do?
- -t myapp gives the image a human-readable tag name. Without it, you'd only have a long hash to reference your image.
- . tells Docker to use the current directory as the "build context." Docker sends everything in this folder (minus what's in .dockerignore) to the build engine.
Watch the terminal output as Docker executes each stage. You should see it pull the python:3.11-slim base image and install your dependencies. A successful build ends with a line like Successfully tagged myapp:latest.
Build failing?
- Make sure Docker Desktop is running. Check for the Docker whale icon in your system tray.
- Confirm your terminal is in the fastapi-docker folder (where the Dockerfile lives). Run cd to check your current directory.
- If you see a "failed to read dockerfile" error, make sure your file is named exactly Dockerfile with a capital D and no file extension.
Help me debug my Docker build error.
Run the container
A container is a running instance of your image. You can create multiple containers from the same image, each isolated from your host machine. You need to explicitly map ports so your browser can reach the application inside the container.
- Start a container from your image by running:
docker run -p 8000:8000 myapp
What does this command do?
- docker run creates a new container and starts it.
- -p 8000:8000 maps port 8000 on your Windows machine to port 8000 inside the container. Without this flag, the container's port would be unreachable from your browser.
- myapp is the image tag you assigned during the build.
- Open your browser and visit http://localhost:8000.
- You should see the JSON response: {"message": "Hello from Docker!", "environment": "production"}.
- Visit http://localhost:8000/docs to see FastAPI's auto-generated Swagger UI.
Not seeing a response in the browser?
- Check that your terminal shows uvicorn startup logs. Look for a line containing Uvicorn running on http://0.0.0.0:8000. If the container exited immediately, check main.py for syntax errors.
- Make sure no other process is using port 8000. If you get a "port already in use" error, stop whatever is running on that port or use a different mapping like -p 9000:8000 (then visit http://localhost:9000 instead).
Help me troubleshoot my Docker container not responding.
Manage the container
Now that your container is running, you need a few commands to control it. You will stop the container, restart it in the background, and inspect running containers.
- Stop the running container by pressing **Ctrl+C** in the terminal where it's running.
The container stops and your terminal returns to the command prompt. Next, you will run the container in detached mode so it runs in the background without tying up your terminal.
- Run your container in detached (background) mode by running:
docker run -d -p 8000:8000 myapp
What is detached mode?
The -d flag runs the container in the background. Docker prints the container ID and immediately returns control of your terminal. Your API is still accessible at http://localhost:8000 even though you don't see any logs.
- List your running containers by running:
docker ps
You should see a table with your container's ID, image name (myapp), port mapping, and status.
- Copy the container ID from the first column of the docker ps output, then stop the detached container by running:
docker stop [[CONTAINER_ID="your container ID from docker ps"]]
- Check your Docker image sizes by running:
docker images
You should see myapp listed with its size. This is your multi-stage production image.
Container won't stop?
- Make sure you copied the correct container ID from the docker ps output. You only need the first few characters of the ID.
- If docker ps shows nothing, your container already stopped. Run docker ps -a to see all containers including stopped ones.
Help me manage my Docker containers.
Your API is now running inside a Docker container. Next up, you will replace this Dockerfile with a production-optimized multi-stage build that creates an even smaller, more secure image.
Create a Production Multi-Stage Dockerfile
Your container runs perfectly, but the image carries extra baggage. Build tools, pip caches, and intermediate files from the install process all sit inside the image even though your app never uses them at runtime.
In this step, you will upgrade your Dockerfile to a multi-stage build. This is the pattern professional teams use to produce small, secure production images.
In this step, get ready to:
- Understand how multi-stage builds separate build-time tools from runtime.
- Rewrite your Dockerfile with a builder stage and a final stage.
- Build a production image and compare its size to the original.
Understand multi-stage builds
A multi-stage build uses multiple FROM instructions in one Dockerfile. Think of it like packing for a trip: you use a big workshop (the builder stage) to prepare everything, then pack only what you need into a small suitcase (the final stage).
The builder stage installs dependencies into a virtual environment. The final stage copies only that virtual environment and your code into a fresh, clean image.
Why does this matter for Python?
Even though Python does not compile to a binary, multi-stage builds still help. Pip's download cache, temporary wheel files, and any build-time-only dependencies (like compilers for C extensions) stay in the builder and never appear in your final image.
The result is a smaller image with fewer files that could contain vulnerabilities.
Write the multi-stage Dockerfile
You will replace the entire content of your Dockerfile with a two-stage version. Stage 1 installs all dependencies into an isolated virtual environment. Stage 2 starts fresh and copies only what the app needs to run.
- Open Dockerfile in VS Code.
- Select all the existing content and delete it.
- Add Stage 1 (the builder) by pasting the following code:
# ============================================
# Stage 1: Builder - install dependencies
# ============================================
FROM python:3.11-slim AS builder
WORKDIR /app
# Create a virtual environment for clean dependency isolation
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Copy and install dependencies (cached layer if requirements unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
What does Stage 1 do?
- FROM python:3.11-slim AS builder names this stage "builder" so Stage 2 can reference it later.
- RUN python -m venv /app/venv creates a virtual environment that holds all installed packages in one portable directory.
- pip install --no-cache-dir installs packages without saving pip's download cache, keeping this layer lean.
- Below Stage 1, add Stage 2 (the final production image) by pasting this code:
# ============================================
# Stage 2: Final - lean production image
# ============================================
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"
WORKDIR /app
# Copy only the virtual environment from the builder stage
COPY --from=builder /app/venv /app/venv
# Copy application code
COPY main.py .
# Document the port the app listens on
EXPOSE 8000
# Start the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What does Stage 2 do?
- FROM python:3.11-slim starts a brand-new image. Nothing from Stage 1 carries over automatically.
- COPY --from=builder /app/venv /app/venv is the key line. It copies only the virtual environment directory from the builder stage into this clean image.
- COPY main.py . brings in your application code. Build tools, pip cache, and temporary files never touch this stage.
- Save Dockerfile.
Seeing a syntax error in the Dockerfile?
- Make sure there are no leftover lines from the old Dockerfile. The file should contain only the two stages above.
- Check that the file has no extension. It should be named exactly Dockerfile, not Dockerfile.txt.
Why is my multi-stage Dockerfile not working?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
# ============================================
# Stage 1: Builder - install dependencies
# ============================================
FROM python:3.11-slim AS builder
WORKDIR /app
# Create a virtual environment for clean dependency isolation
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Copy and install dependencies (cached layer if requirements unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ============================================
# Stage 2: Final - lean production image
# ============================================
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"
WORKDIR /app
# Copy only the virtual environment from the builder stage
COPY --from=builder /app/venv /app/venv
# Copy application code
COPY main.py .
# Document the port the app listens on
EXPOSE 8000
# Start the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Build and compare image sizes
Your original myapp image was built from the beginner Dockerfile. Now you will build a new image from the multi-stage version and compare the two.
- Build the production image by running this command:
docker build -t myapp-prod .
What does this command do?
- -t myapp-prod tags the image with the name "myapp-prod" so you can distinguish it from the earlier myapp image.
- Docker executes both stages in order. You will see it build the "builder" stage first, then start a fresh image for the "final" stage.
- Compare image sizes by running:
docker images
- Look at the SIZE column. You should see both myapp and myapp-prod listed. The multi-stage myapp-prod image should be smaller because it excludes pip's build artifacts and intermediate installation files.
Build failing with a COPY --from error?
- Make sure Stage 1 includes AS builder at the end of the first FROM line. Without that name, Stage 2 cannot reference it.
- Verify you are running the command from inside the fastapi-docker folder where the Dockerfile lives.
Help me fix my multi-stage Docker build
Your production image is lean and secure. Next up, you will create a development Dockerfile that prioritizes fast iteration with hot reload.
Create a Development Dockerfile
Your multi-stage production Dockerfile creates a lean, secure image. But every time you change a line of code, you'd need to rebuild the entire image to see the result.
In this step, you'll create a separate Dockerfile.dev designed for development speed. It uses a volume mount and hot reload so code changes appear instantly inside the container without any rebuild.
In this step, get ready to:
- Create a Dockerfile.dev with hot reload enabled.
- Build and run a development container with a volume mount.
- Compare development vs production workflows.
Create Dockerfile.dev
The production Dockerfile uses a multi-stage build and a virtual environment for clean isolation. During development, you don't need that overhead. You want the fastest possible feedback loop: change code, see the result.
- In VS Code, create a new file called Dockerfile.dev in your fastapi-docker folder.
- Add the following code to Dockerfile.dev:
# Development Dockerfile - optimized for fast iteration
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies directly (no venv needed in dev container)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code (will be overridden by volume mount)
COPY . .
# Document the port
EXPOSE 8000
# Start with --reload for hot reloading during development
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
What does this code do?
- FROM python:3.11-slim uses the same base image as production for consistency.
- No virtual environment is created here. The container itself IS the isolated environment, so installing packages directly into the system Python is fine.
- COPY . . copies all your application code. In practice, the volume mount will override this at runtime.
- The --reload flag tells uvicorn to watch for file changes and restart the server automatically.
- Save Dockerfile.dev.
- Confirm the file exists by running:
ls
You should see Dockerfile.dev listed alongside main.py, requirements.txt, Dockerfile, and .dockerignore.
Don't see Dockerfile.dev?
- Make sure you created the file in the fastapi-docker folder, not inside a subfolder.
- Check that the file name is exactly Dockerfile.dev with no extra extension like .txt.
- help me find my Dockerfile.dev file
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
# Development Dockerfile - optimized for fast iteration
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies directly (no venv needed in dev container)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code (will be overridden by volume mount)
COPY . .
# Document the port
EXPOSE 8000
# Start with --reload for hot reloading during development
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
Build and run the dev container
The -f flag lets you specify a non-default Dockerfile name. Without it, Docker always looks for a file literally named Dockerfile.
- Build your development image by running this command:
docker build -f Dockerfile.dev -t myapp-dev .
- Confirm the image was created by running:
docker images
You should see myapp-dev listed alongside your existing myapp and myapp-prod images.
Build failing?
- Make sure Docker Desktop is still running. Check for the Docker icon in your system tray.
- Confirm your terminal is in the fastapi-docker folder. Run cd ~/Desktop/fastapi-docker if needed.
- help me fix my Docker build error
Now start a development container with a volume mount. The -v .:/app flag mounts your local project folder into the container. File changes on your machine are instantly visible inside the container.
- Start the dev container by running this command:
docker run -p 8000:8000 -v .:/app myapp-dev
What does -v .:/app do?
The -v .:/app flag creates a volume mount. The . refers to your current directory on your machine. The :/app maps it to the /app folder inside the container.
Combined with --reload, this means you can edit main.py locally and uvicorn detects the change and restarts automatically. No rebuild needed.
- Visit http://localhost:8000 in your browser.
You should see the same JSON response: {"message": "Hello from Docker!", "environment": "production"}.
- Stop the container by pressing Ctrl+C in your terminal.
Not seeing the JSON response?
- Check that port 8000 is not already in use by another container. Run docker ps to see running containers, then stop any using port 8000 with docker stop <container_id>.
- If Ctrl+C doesn't stop the container, open a new terminal tab and run docker ps to get the container ID, then docker stop <container_id>.
- help me get my dev container running
Compare development vs production workflows
You now have two Docker workflows that serve the same FastAPI application. Each one is optimized for a different goal.
Why two separate Dockerfiles?
Development prioritizes fast feedback. Hot reload and a mounted code volume mean you never wait for a rebuild. Image size doesn't matter because it only runs on your machine.
Production prioritizes small size, security, and reproducibility. The multi-stage build strips out build tools and pip cache. No volume mount means the image is fully self-contained and runs the same everywhere.
Professional teams maintain both. You develop with Dockerfile.dev locally and deploy with the production Dockerfile.
Secret mission
Orchestrate Dev and Prod with Docker Compose Profiles
You've been running separate docker build and docker run commands for dev and prod. What if you could switch between environments with a single command? Docker Compose profiles let you define both services in one file and selectively start the one you need.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with Docker, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker images (myapp, myapp-dev, myapp-prod, and pulled base images).
- Docker containers created from those images.
- Project folder fastapi-docker on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your containers.
- Your Docker images remain cached locally for fast rebuilds.
- Your project folder stays on your Desktop, ready for edits.
- Docker Desktop continues running in your system tray and uses some background memory even when no containers are active.
Pause - I'll come back to this later
Shut down running containers to free up memory. Your images and project files stay in place so you can restart quickly later.
- Stop any Compose services by running this command in your fastapi-docker folder:
docker compose down
- If you started containers outside of Compose (with docker run), find their IDs by running:
docker ps
- Stop each running container by running:
docker stop CONTAINER_ID
- Replace CONTAINER_ID with the actual ID from the docker ps output.
- Your images remain cached, so future builds will be fast.
- Your fastapi-docker folder stays untouched on your Desktop.
Delete - I don't want to use this again
Remove all project resources and start fresh. This reclaims disk space used by Docker images and your project files.
- Stop and remove all Compose containers and networks by running this in your fastapi-docker folder:
docker compose down
- Remove the Docker images you built during this project by running:
docker rmi myapp myapp-dev myapp-prod
What does this command do?
The docker rmi command removes Docker images by name or tag. This deletes the three images you built during the project. Base images pulled from Docker Hub will remain unless you remove them separately.
- Delete the project folder. On macOS or Linux, run:
rm -rf ~/Desktop/fastapi-docker
- On Windows (PowerShell), run:
Remove-Item -Recurse -Force ~\Desktop\fastapi-docker
Nice Work!
Nice Work!
You just went from Docker zero to production-ready. You built a FastAPI application from scratch, containerized it with Docker, and progressed through the same Dockerfile evolution that professional teams follow.
You've learned how to:
- Built a FastAPI web application and containerized it with a Dockerfile using properly ordered layers for efficient caching.
- Upgraded to a multi-stage production Dockerfile that separates build-time tools from the final image for smaller, more secure containers.
- Created a development Dockerfile with hot reload and volume mounts for fast iteration without rebuilding the image.
- Orchestrated dev and prod environments with Docker Compose profiles to manage multiple workflows from a single file.
Ready to quiz yourself?