
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.