BLOG POSTS
    MangoHost Blog / How to Build and Deploy a Flask Application Using Docker on Ubuntu 24
How to Build and Deploy a Flask Application Using Docker on Ubuntu 24

How to Build and Deploy a Flask Application Using Docker on Ubuntu 24

Building and deploying Flask applications using Docker on Ubuntu 24 is one of those skills that’ll save you countless headaches and make your deployment process bulletproof. This guide walks you through creating a containerized Flask app that you can deploy anywhere – from your local development machine to production servers – with consistent behavior every single time. We’ll cover everything from basic setup to advanced deployment strategies, including real-world examples and gotchas you’ll definitely encounter along the way.

How Docker + Flask Actually Works Under the Hood

Think of Docker as a lightweight virtual machine that packages your Flask app with all its dependencies into a single, portable container. Unlike traditional VMs that virtualize entire operating systems, Docker containers share the host OS kernel, making them incredibly efficient.

Here’s what happens when you containerize a Flask app:

  • Docker creates an isolated environment with your specified Python version
  • Your Flask app and all pip dependencies get bundled together
  • The container runs your app on a specified port (usually 5000 for Flask)
  • You can map container ports to host ports for external access
  • Multiple containers can run simultaneously without conflicts

The magic happens through Docker’s layered filesystem. Each instruction in your Dockerfile creates a new layer, and Docker caches these layers for lightning-fast rebuilds. Change your app code? Only the final layer rebuilds. Update dependencies? Only the pip install layer and subsequent layers rebuild.

Quick Setup: From Zero to Deployed Flask App

Let’s get our hands dirty. First, make sure your Ubuntu 24 system is ready:

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install Docker
sudo apt install docker.io docker-compose -y

# Add your user to docker group (logout/login required after this)
sudo usermod -aG docker $USER

# Start and enable Docker service
sudo systemctl start docker
sudo systemctl enable docker

# Verify installation
docker --version
docker-compose --version

Now let’s create a simple Flask application structure:

# Create project directory
mkdir flask-docker-app && cd flask-docker-app

# Create the basic structure
mkdir app
touch app/__init__.py
touch app/main.py
touch requirements.txt
touch Dockerfile
touch docker-compose.yml
touch .dockerignore

Here’s our basic Flask app (app/main.py):

from flask import Flask, jsonify, request
import os
import socket

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        'message': 'Hello from Docker!',
        'hostname': socket.gethostname(),
        'environment': os.environ.get('FLASK_ENV', 'production')
    })

@app.route('/health')
def health_check():
    return jsonify({'status': 'healthy', 'code': 200}), 200

