Case Study Part 2: The Foundation – Building a Secure Backend with Docker

In the first part of this series, I laid out the architectural blueprint for the Financial & Legal Case Manager app. With a solid plan in place, it was time to move from diagrams to code and build the application’s foundation. This phase was guided by a core DevOps principle: start with a production-like environment from day one.

This meant two things: containerizing everything with Docker and prioritizing security from the very first feature.

Containerizing the World: The Power of Docker

Before writing a single line of Python, I wrote the Dockerfile. This “Infrastructure as Code” approach ensures that the application runs in an identical, predictable environment, whether on my local machine or on the final production server. It completely eliminates the classic “it works on my machine” problem.

Here is the Dockerfile that defines the application’s environment:

# Dockerfile

# 1. Base Image: Start with a lean, official Python image.
FROM python:3.11-slim

# 2. Set Working Directory: All subsequent commands run inside /app.
WORKDIR /app

# 3. Install Dependencies: Copy only the requirements file first to leverage Docker's layer caching.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copy Application Code: Copy the rest of the application's source code.
COPY . .

# 5. Expose Port: Inform Docker that the application will listen on port 5000.
EXPOSE 5000

# 6. Run Command: The command to start the Gunicorn production server when the container starts.
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:5000", "app:app"]

With the application’s environment defined, the docker-compose.yml file orchestrates how it interacts with other services, like the database, creating a complete, isolated development environment with a single command: docker-compose up.

Modeling the Data: The Single Source of Truth

With the environment ready, the next step was to define the database structure using SQLAlchemy. This Object-Relational Mapper (ORM) allows me to define database tables as Python classes, making the code clean and maintainable.

The core of the application revolves around three main models:

  • Usuario (User): Manages application access.
  • Cliente (Client): Stores client information.
  • AcaoJuridica (Legal Case): Links a client to a specific case.
  • Movimentacao (Transaction): Records every financial transaction associated with a legal case.

The relationships between these models were crucial. A Cliente can have multiple AcaoJuridica records, and each AcaoJuridica can have multiple Movimentacao records. Setting up these one-to-many relationships with db.relationship and db.ForeignKey ensured data integrity from the start.

Security First: A Closed Authentication System

For an internal application handling sensitive financial data, security cannot be an afterthought. The very first functional feature I implemented was user authentication.

I chose a closed authentication system. There is no public registration page. Users are created and managed by an administrator via a command-line interface (CLI) built with Flask’s Click integration. This design choice significantly reduces the application’s attack surface.

Passwords are, of course, never stored in plaintext. I used werkzeug.security to hash all passwords upon creation (generate_password_hash) and verify them during login (check_password_hash), ensuring that even if the database were compromised, the user passwords would remain secure.

With a containerized environment, a solid data model, and a secure authentication system in place, the foundation was complete. The application was ready for its core business logic and the challenges of a real-world production deployment.

In the next post, I’ll detail the most challenging and rewarding part of this project: building the secure CI/CD pipeline and deploying the application to a live production server. Get ready for a deep dive into the world of DevOps troubleshooting!