Lab goal
Students will create a small Python API, test it in the browser, package it into a Docker image, run it as a container, and push the project to GitHub.
At the end, the workflow will look like this:
Write API code
↓
Test locally
↓
Create Dockerfile
↓
Build Docker image
↓
Run Docker container
↓
Test API in browser
↓
Push code to GitHub
This is a realistic beginner DevOps workflow because DevOps engineers regularly work with application code, Git repositories, Docker images, ports, logs, and deployments.
Part 1: What we are building
Our API will have three endpoints:
GET /
GET /health
GET /courses
Expected results:
http://localhost:5001/
{
"message": "Welcome to JumpToTech API"
}
http://localhost:5001/health
{
"status": "healthy"
}
http://localhost:5001/courses
{
"courses": [
"Linux",
"Git",
"Docker",
"AWS"
]
}
Part 2: Check the required software
Open Terminal.
Run:
docker --version
Run:
git --version
Run:
python3 --version
Expected output will look similar to:
Docker version ...
git version ...
Python 3...
Why do we check this?
Before starting a project, a DevOps engineer verifies that the required tools are installed.
This prevents us from writing the entire project and discovering later that Docker, Git, or Python is missing.
Part 3: Create the project folder
Run:
mkdir github-docker-api-lab
Move into it:
cd github-docker-api-lab
Check your location:
pwd
Expected:
/Users/your-name/github-docker-api-lab
Why?
Every application should have its own project folder.
All files related to this API will stay together:
github-docker-api-lab/
├── app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
├── .gitignore
└── README.md
Part 4: Create the Python API with Vim
Create and open the application file:
vim app.py
Vim opens in command mode.
Press:
i
The letter i puts Vim into Insert mode, which allows you to type.
Paste this code:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Welcome to JumpToTech API"
})
@app.route("/health")
def health():
return jsonify({
"status": "healthy"
})
@app.route("/courses")
def courses():
return jsonify({
"courses": [
"Linux",
"Git",
"Docker",
"AWS"
]
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Now press:
Esc
Save and exit:
:wq
Press Enter.
Basic Vim commands
| Command | Meaning |
|---|---|
vim app.py |
Open or create the file |
i |
Start typing |
Esc |
Leave Insert mode |
:wq |
Save and exit |
:q! |
Exit without saving |
dd |
Delete one line |
u |
Undo |
/word |
Search for a word |
Part 5: Understand the API code
Import Flask
from flask import Flask, jsonify
Flask is a Python framework used to build web applications and APIs.
jsonify converts Python data into JSON.
Flask is designed to make small web applications easy to start, which is why it works well for a beginner API lab. (Flask Documentation)
Create the application
app = Flask(__name__)
This creates our Flask application.
Create the home endpoint
@app.route("/")
def home():
This tells Flask:
When a user sends a GET request to
/, run thehomefunction.
Return JSON
return jsonify({
"message": "Welcome to JumpToTech API"
})
An API usually returns structured data such as JSON instead of a full webpage.
Health endpoint
@app.route("/health")
DevOps engineers often use a health endpoint to determine whether an application is running.
Monitoring systems, load balancers, Docker, and Kubernetes can check endpoints like:
/health
/healthz
/status
Courses endpoint
@app.route("/courses")
This simulates getting data from an application.
Later, the data could come from:
- PostgreSQL
- MySQL
- MongoDB
- Another API
- AWS service
Host setting
host="0.0.0.0"
This is extremely important for Docker.
If Flask listens only on:
127.0.0.1
it may be reachable only from inside the container.
Using:
0.0.0.0
allows Flask to receive requests through Docker port mapping.
Part 6: Create requirements.txt
Run:
vim requirements.txt
Press:
i
Add:
Flask==3.1.1
Press:
Esc
Then:
:wq
Why do we need this file?
requirements.txt lists the Python packages required by the application.
Without it, another computer would not know that Flask must be installed.
In a real company:
Developer code
+
requirements.txt
↓
CI/CD pipeline
↓
Dependencies installed automatically
Part 7: Test the application without Docker
Install the dependency:
python3 -m pip install -r requirements.txt
Run the application:
python3 app.py
Expected output:
Running on http://127.0.0.1:5000
Running on http://...:5000
Open the browser:
http://localhost:5000
Then test:
http://localhost:5000/health
And:
http://localhost:5000/courses
Why test before Docker?
We first verify that the application itself works.
This separates two possible problems:
Application problem
from:
Docker problem
If the application fails before Docker, then Docker is not the cause.
Stop the Flask server by returning to Terminal and pressing:
Ctrl + C
Part 8: Create the Dockerfile with Vim
Run:
vim Dockerfile
Important: the file must be named exactly:
Dockerfile
Not:
Dockerfile.txt
dockerfile
DockerFile
Press:
i
Paste:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Press:
Esc
Save:
:wq
A Dockerfile is a text file containing the instructions Docker uses to create a container image. (Docker Documentation)
Part 9: Understand every Dockerfile instruction
FROM
FROM python:3.12-slim
This is our base image.
It already contains:
- Linux filesystem
- Python
- pip
- required runtime tools
The slim image is smaller than the full Python image. Docker recommends choosing minimal base images where practical because they reduce download size and unnecessary components. (Docker Documentation)
WORKDIR
WORKDIR /app
This creates or selects /app inside the image.
The commands that follow run from this directory.
Think of it like:
cd /app
inside the image.
COPY requirements.txt
COPY requirements.txt .
This copies the local file into /app inside the image.
The period means:
current working directory
Because WORKDIR is /app, the file becomes:
/app/requirements.txt
RUN
RUN pip install --no-cache-dir -r requirements.txt
This command runs while the image is being built.
It installs Flask into the image.
RUN is a build-time instruction.
COPY app.py
COPY app.py .
This copies the Python API into the image.
The image now contains:
/app/app.py
/app/requirements.txt
EXPOSE
EXPOSE 5000
This documents that the application listens on port 5000 inside the container.
It does not automatically make the application accessible from the Mac.
We still need -p when we run the container.
CMD
CMD ["python", "app.py"]
This starts the API when the container starts.
Difference:
RUN = executed while building the image
CMD = executed when starting the container
Part 10: Create .dockerignore
Run:
vim .dockerignore
Press i and add:
.git
.gitignore
README.md
__pycache__
*.pyc
venv
.env
Press Esc, then:
:wq
Why?
When we run:
docker build .
Docker sends the current directory as the build context.
We do not want unnecessary files included.
For example:
- Git history
- temporary Python files
- local virtual environment
- secret
.envfiles
This keeps builds cleaner and often faster.
Part 11: Create .gitignore
Run:
vim .gitignore
Press i and add:
__pycache__/
*.pyc
venv/
.env
.DS_Store
Press Esc, then:
:wq
Why?
.gitignore tells Git which local files should not be committed.
We normally do not push:
- operating-system files
- Python cache
- local environments
- secrets
- passwords
- API keys
Part 12: Verify all files
Run:
ls -la
You should see:
.
..
.dockerignore
.gitignore
Dockerfile
app.py
requirements.txt
Check the contents:
cat app.py
cat requirements.txt
cat Dockerfile
Why?
Your students previously had an empty Dockerfile. This check catches that problem before the build.
Part 13: Build the Docker image
Run:
docker build -t beginner-api:v1 .
Explanation:
docker build
Build an image.
-t
Add a name and tag.
beginner-api
Image name.
v1
Image version.
.
Use the current directory as the Docker build context.
Docker processes Dockerfile instructions in order, beginning from a base image defined by FROM. (Docker Documentation)
Part 14: Check the image
Run:
docker images
Expected:
REPOSITORY TAG
beginner-api v1
Important difference
Dockerfile = instructions
Image = packaged application
Container = running instance of the image
A container is a runnable instance of an image. (Docker Documentation)
Part 15: Run the container
Use host port 5001 to avoid conflicts with macOS services or another Flask process:
docker run -d \
-p 5001:5000 \
--name beginner-api-container \
beginner-api:v1
Explanation
docker run
Create and start a container.
-d
Run in detached mode, in the background.
-p 5001:5000
Map ports:
Mac port 5001 → container port 5000
--name beginner-api-container
Give the container a readable name.
beginner-api:v1
Use this image.
Part 16: Check the container
Run:
docker ps
Expected:
beginner-api-container
Up ...
0.0.0.0:5001->5000/tcp
Test in the browser:
http://localhost:5001
http://localhost:5001/health
http://localhost:5001/courses
Part 17: Test with curl
Run:
curl http://localhost:5001
Run:
curl http://localhost:5001/health
Run:
curl http://localhost:5001/courses
Why use curl?
A browser is useful, but DevOps engineers frequently test APIs from the terminal.
curl is helpful when:
- working on a remote Linux server
- troubleshooting containers
- testing CI/CD
- checking health endpoints
- testing APIs without a graphical browser
Part 18: Check logs
Run:
docker logs beginner-api-container
Refresh the browser, then run it again:
docker logs beginner-api-container
You should see requests such as:
GET / HTTP/1.1
GET /health HTTP/1.1
GET /courses HTTP/1.1
To follow logs live:
docker logs -f beginner-api-container
Press:
Ctrl + C
to stop following the logs.
Why?
Logs are often the first place DevOps engineers investigate when an application fails.
Part 19: Enter the container
Run:
docker exec -it beginner-api-container sh
Inside the container, run:
pwd
Expected:
/app
Run:
ls -la
Run:
cat app.py
Check Python:
python --version
Check installed packages:
pip list
Exit:
exit
Why?
This helps students see that the application and dependencies really exist inside the container.
In production, DevOps engineers may enter a container temporarily to investigate files, environment variables, or network configuration. Permanent changes should be made in code or the Dockerfile, not manually inside the running container.
Part 20: Create a README with Vim
Run:
vim README.md
Press i and add:
# Beginner GitHub Docker API Lab
This project demonstrates a simple Python Flask API running inside Docker.
## API endpoints
- `GET /`
- `GET /health`
- `GET /courses`
## Build the image
```bash
docker build -t beginner-api:v1 .
Run the container
docker run -d -p 5001:5000 --name beginner-api-container beginner-api:v1
Test
curl http://localhost:5001
curl http://localhost:5001/health
curl http://localhost:5001/courses
Stop and remove
docker stop beginner-api-container
docker rm beginner-api-container
Press `Esc`, then:
```text
:wq
Why?
A README explains:
- what the project does
- how to build it
- how to run it
- how to test it
A new team member should be able to use the project without asking the original developer for every command.
Part 21: Initialize Git
Run:
git init
Check:
git status
You should see untracked files.
Why?
git init turns the folder into a local Git repository.
Git will now track the history of the project.
Part 22: Add the files
Run:
git add .
Check:
git status
The files should appear under:
Changes to be committed
Why?
git add places files into the staging area.
The staging area lets us choose which changes should be part of the next commit.
Part 23: Commit
Run:
git commit -m "Create beginner Flask API with Docker"
Check history:
git log --oneline
Expected:
abc1234 Create beginner Flask API with Docker
Why?
A commit is a saved checkpoint.
Good commit messages explain what changed.
Bad:
update
stuff
changes
Better:
Create beginner Flask API with Docker
Add health-check endpoint
Update Docker port configuration
Part 24: Create a GitHub repository
Go to GitHub and sign in.
Then:
- In the upper-right corner, click the plus icon.
- Click New repository.
- In Repository name, enter:
beginner-github-docker-api
- Add a description:
Beginner Flask API containerized with Docker
- Choose Public or Private.
- Do not select Add a README file, because you already created one locally.
- Do not add
.gitignore, because you already created it. - Do not choose a license for this lab.
- Click Create repository.
GitHub’s current documented workflow is to use the upper-right menu, select New repository, enter the repository details, and create it. (GitHub Docs)
Part 25: Connect the local project to GitHub
After creating the empty repository, GitHub displays commands.
Copy the repository URL.
It should look similar to:
https://github.com/YOUR-USERNAME/beginner-github-docker-api.git
Return to Terminal.
Rename the main branch:
git branch -M main
Add GitHub as the remote:
git remote add origin https://github.com/YOUR-USERNAME/beginner-github-docker-api.git
Verify:
git remote -v
Expected:
origin https://github.com/YOUR-USERNAME/beginner-github-docker-api.git (fetch)
origin https://github.com/YOUR-USERNAME/beginner-github-docker-api.git (push)
GitHub documents git remote add as the command for connecting a local repository to a remote repository. (GitHub Docs)
Part 26: Push to GitHub
Run:
git push -u origin main
Git may open a browser and ask you to authenticate.
After the push finishes:
- Return to the GitHub repository page.
- Refresh the page.
- You should see:
app.py
requirements.txt
Dockerfile
.dockerignore
.gitignore
README.md
GitHub also documents using Git commands in the terminal to add locally hosted code to GitHub. (GitHub Docs)
Part 27: Make version 2
Open the API:
vim app.py
Move to the bottom of the endpoint section.
Press:
i
Add this route before the final if block:
@app.route("/students")
def students():
return jsonify({
"students": [
"Samara",
"Kasiet",
"Jipara"
]
})
Press Esc, then:
:wq
Check the change:
git diff
Part 28: Rebuild the image
The currently running container will not automatically receive the code change.
Build version 2:
docker build -t beginner-api:v2 .
Remove the old container:
docker rm -f beginner-api-container
Run version 2:
docker run -d \
-p 5001:5000 \
--name beginner-api-container \
beginner-api:v2
Test:
curl http://localhost:5001/students
Expected:
{
"students": [
"Samara",
"Kasiet",
"Jipara"
]
}
Why rebuild?
The image is an immutable package.
Changing app.py on your laptop does not change an image that was already built.
The correct workflow is:
Change code
↓
Build new image
↓
Create new container
This repeatability is one of the major reasons teams use Docker.
Part 29: Push version 2 to GitHub
Run:
git status
Stage:
git add app.py
Commit:
git commit -m "Add students API endpoint"
Push:
git push
Go to GitHub and refresh.
Click:
app.py
You should see the /students endpoint.
Click Commits to see both commits.
Final architecture
Developer
|
| edits with Vim
v
Python Flask API
|
| git add / commit / push
v
GitHub Repository
|
| docker build
v
Docker Image
|
| docker run
v
Docker Container
|
| port 5001 → 5000
v
Browser or curl
What each technology did
API
The API accepted HTTP requests and returned JSON.
Client request → API → JSON response
Docker
Docker packaged:
- Python
- Flask
- source code
- dependency information
- startup command
Git
Git tracked changes locally through commits.
GitHub
GitHub stored the repository remotely so the code could be shared, reviewed, and later used by a CI/CD pipeline.
Vim
Vim allowed students to create and edit files directly from the terminal, which is especially useful on remote Linux servers.
Troubleshooting
Error: Dockerfile cannot be empty
Check:
cat Dockerfile
If nothing appears:
vim Dockerfile
Add the Dockerfile content and save with:
Esc
:wq
Error: port already allocated
Use another host port:
docker run -d -p 5002:5000 --name beginner-api-container beginner-api:v1
Then open:
http://localhost:5002
Error: container name already in use
Run:
docker ps -a
Remove the old container:
docker rm -f beginner-api-container
Run again.
Error: unable to find image locally
Check:
docker images
If the image does not exist, build it:
docker build -t beginner-api:v1 .
Container starts but browser does not work
Check:
docker ps
Then:
docker logs beginner-api-container
Make sure the Python code contains:
app.run(host="0.0.0.0", port=5000)
Also make sure the port mapping is:
-p 5001:5000
Git says remote origin already exists
Check:
git remote -v
Change it:
git remote set-url origin https://github.com/YOUR-USERNAME/beginner-github-docker-api.git
Student challenge
Ask students to add one new endpoint:
GET /devops-tools
It should return:
{
"tools": [
"Git",
"Docker",
"Terraform",
"Ansible",
"Kubernetes"
]
}
Then they must:
1. Test the Python application
2. Build beginner-api:v3
3. Replace the container
4. Test with curl
5. Commit the change
6. Push it to GitHub
This lab teaches the complete beginner DevOps cycle:
Code → Test → Package → Run → Troubleshoot → Version → Push
Top comments (0)