My personal website 2

Containerized Architecture: Building a High-Performance, Isolated Web Service Stack

Posted by yuanhang on June 09, 2026

Introduction

Moving an application from a simple local python script to a production-ready, cloud-native application requires an architecture that prioritizes isolation, scalability, and resilience. Relying on a framework's built-in development server in a production setting is an anti-pattern; it lacks the capacity to handle multi-threaded requests and exposes the underlying environment to critical instability.

This post highlights the production deployment of a personal web service stack on an open public cloud infrastructure. By utilizing Docker Compose, I orchestrate an interconnected, multi-layered environment comprising an Edge Reverse Proxy, an Application Web Server, a Python WSGI Framework, and an Enterprise Database Layer Engine—all operating inside an isolated virtual network environment.

1. The Architectural Blueprint

When an external user requests a page from the blog, the traffic flows through a highly structured, decoupled architecture. Each component is completely independent, running inside its own isolated Linux container:

[ Public Internet ] │ (Ports 80 / 443 HTTPS)
                             ▼
[ Nginx Proxy Manager (NPM) ] │ (Internal Container Network)
                             ▼
[ Gunicorn (WSGI Server) ] │ (WSGI Python Interface)
                             ▼
[ Flask Application ] │ (SQLAlchemy / Driver)
                             ▼
         [ PostgreSQL Database ]

 

2. Breaking Down the Architectural Layers

 

Layer A: The Reverse Proxy (Nginx Proxy Manager)

Nginx Proxy Manager (NPM) occupies the edge of our container architecture, acting as the front door facing the public cloud firewall. It handles two critical tasks:

  1. SSL/TLS Termination: It manages public Let's Encrypt certificates, ensuring all communication from the user's browser is encrypted over HTTPS before decrypting the packets and passing them into the internal network.

  2. Reverse Proxying: It hides the internal application layout from the public. Instead of pointing traffic to raw server ports, it reads the incoming domain name and smoothly forwards the traffic down the pipeline.

Layer B: The Web Server Gateway Interface (Gunicorn)

A common architectural question is: If we have Flask, why do we need Gunicorn? Flask's built-in server is single-threaded and built exclusively for debugging. If two users visit the site simultaneously, the second user is blocked until the first request completes.

Gunicorn (Green Unicorn) is a high-performance WSGI server designed to run Python apps at scale. It implements a robust Pre-Fork Worker Model:

  • The Master Process: Instantiated upon container startup, this process handles no direct web requests. It acts purely as a manager, loading the code and spawning independent Worker Processes.

  • The Worker Processes: These workers listen continuously on internal port 5000. When Nginx hands a request down, an idle worker picks it up, runs the Python code, and returns the response. If a worker encounters a catastrophic error and crashes, the Master process kills it and spins up a brand-new, healthy worker in milliseconds, preventing system downtime.

Layer C: The Application (Flask) & Database Interface (PostgreSQL)

The Flask container runs our custom application logic. Rather than communicating with the database via fragile, hardcoded string connections or raw SQL execution blocks, the backend architecture relies on SQLAlchemy ORM (Object-Relational Mapper).

SQLAlchemy translates Python object manipulations into optimized SQL queries. These queries are handed down to a binary driver (psycopg2), which pipes the data directly into an isolated PostgreSQL database container.

3. Infrastructure as Code: The docker-compose.yml Blueprints

The magic of this entire architecture is defined entirely as code inside a single configuration file. Below is the production-ready infrastructure blueprint:

services:
  postgres:
    image: postgres:15-alpine
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
  nginx-proxy-manager:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: always
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
    volumes:
      - ./npm/data:/data
      - ./npm/letsencrypt:/etc/letsencrypt
    depends_on:
      - flask-blog
  flask-blog:
    build: .
    restart: always
    environment:
      FLASK_ENV: ${FLASK_ENV}
      FLASK_KEY: ${FLASK_KEY}
      DB_URI: ${DB_URI}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/', timeout=5)"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  postgres_data:
    driver: local

 

4. Advanced DevOps Techniques Highlighted in This Stack

A. Internal Network Isolation & Zero-Trust DNS

Notice that the ports: block has been completely stripped out of the postgres and flask-blog services. They do not expose any ports to the host machine or the public internet.

When Docker Compose runs this file, it creates an isolated, private virtual software network. Containers speak to each other internally using Service Name Resolution (Docker's built-in DNS).

Nginx routes traffic internally to http://flask-blog:5000, and Flask speaks internally to postgresql://postgres:5432. Because no external entity can connect to port 5432 or 5000 directly, the database and application are perfectly safe from internet-wide brute force attacks.

B. Declarative Ordering via Advanced Healthchecks

A common issue in multi-container setups is a race condition: the application starts up faster than the database, tries to connect, fails, and crashes.

We solve this using deterministic orchestration hooks:

  1. The postgres container executes an internal pg_isready healthcheck.

  2. The flask-blog container specifies a strict dependency constraint: condition: service_healthy. Docker Compose will hold the Flask container back, refusing to start it until the database reports it is fully initialized and accepting connections.

  3. The flask-blog container executes its own lightweight healthcheck using a native Python urllib script to query its loopback interface. If Gunicorn or Flask stalls, Docker immediately flags the state as unhealthy for automated monitoring and recovery.

C. Persistent Storage Abstraction

Containers are ephemeral; if a container stops, any data written inside its file system layers is lost. To ensure enterprise-grade reliability, data permanence is decoupled from the runtime environment.

By mapping a named volume (postgres_data) to /var/lib/postgresql/data, the actual SQL database records are stored safely on the host's physical disk storage array. The database engine container can be destroyed, upgraded, or replaced seamlessly without losing a single line of application data.

Summary

By leveraging container isolation, internal service discovery, and state-based infrastructure dependencies, this architecture demonstrates how modern cloud backends achieve absolute stability and security. It shifts our security posture from a reliance on reactive monitoring to proactive architecture, creating a reliable, highly defensible environment fit for production workloads.