Building a Student REST API with Flask and Docker
Hey there! I recently built a REST API to manage student records using Flask, Flask-SQLAlchemy, and SQLite, and shared it in my last post. Now, I have taken it to the next level by containerizing it with Docker, making it portable and consistent across machines.
In this post, I will walk you through how I Dockerized my API, including the mistakes I made and how I fixed them, so you can follow along or avoid my pitfalls.
Why Docker?
Docker packages your app with its dependencies into a container, ensuring it runs the same everywhere, your laptop, a server, or the cloud. For my Student API, this meant no more “it works on my machine” excuses.
Let’s dive into the steps I took, bugs and all.
Step 1: Writing the Dockerfile
I started by creating a Dockerfile
in my project folder (student-api-new/
) to define how to build the Docker image. I used a multi-stage build to keep the image small: one stage for installing dependencies, another for running the app.
Stage 1: Build dependencies
# Use the slim version of Python 3.13 as base image
FROM python:3.13-slim AS builder
# Set working directory in the container
WORKDIR /app
# Update package list
RUN apt-get update
# Install build tools like gcc, then clean up
RUN apt-get install -y --no-install-recommends gcc && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
# Copy the requirements file into the container
COPY requirements.txt .
# Install required Python packages without caching
RUN pip install --no-cache-dir -r requirements.txt
# Install Gunicorn explicitly for running the app
RUN pip install --no-cache-dir gunicorn==23.0.0
Stage 2: Runtime image
# Start again from a clean slim Python image
FROM python:3.13-slim
# Set working directory again for final image
WORKDIR /app
# Copy installed Python packages from the builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
# Copy Gunicorn from builder stage
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn
# Copy your actual Flask app code
COPY app/ ./app/
# Create non-root user for better security
RUN useradd -m appuser && chown -R appuser:appuser /app
# Switch to the new user
USER appuser
# Expose the port the app runs on
EXPOSE 5000
# Set the Flask app environment variable
ENV FLASK_APP=app
# Command to run the Flask app with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:create_app()"]
What I Learned
- Multi-Stage Builds: The builder stage handles heavy lifting (like installing gcc), but the final image only includes essentials, keeping it around 235MB.
-
Slim Base Image:
python:3.13-slim
is lighter than the full Python image. -
Non-Root User: Using
appuser
improves security for production.
Step 2: Adding a .dockerignore
To reduce the image size, I created a .dockerignore
file to exclude unnecessary files, like my virtual environment and test folder:
.venv/
.env
__pycache__/
*.pyc
students.db
.git/
.gitignore
tests/
README.md
Makefile
This ensured only the app code and dependencies went into the container, saving space.
Step 3: Building the Docker Image
I built the image with a semantic version tag (avoiding latest
, which I learned is a no-no for reproducibility):
# Build the Docker image and tag it
docker build -t student-api:1.0.0 .
Step 4: Running the Container
To run the API, I used a command that maps port 5000 and loads environment variables from a .env
file:
# Run the container in detached mode and pass in environment variables
docker run -d -p 5000:5000 --env-file .env student-api:1.0.0
Contents of my .env
file:
FLASK_ENV=development
DATABASE_URL=sqlite:///students.db
SECRET_KEY=mysecretkey
Step 5: Testing the API
With the container running, I tested it using curl
:
# Check if the API is healthy
curl http://localhost:5000/api/v1/healthcheck
Output:
## Output: {"status":"healthy"}
# Add a student record
curl -X POST http://localhost:5000/api/v1/students \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 20}'
Output:
{"id":1,"name":"Alice","age":20}
Seeing those responses felt like such a huge win considering all the errors I encountered 🥲 My API was finally alive in Docker.
Step 6: Updating Documentation
I updated my README.md
to include Docker instructions, so anyone could try it.
Setup (Docker)
Prerequisites
- Docker installed (
docker --version
)
Build the Image
make docker-build
Run the Container
make docker-run
I also added Makefile
targets to simplify things:
# Makefile for building and running Docker image
docker-build:
docker build -t student-api:1.0.0 .
docker-run:
docker run -d -p 5000:5000 --env-file .env student-api:1.0.0
What I Learned: Good docs make your project accessible. The Makefile was a lifesaver for quick commands.
Step 7: Committing to GitHub
I wanted to share my Docker setup on GitHub, so I committed the Dockerfile and other files:
# Add Docker setup files to Git
git add Dockerfile .gitignore
# Commit with message
git commit -m "Add Docker setup for Student API"
# Push to GitHub
git push origin main
Errors I Ran Into (and How I Fixed Them)
1. Slow Build Time (17+ Minutes)
Error:
[+] Building 1050.2s (8/12)
=> [builder 4/4] RUN apt-get update && apt-get install ... 970.8s
Why: My internet connection was slow, which bogged down apt-get
.
Fix:
# Check network speed
curl -o /dev/null http://deb.debian.org/debian/dists/bookworm/InRelease
# Clear Docker build cache
docker builder prune
Also split apt-get update
into a separate RUN
to cache it better. My build time dropped to ~1–2 minutes afterwards.
2. Gunicorn Not Found
Error:
docker: Error response from daemon: ... exec: "gunicorn": executable file not found in $PATH
Why: I forgot to add gunicorn
to requirements.txt
, and didn’t rebuild properly.
Fix:
# requirements.txt
Flask==3.1.0
Flask-SQLAlchemy==3.1.1
gunicorn==23.0.0
Then in Dockerfile:
RUN pip install --no-cache-dir gunicorn==23.0.0
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn
Rebuild clean:
docker build --no-cache -t student-api:1.0.0 .
3. Container Conflict When Rebuilding
Error:
Error response from daemon: conflict: unable to delete student-api:1.0.0 ... container 25ae709a77aa is using it
Fix:
# List all containers (even stopped)
docker ps -a
# Remove the stopped container
docker rm 25ae709a77aa
# Remove the image
docker rmi student-api:1.0.0
4. Git Ignoring Dockerfile
Error:
The following paths are ignored by one of your .gitignore files: Dockerfile
Fix: Removed Dockerfile*
from .gitignore
:
.venv/
.env
__pycache__/
*.pyc
students.db
.git/
.gitignore
tests/
docker-compose*
*.dockerignore
Then committed successfully.
5. VS Code Warning About Gunicorn
Error:
Package gunicorn is not installed in the selected environment
Why: I hadn't installed gunicorn
in my local .venv/
.
Fix:
# Activate virtual environment
source .venv/bin/activate
# Install dependencies locally
pip install -r requirements.txt
What I Learned Overall
These errors taught me to:
-
Rebuild Images: Always rebuild after changing
requirements.txt
orDockerfile
. -
Check Logs:
docker logs <container-id>
is your best debugging buddy. - Clean Up: Remove old containers/images to avoid conflicts.
- Document Everything: Clear steps save time later.
What’s Next?
Dockerizing my API was a huge win, as it is now portable and production-ready. My next steps are:
- Pushing
student-api:1.0.0
to Docker Hub to share with others. - Adding endpoints like
GET /students
orDELETE
. - Writing more tests to ensure reliability.
Thanks for reading! If you’re Dockerizing your own project, I hope my mistakes save you some headaches. Drop a comment with your tips or questions, I’d love to hear them ✨