This repository contains the work completed for the DevOps practical examination.
frontend/index.html: A simple HTML web page containing a heading, paragraph, repository link, and button.backend/main.py: FastAPI backend application.backend/requirements.txt: Python dependencies for the backend.Dockerfile: Container image definition for the application.
git init
git add .
git commit -m "Initial commit: add index.html page"
git branch -M main
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPOSITORY.git
git push -u origin mainFirst, I initialized a Git repository using git init. Then I added all project files using git add . and committed them with a proper commit message. After that, I renamed the branch to main, connected the local repository to GitHub using git remote add origin, and pushed the code to GitHub using git push -u origin main.
Initial commit: added index.html pagemain: Stable production branch.develop: Integration branch where completed features are merged.feature: Branch used by a developer to work on a new feature.
git branch develop
git checkout develop
git checkout -b feature/update-homepage
git add .
git commit -m "Update homepage content"
git checkout develop
git merge feature/update-homepage
git checkout main
git merge develop
git push origin main
git push origin develop
git push origin feature/update-homepageFirst, I created a develop branch from main. Then I created a feature branch named feature/update-homepage from develop to simulate a developer working on a new feature. After completing the work, I committed the changes and merged the feature branch into develop. Finally, I merged develop into main to simulate releasing stable code.
A merge conflict was created by editing the same heading line in index.html on two different branches:
conflict-change-aconflict-change-b
git checkout -b conflict-change-a
git add index.html
git commit -m "Update heading from first conflict branch"
git checkout main
git checkout -b conflict-change-b
git add index.html
git commit -m "Update heading from second conflict branch"
git checkout main
git merge conflict-change-a
git merge conflict-change-bThe second merge created a conflict in index.html.
git statusI opened index.html, removed the conflict markers, and kept the final correct heading:
<h1>Welcome to My DevOps Practical Website</h1>Then I added and committed the resolved file:
git add index.html
git commit -m "Resolve homepage heading merge conflict"Update heading from first conflict branch
Update heading from second conflict branch
Resolve homepage heading merge conflict
Document merge conflict resolution stepsThe conflict happened because two branches changed the same line in index.html. Git could not decide which change to keep, so I manually edited the file, removed the conflict markers, kept the correct final heading, staged the file, and committed the merge conflict resolution.
backend/main.py: Simple FastAPI application that servesfrontend/index.html.backend/requirements.txt: Contains Python dependencies.frontend/index.html: Static frontend page served by FastAPI.Dockerfile: Builds the application container image..dockerignore: Removes unnecessary files from the Docker build context.
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN adduser --disabled-password --gecos "" --no-create-home appuser
COPY backend/requirements.txt .
RUN pip install -r requirements.txt
COPY --chown=appuser:appuser backend ./backend
COPY --chown=appuser:appuser frontend ./frontend
USER appuser
EXPOSE 8000
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]docker build -t devops-practical-app .
docker run -p 8000:8000 devops-practical-app- Used
python:3.12-slim, which is smaller than a full Python image. - Copied only required files into the image.
- Used
.dockerignoreto exclude Git files, screenshots, logs, environment files, and dependencies. - Set
PIP_NO_CACHE_DIR=1to avoid storing pip cache in the image. - Set
PIP_DISABLE_PIP_VERSION_CHECK=1to reduce unnecessary pip output and checks. - Set Python environment variables to avoid
.pycfiles and improve container logging. - Created and used a non-root
appuserfor better container security. - Used
--no-create-hometo avoid creating an unnecessary home directory for the container user. - Used
COPY --chown=appuser:appuserso files are owned by the non-root user.
Docker image build completed successfully:
Docker container running successfully:
The Dockerfile creates a lightweight image for the FastAPI application. The app serves the static frontend/index.html page through backend/main.py on port 8000 and provides a /health endpoint for container checks. The image is optimized by using a slim Python base image, excluding unnecessary files, installing dependencies without cache, and running the application as a non-root user.
To simulate a container startup failure, I ran the image with an incorrect FastAPI module path:
docker run --name devops-practical-fail devops-practical-app uvicorn backend.wrong:app --host 0.0.0.0 --port 8000docker ps -a
docker logs devops-practical-fail
docker inspect devops-practical-failERROR: Error loading ASGI app. Could not import module "backend.wrong".
The container failed because the startup command used the wrong module path:
backend.wrong:app
The actual FastAPI application is located in:
backend.main:app
I removed the failed container and started a new container using the correct application path:
docker rm devops-practical-fail
docker run -d --name devops-practical-fixed -p 8000:8000 devops-practical-app uvicorn backend.main:app --host 0.0.0.0 --port 8000Then I verified the application using the health check endpoint:
curl http://localhost:8000/healthExpected output:
{"status":"ok"}Container failed because of the wrong FastAPI module path:
Container started successfully after using the correct module path:
I used docker ps -a to check the stopped container, docker logs to view the startup error, and identified that the application module path was incorrect. After changing the command from backend.wrong:app to backend.main:app, the container started successfully and the health check returned {"status":"ok"}.
docker-compose.yml: Defines the app and database services..env.example: Shows required database environment variables without exposing real secrets.
app: FastAPI application built from the localDockerfile.database: PostgreSQL database using thepostgres:16-alpineimage.postgres_data: Docker volume used to persist database data.
services:
app:
build: .
container_name: devops-practical-app
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-devops_user}:${POSTGRES_PASSWORD:-devops_password}@database:5432/${POSTGRES_DB:-devops_db}
depends_on:
database:
condition: service_healthy
restart: unless-stopped
database:
image: postgres:16-alpine
container_name: devops-practical-db
environment:
POSTGRES_USER: ${POSTGRES_USER:-devops_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devops_password}
POSTGRES_DB: ${POSTGRES_DB:-devops_db}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres_data:If another container is already using port 8000, stop it first:
docker rm -f devops-practical-fixedStart the multi-container setup:
docker compose up --buildCheck running services:
docker compose psCheck logs:
docker compose logs app
docker compose logs databaseVerify the app:
curl http://localhost:8000/health
curl http://localhost:8000/configStop the setup:
docker compose downDocker Compose running the FastAPI app and PostgreSQL database:
Docker Compose was used to manage a multi-container application. The app service runs the FastAPI application, and the database service runs PostgreSQL. The app waits for the database health check before starting because of depends_on with condition: service_healthy. The database data is stored in a named Docker volume called postgres_data, so data can persist even if the database container is recreated.
.github/workflows/ci.yml: GitHub Actions workflow for continuous integration.backend/test_main.py: Unit tests for the FastAPI application.
name: CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt
- name: Run tests
run: |
python -m unittest discover -s backend -p "test_*.py"
- name: Validate Docker Compose file
run: |
docker compose config
- name: Build Docker image
run: |
docker build -t devops-practical-app .python3 -m unittest discover -s backend -p "test_*.py"
docker compose configGitHub Actions CI pipeline completed successfully:
The CI pipeline runs automatically on pushes and pull requests to the main and develop branches. It checks out the code, installs Python dependencies, runs unit tests, validates the Docker Compose file, and builds the Docker image. This helps detect broken code, invalid Compose configuration, or Docker build failures before changes are merged.
scripts/deploy.sh: Automates the deployment setup using Docker Compose.
#!/usr/bin/env bash
set -euo pipefail
APP_URL="${APP_URL:-http://localhost:8000}"
echo "Starting deployment setup..."
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is not installed or not available in PATH."
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker daemon is not running. Start Docker Desktop and try again."
exit 1
fi
if [ ! -f .env ] && [ -f .env.example ]; then
cp .env.example .env
echo "Created .env from .env.example."
fi
echo "Validating Docker Compose configuration..."
docker compose config >/dev/null
echo "Building and starting containers..."
docker compose up --build -d
echo "Waiting for application health check..."
for attempt in {1..10}; do
if curl -fsS "${APP_URL}/health" >/dev/null; then
echo "Application is healthy at ${APP_URL}/health"
docker compose ps
exit 0
fi
echo "Health check attempt ${attempt}/10 failed. Retrying..."
sleep 3
done
echo "Application did not become healthy. Showing app logs:"
docker compose logs app
exit 1Make the script executable:
chmod +x scripts/deploy.shRun the deployment script:
./scripts/deploy.shDeployment automation script completed successfully:
The script automates a repetitive deployment setup. It checks whether Docker is installed and running, creates .env from .env.example if needed, validates the Docker Compose file, builds and starts the containers, waits for the FastAPI health check, and shows logs if the application fails to become healthy.
.env.example: Template file showing required environment variables..gitignore: Ignores the real.envfile so secrets are not committed.docker-compose.yml: Loads environment variables into the containers.backend/main.py: Reads environment variables from the container environment.
APP_ENV=development
SECRET_KEY=change_this_secret_key
POSTGRES_USER=devops_user
POSTGRES_PASSWORD=change_this_password
POSTGRES_DB=devops_dbCreate a real local .env file from the example:
cp .env.example .envEdit .env and replace placeholder values:
nano .envConfirm .env is ignored by Git:
git status --shortStart the application with Docker Compose:
docker compose up --buildVerify that the app can read the environment configuration without exposing secret values:
curl http://localhost:8000/configExpected output:
{
"app_environment": "development",
"database_configured": true,
"database_host": "database",
"secret_key_configured": true
}Environment variables are configured securely and .env is ignored by Git:
Sensitive values should not be hardcoded in application code or committed to GitHub. I used .env.example to document required variables and .gitignore to prevent the real .env file from being committed. Docker Compose loads the variables from .env and passes them to the application. The /config endpoint only confirms whether values are configured; it does not expose the actual database password or secret key.
nginx/default.conf: Nginx reverse proxy configuration.docker-compose.yml: Added annginxservice.
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://app:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}nginx:
image: nginx:1.27-alpine
container_name: devops-practical-nginx
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
restart: unless-stoppedStart the services:
docker compose up --buildCheck that Nginx is running:
docker compose psTest the reverse proxy:
curl http://localhost:8080/health
curl http://localhost:8080/configCheck Nginx logs:
docker compose logs nginxNginx reverse proxy forwarding requests to the FastAPI backend:
Nginx runs as a separate container and acts as a reverse proxy in front of the FastAPI backend. Requests sent to localhost:8080 are received by Nginx and forwarded internally to the app service at http://app:8000. This keeps the backend behind a proxy layer and demonstrates how production traffic can be routed through Nginx.
https://devopspracticaltask.netlify.app
- Logged in to Netlify.
- Created a new site.
- Uploaded or connected the static frontend from the
frontendfolder. - Set the publish directory to
frontend. - Deployed the site and verified it in the browser.
Static frontend deployed successfully on Netlify:
The static frontend was deployed on Netlify so it can be accessed publicly through a live URL. Netlify serves the frontend/index.html page as a static website, which is suitable for simple HTML, CSS, and JavaScript frontend deployments.
logs-analysis.md: Contains log analysis notes, observations, possible root causes, and fixes.
docker compose ps
docker compose logs app
docker compose logs nginx
docker compose logs databaseThe FastAPI app logs showed successful startup:
Started server process
Application startup complete
Uvicorn running on http://0.0.0.0:8000
The app also showed successful requests:
GET /health HTTP/1.1 200 OK
GET /config HTTP/1.1 200 OK
The Nginx logs showed that the reverse proxy started successfully and forwarded requests:
Configuration complete; ready for start up
GET /health HTTP/1.1 200
GET /config HTTP/1.1 200
If the application fails, possible causes include:
- FastAPI backend container is stopped or unhealthy.
- Nginx is not running.
- Nginx
proxy_passpoints to the wrong service or port. - Port
8080or8000is already in use. - Database container is not healthy.
- Required environment variables are missing.
The logs showed successful startup and 200 OK responses, so there was no current application failure. A harmless Nginx startup notice appeared because the mounted config file is read-only, but Nginx still started correctly and served requests.
I analyzed logs using docker compose logs for the app, Nginx, and database services. The logs helped confirm that containers started correctly and requests were successfully handled. If a failure occurred, these logs would help identify whether the issue was caused by the application, reverse proxy, database, port conflict, or missing environment variables.
container-debug-notes.md: Contains the failing container command, logs, root cause, and proposed fix.
I created a failing container by starting the FastAPI app with an incorrect module path:
docker run --name devops-practical-log-debug devops_practical-app uvicorn backend.missing:app --host 0.0.0.0 --port 8000docker ps -a
docker logs devops-practical-log-debug
docker inspect devops-practical-log-debugERROR: Error loading ASGI app. Could not import module "backend.missing".
The container failed because the startup command referenced a module that does not exist:
backend.missing:app
The correct FastAPI application module is:
backend.main:app
Run the container with the correct Uvicorn command:
docker run -d --name devops-practical-fixed -p 8000:8000 devops_practical-app uvicorn backend.main:app --host 0.0.0.0 --port 8000Then verify the application:
curl http://localhost:8000/healthExpected output:
{"status":"ok"}I used docker ps -a to confirm that the container exited, then used docker logs to inspect the startup error. The logs showed that Uvicorn could not import the module backend.missing. The fix is to use the correct module path, backend.main:app, or update the Dockerfile/Compose command if the wrong path is configured there.
.github/workflows/cicd.yml: GitHub Actions CI/CD workflow with build, test, and deploy stages.
name: CI/CD
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Prepare environment file
run: |
cp .env.example .env
- name: Validate Docker Compose configuration
run: |
docker compose config
- name: Build Docker image
run: |
docker build -t devops-practical-app .
test:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt
- name: Run unit tests
run: |
python -m unittest discover -s backend -p "test_*.py"
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Simulate deployment
run: |
echo "Deploying application from main branch..."
echo "Deployment stage completed successfully."build: Validates Docker Compose configuration and builds the Docker image.test: Installs dependencies and runs backend unit tests.deploy: Runs only on themainbranch after tests pass and simulates deployment.
CI/CD workflow completed successfully:
This CI/CD workflow separates the pipeline into clear build, test, and deploy stages. The test stage depends on the build stage, and the deploy stage depends on the test stage. This prevents deployment if the build or tests fail. In a real production setup, the deploy step could be replaced with Netlify, SSH, Kubernetes, or cloud deployment commands using GitHub Secrets.
- Code is developed for the frontend and backend application.
- Code is committed to Git with proper commit messages.
- Code is pushed to GitHub.
- Developers use feature branches, merge into
develop, and merge stable code intomain. - GitHub Actions runs the CI pipeline.
- The pipeline installs dependencies and runs tests.
- Docker builds the application image using the
Dockerfile. - Environment variables are configured securely using
.env,.env.example, and GitHub Secrets when needed. - Docker Compose starts the application, database, and Nginx containers.
- Nginx acts as a reverse proxy and forwards requests to the FastAPI backend.
- The static frontend is deployed to Netlify.
- Health checks and logs are used to verify that the application is working.
- Wrong Git branch used for development or deployment.
- Merge conflict not resolved correctly.
- GitHub Actions workflow fails.
- Unit tests fail.
- Docker image build fails.
- Docker daemon is not running.
- Missing or incorrect environment variables.
- Database credentials are wrong.
- Database container is unhealthy.
- Application container fails to start.
- Wrong FastAPI module path used in the container command.
- Port
8000or8080is already in use. - Nginx reverse proxy is misconfigured.
- Netlify publish directory is set incorrectly.
- Health check endpoint fails.
- Logs show application or proxy errors.
The deployment lifecycle starts from writing code, committing it to GitHub, running CI tests, building a Docker image, configuring environment variables, deploying containers using Docker Compose, routing traffic through Nginx, and deploying the frontend to Netlify. At each stage, failures can happen, so CI results, container status, health checks, and logs are used to identify and fix issues.