@app.route('/api/data', methods=['GET', 'POST'])
def api_data():
    if request.method == 'POST':
        data = request.get_json()
        return jsonify({'received': data, 'method': 'POST'})
    return jsonify({'message': 'GET request received', 'method': 'GET'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=os.environ.get('FLASK_ENV') == 'development')

Requirements file (requirements.txt):

Flask==3.0.0
gunicorn==21.2.0
python-dotenv==1.0.0

Now the crucial Dockerfile:

# Use Python 3.11 slim image for smaller size
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    FLASK_APP=app/main.py

# Install system dependencies
RUN apt-update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements first for better caching
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY app/ ./app/

# Create non-root user for security
RUN adduser --disabled-password --gecos '' flaskuser && \
    chown -R flaskuser:flaskuser /app
USER flaskuser

# Expose port
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:5000/health || exit 1

# Run with gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app.main:app"]

Docker Compose for easier management (docker-compose.yml):

version: '3.8'

services:
  flask-app:
    build: .
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
    volumes:
      - ./app:/app/app  # For development only
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - flask-app
    restart: unless-stopped

Don’t forget your .dockerignore file:

__pycache__
*.pyc
.git
.gitignore
README.md
Dockerfile
.dockerignore
node_modules
.pytest_cache

Build and run your application:

# Build the image
docker build -t flask-docker-app .

# Run single container
docker run -d -p 5000:5000 --name my-flask-app flask-docker-app

# Or use docker-compose (recommended)
docker-compose up -d

# Check if it's running
docker ps
curl http://localhost:5000

Real-World Examples and Production Considerations

Let’s dive into some real scenarios you’ll encounter and how to handle them properly.

Multi-Environment Setup

Create separate Docker Compose files for different environments:

# docker-compose.dev.yml
version: '3.8'
services:
  flask-app:
    build: 
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=development
      - DEBUG=1
    volumes:
      - ./app:/app/app
      - ./logs:/app/logs

# docker-compose.prod.yml  
version: '3.8'
services:
  flask-app:
    build: .
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
    restart: always
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

Run with specific environment:

# Development
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

# Production
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Database Integration Example

Here’s a more complex setup with PostgreSQL:

# docker-compose.full.yml
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: flaskapp
      POSTGRES_USER: flaskuser
      POSTGRES_PASSWORD: secretpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  flask-app:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://flaskuser:secretpassword@db:5432/flaskapp
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - flask-app

volumes:
  postgres_data:

Performance Comparison Table

Deployment Method Startup Time Memory Usage Scaling Difficulty Isolation Portability
Direct Python ~2s 50-100MB Hard None Poor
Docker Container ~5s 150-300MB Easy Excellent Excellent
Virtual Machine ~30s 1GB+ Medium Complete Good

Common Gotchas and How to Fix Them

Problem: Permission denied errors

# Wrong way - running as root
USER root

# Right way - create and use non-root user
RUN adduser --disabled-password --gecos '' appuser
USER appuser

Problem: Slow builds due to pip reinstalls

# Wrong order - code changes invalidate pip cache
COPY . .
RUN pip install -r requirements.txt

# Right order - requirements copied first
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Problem: Large image sizes

# Before optimization: ~800MB
FROM python:3.11

# After optimization: ~200MB  
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

Monitoring and Logging Setup

Add monitoring capabilities to your setup:

# Add to docker-compose.yml
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Advanced Deployment Strategies

For production deployments on VPS or dedicated servers, you’ll want to consider these advanced patterns:

Blue-Green Deployment Script

#!/bin/bash
# deploy.sh - Zero-downtime deployment script

# Build new image with timestamp tag
TAG=$(date +%Y%m%d_%H%M%S)
docker build -t flask-app:$TAG .

# Start new container on different port
docker run -d -p 5001:5000 --name flask-app-$TAG flask-app:$TAG

# Health check new container
for i in {1..30}; do
    if curl -f http://localhost:5001/health; then
        echo "New container is healthy"
        break
    fi
    sleep 2
done

# Update nginx to point to new container
# Stop old container
OLD_CONTAINER=$(docker ps --filter "name=flask-app" --filter "status=running" --format "{{.Names}}" | grep -v $TAG)
if [ ! -z "$OLD_CONTAINER" ]; then
    docker stop $OLD_CONTAINER
    docker rm $OLD_CONTAINER
fi

echo "Deployment complete: $TAG"

Auto-scaling with Docker Swarm

# Initialize swarm mode
docker swarm init

# Deploy stack with auto-scaling
docker stack deploy -c docker-compose.prod.yml flask-stack

# Scale service based on load
docker service scale flask-stack_flask-app=5

Performance Tuning and Optimization

Here are some battle-tested optimizations that actually make a difference:

Multi-stage Builds for Smaller Images

# Dockerfile.optimized
FROM python:3.11-slim as builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim as runner

WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app/ ./app/

ENV PATH=/root/.local/bin:$PATH
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app.main:app"]

Nginx Configuration for Production

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream flask_app {
        server flask-app:5000;
    }

    server {
        listen 80;
        
        location / {
            proxy_pass http://flask_app;
            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_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }

        location /static/ {
            alias /app/static/;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
}

Security Best Practices

Security should never be an afterthought. Here’s how to lock down your containerized Flask app:

# Dockerfile.secure
FROM python:3.11-slim

# Security: Update packages and remove package manager
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y --no-install-recommends gcc && \
    rm -rf /var/lib/apt/lists/* && \
    apt-get purge -y --auto-remove

# Security: Create non-root user with specific UID
RUN groupadd -r flaskgroup && useradd -r -g flaskgroup -u 1001 flaskuser

WORKDIR /app

# Security: Set proper file permissions
COPY --chown=flaskuser:flaskgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=flaskuser:flaskgroup app/ ./app/

USER flaskuser

# Security: Don't run as root, limit resources
EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--max-requests", "1000", "app.main:app"]

Troubleshooting Common Issues

Let’s address the most frequent problems you’ll encounter:

Container Won’t Start

# Check container logs
docker logs container_name

# Common fixes:
# 1. Port already in use
sudo lsof -i :5000
sudo kill -9 PID

# 2. Permission issues
docker exec -it container_name ls -la /app

# 3. Missing dependencies
docker exec -it container_name pip list

Performance Issues

# Monitor container resources
docker stats

# Check container resource limits
docker inspect container_name | grep -A 10 "Memory"

# Optimize gunicorn workers
# Rule of thumb: (2 x CPU cores) + 1
docker run -e GUNICORN_WORKERS=5 your_image

Integration with CI/CD

Here’s a basic GitHub Actions workflow for automated deployment:

# .github/workflows/deploy.yml
name: Deploy Flask App

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Build Docker image
      run: docker build -t flask-app:${{ github.sha }} .
    
    - name: Deploy to server
      run: |
        echo "${{ secrets.SSH_KEY }}" > ssh_key
        chmod 600 ssh_key
        scp -i ssh_key -o StrictHostKeyChecking=no docker-compose.yml user@your-server:/opt/flask-app/
        ssh -i ssh_key -o StrictHostKeyChecking=no user@your-server "cd /opt/flask-app && docker-compose pull && docker-compose up -d"

When you’re ready to deploy this to production, you’ll need reliable hosting. For smaller applications, a VPS solution works perfectly and gives you full control over your Docker environment. For high-traffic applications requiring serious resources, consider a dedicated server that can handle multiple containerized applications with room to scale.

Monitoring and Observability

Production applications need proper monitoring. Here’s how to add observability to your Flask Docker setup:

# Add to your Flask app for metrics
from prometheus_client import Counter, Histogram, generate_latest
import time

REQUEST_COUNT = Counter('flask_requests_total', 'Total requests', ['method', 'endpoint'])
REQUEST_LATENCY = Histogram('flask_request_duration_seconds', 'Request latency')

@app.before_request
def before_request():
    request.start_time = time.time()

@app.after_request  
def after_request(response):
    REQUEST_COUNT.labels(method=request.method, endpoint=request.endpoint).inc()
    REQUEST_LATENCY.observe(time.time() - request.start_time)
    return response

@app.route('/metrics')
def metrics():
    return generate_latest()

Conclusion and Recommendations

Containerizing Flask applications with Docker on Ubuntu 24 is absolutely worth the initial learning curve. You get consistent deployments, easy scaling, and the ability to run your app anywhere Docker runs. The key benefits that make this approach superior include:

  • Consistency: Your app runs the same everywhere – no more “works on my machine” issues
  • Scalability: Horizontal scaling becomes trivial with container orchestration
  • Isolation: Dependencies and conflicts are contained within each container
  • Resource efficiency: Containers use fewer resources than VMs while providing similar isolation

Use this approach when you need reliable, scalable deployments that can grow with your application. It’s particularly valuable for microservices architectures, multi-environment deployments, and teams that need consistent development environments.

Start with the basic setup I’ve shown here, then gradually add complexity as your needs grow. Remember that containerization is just one piece of the puzzle – proper monitoring, security practices, and deployment automation are equally important for production success.

For further reading, check out the official Docker documentation at docs.docker.com and Flask’s deployment guide at flask.palletsprojects.com. The combination of these technologies opens up possibilities for everything from simple web APIs to complex distributed systems, and mastering this stack will serve you well in modern application development.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked