Deploying a Flask Web App as a Docker Container on Fly.io using Jenkins
Introduction
While it might sound somewhat philosophical, I've always believed that certain activities approach a form of meditation. Take laughter, for example; it has the power to momentarily immerse us in pure being. Similarly, studying, especially when engrossed in a subject we love, can make us lose all sense of time and place. This was precisely my experience when I began preparing for the CCNA (Cisco Certified Network Associate) certification on July 1st, 2023. My journey, culminating in passing the exam on September 14th, spanned a rigorous 75 days. During this period, I not only delved deep into the exam topics but also honed my study methods, embracing tools like Anki and Notion, and undertaking various projects to optimize my study hours.
One crucial skill for the CCNA exam is rapid subnetting - being able to calculate subnet addresses within seconds. While preparing, I noticed a gap in the available practice tools: none offered timed responses with comprehensive feedback. This realization led me to create a simple Python script using the ipaddress library, which later evolved into a full-fledged web application.
In this blog, I aim to outline my approach to this project. I'll discuss why I chose specific tools, how I integrated them into the project, and the insights I gained along the way.
Overview of the Project
The core concept behind my IPv4 subnetting app is elegant in its simplicity. Users specify how many questions they want to tackle, and the application generates random subnetting problems. For each question, users need to determine four key addresses: network, broadcast, first usable host, and last usable host. After completing the quiz, they receive detailed feedback including:
- Overall score
- Average time per question
- Detailed breakdown of each answer
- Correct solutions for comparison
I deliberately kept the user experience streamlined - no login required, no personal data collected. The focus is purely on practice and improvement.
For the technical implementation, I chose Flask as my web framework for its simplicity and efficiency. The application's architecture follows a modern DevOps approach:
- Local development and testing
- GitHub repository for version control
- Jenkins for automated builds
- Docker for containerization
- Fly.io for deployment
This setup creates a seamless pipeline where code changes automatically trigger builds, tests, and deployment - a significant improvement over manual deployment processes
Development with Flask
-
Flask Framework: I selected Flask for its "micro" nature - it provides exactly what's needed for web development without unnecessary complexity. Unlike larger frameworks that come with numerous built-in features, Flask offers the essentials while allowing flexibility in architectural decisions. Here's a simple example that demonstrates Flask's elegance:
from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, World!'
This minimalist approach aligned perfectly with my project's goals - creating a focused, efficient learning tool without excessive overhead. Flask's simplicity also made it easier to containerize and deploy the application later in the development process.
-
App Development: The development of the Flask application was driven by my original Python script's functionality, but with added web capabilities and user interaction. The core of the application revolves around generating random subnetting questions and validating user responses. One of my initial challenges was managing user sessions effectively. I needed to track each user's progress, store their answers, and calculate timing - all without implementing a database. Flask's session management provided an elegant solution:
@app.route('/', methods=['GET', 'POST']) def ipv4_quiz(): if 'question_details' not in session: return redirect('/settings') if request.method == 'POST': # Validate user answers user_answer_network = request.form['network_address'] user_answer_broadcast = request.form['broadcast_address'] subnet = ipaddress.IPv4Network(session['current_subnet']) is_correct = (user_answer_network == str(subnet.network_address) and user_answer_broadcast == str(subnet.broadcast_address))
A particularly interesting aspect was generating random yet valid subnetting questions. I leveraged Python's ipaddress library to ensure all generated questions were practical and accurate:
def generate_subnetting_question(): while True: random_ip = ".".join(str(random.randint(0, 255)) for _ in range(4)) random_cidr_prefix = random.randint(8, 30) try: subnet = ipaddress.IPv4Network(f'{random_ip}/{random_cidr_prefix}') host_addresses = [str(ip) for ip in subnet.hosts()] if not host_addresses: continue question_address = random.choice(host_addresses) return question_address, subnet except ValueError: continue
One challenge I faced was timing user responses accurately. I wanted to measure how long users took to answer each question without relying on client-side JavaScript. My solution was to store the start time in the session when generating a new question and calculate the difference when receiving the answer:
session['start_time'] = time.time() # When receiving answer: question_time = time.time() - session['start_time']
To maintain a clean and organized codebase, I separated the core subnetting logic from the web application code. This not only made the code more maintainable but also allowed me to test the subnetting functions independently of the Flask application. The front end was intentionally kept minimal and responsive, focusing on functionality over aesthetics. I used basic HTML and CSS to ensure the application would work well across different devices and screen sizes, making it accessible to students practicing on various devices.
Containerization with Docker
-
Introduction to Docker: I realized that my application needed to be easily deployable and consistent across different environments. This led me to Docker, a technology that fundamentally changed how I approached application deployment. Docker solves what I call the "it works on my machine" problem through containerization. Think of containers as lightweight, standalone packages that include everything needed to run an application - the code, runtime environment, system libraries, and settings. It's like creating a standardized shipping container for software, ensuring that my application runs exactly the same way whether it's on my development laptop or in production. What particularly drew me to Docker was its ability to isolate applications and their dependencies. Instead of worrying about Python version conflicts or missing libraries on different systems, I could package everything into a single container image. This meant anyone wanting to run my subnetting practice app wouldn't need to manually set up Python, Flask, and other dependencies - they could simply run the container.
-
Dockerizing the Flask App: Containerizing my Flask application was surprisingly straightforward. I started by creating a Dockerfile in my project's root directory:
# Use Python 3.9 as the base image FROM python:3.9-slim # Set working directory in container WORKDIR /app # Copy requirements file COPY requirements.txt . # Install dependencies RUN pip install -r requirements.txt # Copy the rest of the application COPY . . # Expose port 8080 EXPOSE 8080 # Command to run the application CMD ["python", "subnetting.py"]
My requirements.txt file kept track of my application's dependencies:
flask==2.0.1 ipaddress==1.0.23
The most challenging part was configuring the application for containerized deployment. I needed to make a few adjustments to my Flask application to ensure it worked properly within a container:
-
Modified the host and port settings in my Flask app:
if __name__ == '__main__': app.run(host='0.0.0.0', port=8080)
-
Ensured all paths in the application were relative to the container's working directory
-
Verified that all environment-specific configurations were externalized
To build and test the container locally, I used these commands:
# Build the Docker image docker build -t subnetting-app . # Run the container docker run -p 8080:8080 subnetting-app
One interesting challenge I encountered was managing the Flask application's secret key within the container. Initially, I had hardcoded it in the application, but for security reasons, I modified it to accept an environment variable:
app.secret_key = os.environ.get('SECRET_KEY', 'default-dev-key')
This Dockerization process not only made my application more portable but also forced me to think about proper application configuration and security practices. It was a valuable learning experience that went beyond just containerization, teaching me about proper application architecture and deployment best practices.
-
Continuous Integration with Jenkins
-
CI/CD Concepts: After containerizing my application, I wanted to streamline the development and deployment process. This led me to explore CI/CD (Continuous Integration/Continuous Deployment), a modern software development practice that has transformed how I manage my application updates. Continuous Integration, in my implementation, means that every time I push code changes to my GitHub repository, they're automatically tested and built. Continuous Deployment takes this a step further - once the changes pass all checks, they're automatically deployed to production. This automation eliminated the tedious manual steps I initially performed for each update to my subnetting app.
-
Jenkins Setup: Setting up Jenkins for my project was both challenging and rewarding. I installed Jenkins on my local machine and configured it to monitor my GitHub repository. The basic setup involved:
- Creating a new Jenkins pipeline project
- Configuring GitHub webhook to notify Jenkins of any code pushes
- Setting up necessary credentials for Docker Hub access
Here's a snippet of my Jenkins pipeline configuration (Jenkinsfile):
pipeline { agent any environment { DOCKERHUB_CREDENTIALS = credentials('dockerhub') APP_NAME = 'subnetting-app' DOCKER_IMAGE = "myusername/${APP_NAME}" } stages { stage('Checkout') { steps { checkout scm } } stage('Build Docker Image') { steps { script { docker.build("${DOCKER_IMAGE}:${BUILD_NUMBER}") } } } stage('Push to Docker Hub') { steps { script { docker.withRegistry('<https://registry.hub.docker.com>', 'dockerhub') { docker.image("${DOCKER_IMAGE}:${BUILD_NUMBER}").push() docker.image("${DOCKER_IMAGE}:${BUILD_NUMBER}").push('latest') } } } } } }
-
Automating Deployment: The most satisfying part of my CI/CD setup was watching the automated deployment process in action. Here's how I structured the automation flow:
- When I push code to GitHub, a webhook triggers Jenkins to start a new build
- Jenkins pulls the latest code and executes the pipeline:
- Builds a new Docker image with the latest changes
- Runs automated tests (if any fail, the deployment stops)
- Pushes the image to Docker Hub with appropriate tags
- Triggers deployment to Fly.io
I added a final deployment stage to my Jenkins pipeline to handle the Fly.io deployment:
stage('Deploy to Fly.io') { steps { script { sh ''' flyctl deploy --image ${DOCKER_IMAGE}:${BUILD_NUMBER} ''' } } }
One challenge I faced was managing secrets and credentials securely. I solved this by using Jenkins' credential management system to store sensitive information like Docker Hub and Fly.io credentials, ensuring they never appeared in the code. This automation setup saves me significant time - what used to be a 15-20 minute manual deployment process now happens automatically in a few minutes. More importantly, it reduces the chance of human error during deployment and ensures consistent builds every time. A particularly useful feature I implemented was automatic rollback - if a deployment fails, Jenkins automatically reverts to the last stable version, ensuring my application remains available to users even if something goes wrong during deployment.
Deployment on Fly.io
-
Why Fly.io: After exploring various hosting platforms like Heroku, AWS, and DigitalOcean, I chose Fly.io for several compelling reasons. My primary requirement was a platform that could host my Docker container while being both cost-effective and easy to manage for a small project.
Fly.io stood out because it offers a generous free tier that perfectly suited my needs - allowing me to run my subnetting practice app without incurring costs during the initial phase. What really impressed me was its ability to deploy applications close to users through its global edge network, ensuring low latency regardless of where my users are located.
Another factor that influenced my decision was Fly.io's straightforward command-line interface. As someone who appreciates clean, no-nonsense tooling, the
flyctl
CLI tool felt natural to use. -
Deployment Process: Setting up my application on Fly.io was remarkably straightforward. Here's how I configured and deployed my containerized application:
- First, I installed the Fly CLI:
curl -L <https://fly.io/install.sh> | sh
- Then, I created a
fly.toml
configuration file in my project root:
app = "subnetting-quiz" primary_region = "iad" [env] PORT = "8080" [http_service] internal_port = 8080 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 0 [[services]] protocol = "tcp" internal_port = 8080
- The initial deployment was as simple as:
flyctl launch
For automated deployments through Jenkins, I created a specific deployment command:
flyctl deploy --image registry.hub.docker.com/myusername/subnetting-app:latest
What I particularly appreciate about Fly.io's deployment process is the automatic SSL certificate provisioning and the ability to roll back deployments if something goes wrong. This proved invaluable during one instance when I accidentally deployed a broken version - I could quickly revert to the previous working version using:
flyctl deploy --image registry.hub.docker.com/myusername/subnetting-app:previous-tag
The platform also provides useful monitoring capabilities. I can easily check my application's status and logs:
# Check application status flyctl status # View logs in real-time flyctl logs
This setup has been running smoothly for several months now, requiring minimal maintenance while providing reliable service to users practicing their subnetting skills. The combination of Docker containerization and Fly.io's deployment platform has given me exactly what I needed - a reliable, scalable, and cost-effective way to host my application.
Results and Learnings
-
Outcomes: The end result of this project exceeded my initial expectations. What started as a simple command-line script for my CCNA preparation evolved into a fully functional web application that helps networking students practice their subnetting skills efficiently.
The application now features:
- A clean, intuitive interface that works smoothly across devices
- Randomized subnet questions that cover a comprehensive range of scenarios
- Real-time performance tracking with detailed timing metrics
- Immediate feedback on answers with detailed explanations
- A robust backend that handles concurrent users effectively
-
Key Learnings: This project has been an incredible learning journey, teaching me valuable lessons across multiple domains:
- Technical Skills:
- Gained practical experience with Flask and web application architecture
- Mastered Docker containerization and understanding of microservices
- Learned how to implement CI/CD pipelines using Jenkins
- Developed a deeper understanding of cloud deployment with Fly.io
- Development Best Practices:
- The importance of separating concerns in code (like keeping the subnetting logic separate from the web interface)
- The value of automated testing and deployment
- How to manage application state without a database using Flask sessions
- The significance of proper error handling in web applications
- Project Management:
- Starting small and iterating based on feedback works better than trying to build everything at once
- The importance of documentation, not just for others but for my future self
- How to balance feature development with maintenance and improvements
- Technical Skills:
Future Enhancements
-
Planned Improvements: While the current version of the subnetting practice app serves its core purpose effectively, I have several exciting improvements planned for the future:
-
Learning-Focused Features:
- Implementation of difficulty levels (beginner, intermediate, advanced) to help users progress systematically
- Addition of a "Practice Mode" where users can take their time with detailed explanations of each step
-
User Experience Enhancements:
- Development of a progress tracking system to help users monitor their improvement over time
- Addition of keyboard shortcuts for faster answer input
- Implementation of a dark mode option for reduced eye strain during long practice sessions
-
Educational Resources:
- Integration of quick reference guides and cheat sheets
- Addition of video explanations for particularly challenging concepts
-
Technical Improvements:
# Example of planned IPv6 support def generate_ipv6_question(): random_prefix = ipaddress.IPv6Network( (random.getrandbits(128), random.randint(48, 64)), strict=False ) return random_prefix
- Expansion to include IPv6 subnetting practice
- Implementation of API endpoints for potential integration with other learning platforms
-
Social Features:
- Optional user accounts for progress tracking
- A global leaderboard for speed and accuracy
- Ability to create and share custom question sets
The beauty of this project is that it can grow in many directions while maintaining its core purpose of helping people learn subnetting. I'm especially excited about implementing these features based on user feedback.
These planned improvements aim to transform the application from a simple practice tool into a comprehensive learning platform while maintaining its simplicity and ease of use.
-
footnote:
I'd like to thank Anthropic's Claude for assistance with organizing and articulating my thoughts in this blog post. While the project, experiences, and technical implementations are entirely my own, Claude helped me structure and express these ideas more clearly.
Sources:
Github Repo: https://github.com/alapvyas/Subnetting_project
Flask Documentation: https://flask.palletsprojects.com/en/3.0.x/
Docker Documentation: https://docs.docker.com/
Jenkins Documentation: https://www.jenkins.io/doc/
Fly Documentation: https://fly.io/docs/
Comments
Post a Comment