Lecture Overview
As a JavaScript developer, you're likely familiar with containerizing Node.js applications. In this session, we'll explore how Python container setups compare to Node.js, highlighting similarities and differences to help you transition smoothly. We'll cover environment setup, dependency management, development workflows, and production considerations for both ecosystems.
Base Image Selection
One of the first decisions when containerizing an application is choosing a base image. Let's compare the options available for Node.js and Python.
Node.js Base Images
Node.js official images on Docker Hub offer several variants:
- node:18 - Full image with build tools (~900MB)
- node:18-slim - Debian-based minimal image (~200MB)
- node:18-alpine - Alpine-based minimal image (~50MB)
Python Base Images
Python official images follow a similar pattern:
- python:3.9 - Full image with build tools (~900MB)
- python:3.9-slim - Debian-based minimal image (~150MB)
- python:3.9-alpine - Alpine-based minimal image (~45MB)
| Variant | Node.js | Python | Best Used For |
|---|---|---|---|
| Full | node:18 | python:3.9 | Complex build requirements, native extensions |
| Slim | node:18-slim | python:3.9-slim | Balance of size and compatibility |
| Alpine | node:18-alpine | python:3.9-alpine | Minimal size, simple applications |
Note for JS developers: Just like in the Node.js world where Alpine images can cause issues with native modules (like bcrypt or node-sass), Python Alpine images can have problems with certain packages that require compilation (like numpy or pandas). When in doubt, start with the slim variant.
Example Dockerfile Comparison
Here's a basic comparison of Dockerfiles for a simple web application:
Node.js Dockerfile:
FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Python Dockerfile:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Analogy: Think of base images like foundations for a house. Full images are like foundations with a full workshop and all possible tools included. Slim images include just the essential construction tools, while Alpine images are minimalist foundations with only the bare necessities. The choice depends on what you're building and what trade-offs you're willing to make between size and convenience.
Dependency Management
The most significant difference you'll notice when moving from Node.js to Python is how dependencies are managed.
Node.js Dependency Management
In Node.js, dependencies are managed using:
- package.json - Lists dependencies with version constraints
- package-lock.json or yarn.lock - Locks exact versions
- node_modules/ - Directory where dependencies are installed
Python Dependency Management
Python typically uses:
- requirements.txt - Lists packages with version constraints
- Pipfile and Pipfile.lock - Modern alternative using pipenv
- poetry.lock - Another modern alternative using Poetry
- Virtual environments - Isolated environments for dependencies
| Node.js | Python | Purpose |
|---|---|---|
| package.json | requirements.txt | Listing dependencies |
| package-lock.json | Pipfile.lock / poetry.lock | Locking exact versions |
| npm install | pip install -r requirements.txt | Installing dependencies |
| node_modules/ | venv/ or .venv/ (virtual environment) | Where dependencies are stored |
| npm scripts | No direct equivalent (often custom scripts) | Running common tasks |
Dockerized Dependency Installation
The approach to installing dependencies in containers is similar but with some key differences:
Node.js dependency installation in Dockerfile:
# Copy dependency files
COPY package*.json ./
# Install dependencies
RUN npm ci # For production (uses package-lock.json)
# or
RUN npm install # For development
Python dependency installation in Dockerfile:
# Copy dependency files
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
Best practice: In both Node.js and Python containers, copy only the dependency definition files first, before copying the rest of your code. This leverages Docker's layer caching to avoid reinstalling dependencies when only your application code changes.
Note for JS developers: Unlike Node.js, Python doesn't have a built-in concept of "development" vs "production" dependencies. You'll typically manage this with separate requirements files (e.g., requirements.txt and requirements-dev.txt) or by using tools like Pipenv or Poetry that support this distinction.
Application Structure and Workflow
The application structure and development workflow differ between Node.js and Python environments.
Application Entry Points
| Node.js | Python |
|---|---|
|
|
Development Servers
| Node.js | Python |
|---|---|
|
|
Hot Reloading in Containers
For development workflows, you'll want code changes to be reflected immediately in your containers:
Node.js development container:
# Dockerfile
FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
# Don't copy code - will be mounted
EXPOSE 3000
# Use nodemon for hot reloading
CMD ["npx", "nodemon", "server.js"]
Running with mounted code:
docker run -it --rm -v $(pwd):/app -p 3000:3000 node-app
Python development container:
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Don't copy code - will be mounted
EXPOSE 5000
# Flask development server with hot reload
CMD ["flask", "run", "--host=0.0.0.0"]
Running with mounted code:
docker run -it --rm -v $(pwd):/app -p 5000:5000 -e FLASK_APP=app.py -e FLASK_ENV=development python-app
Note for JS developers: Python doesn't have a direct equivalent to npm scripts. While you might be used to defining scripts in package.json, Python projects typically use Makefiles, shell scripts, or tools like invoke or tox for task automation.
Environment Variables and Configuration
Both Node.js and Python applications commonly use environment variables for configuration, especially in containerized environments.
Environment Variables Usage
Node.js environment variables:
// Accessing environment variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const nodeEnv = process.env.NODE_ENV || 'development';
Python environment variables:
import os
# Accessing environment variables
port = os.environ.get('PORT', 5000)
db_url = os.environ.get('DATABASE_URL')
flask_env = os.environ.get('FLASK_ENV', 'development')
Environment Variables in Dockerfile
Setting environment variables in Dockerfiles is identical for both:
# Setting environment variables
ENV NODE_ENV=production PORT=3000
# or
ENV FLASK_ENV=production PORT=5000
Configuration Libraries Comparison
| Node.js | Python | Purpose |
|---|---|---|
| dotenv | python-dotenv | Loading .env files |
| config | configparser | Hierarchical configuration |
| convict | pydantic | Configuration validation |
Example: Using dotenv in both ecosystems
Node.js dotenv:
// Load environment variables from .env file
require('dotenv').config();
const port = process.env.PORT || 3000;
Python dotenv:
from dotenv import load_dotenv
import os
# Load environment variables from .env file
load_dotenv()
port = os.environ.get('PORT', 5000)
Note for JS developers: Both ecosystems follow similar patterns for environment-based configuration, making this aspect of the transition relatively smooth. The main difference is how variables are accessed (process.env in Node.js vs os.environ in Python).
Multi-Stage Builds
Multi-stage builds are a powerful Docker feature used in both Node.js and Python applications to create smaller, more secure production images.
Node.js Multi-Stage Build
# Build stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# For applications with a build step
RUN npm run build
# Production stage
FROM node:18-slim
WORKDIR /app
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
Python Multi-Stage Build
# Build stage
FROM python:3.9 AS build
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Production stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=build /app/wheels /wheels
COPY --from=build /app/requirements.txt .
RUN pip install --no-cache --no-index --find-links=/wheels -r requirements.txt && \
rm -rf /wheels
COPY . .
EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
What's happening in the Python example:
- The first stage uses the full Python image to build wheel files (pre-built packages)
- The second stage uses the slim Python image and copies only the wheel files
- Dependencies are installed from the wheels rather than from PyPI
- Application code is copied to the production image
- The application is run using Gunicorn (a production WSGI server)
Note for JS developers: While the details differ, the multi-stage build pattern is conceptually identical between Node.js and Python. The key difference is what constitutes the "build" step—in Node.js it's often compiling TypeScript or bundling for production, while in Python it might involve pre-building wheels or compiling C extensions.
Production Server Considerations
A key difference between Node.js and Python in containerized environments is how production servers are typically set up.
Node.js Production Servers
In Node.js applications:
- Often the Node.js process itself serves requests (using Express, Fastify, etc.)
- Process managers like PM2 might be used for resilience
- Clustering is available via the cluster module or PM2
- A reverse proxy (NGINX) is optional but common
Python Production Servers
Python web applications typically use a multi-component setup:
- WSGI/ASGI server (Gunicorn, uWSGI, Uvicorn) runs the Python application
- Multiple worker processes handle requests concurrently
- A reverse proxy (NGINX) is almost always used in production
| Component | Node.js Example | Python Example |
|---|---|---|
| Application Server | Node.js with Express | Flask/Django with Gunicorn |
| Concurrency Model | Event loop (single-threaded) | Multiple worker processes |
| Process Management | PM2 or Docker restart policies | Gunicorn master or Docker restart policies |
| Typical Dockerfile CMD | CMD ["node", "server.js"] |
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"] |
Example: Production-ready Python container with Gunicorn
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn
COPY . .
EXPOSE 5000
# Run with 4 worker processes
CMD ["gunicorn", "--workers=4", "--bind=0.0.0.0:5000", "app:app"]
Note for JS developers: The biggest adjustment when moving from Node.js to Python for web applications is understanding that the development server (like Flask's built-in server) is never used in production. Instead, a dedicated WSGI/ASGI server runs your application. This is conceptually similar to how you might use nodemon in development but Node.js directly in production.
Development Workflows: Side-by-Side Comparison
Let's compare complete development workflows for containerized Node.js and Python applications.
Node.js Express Application
Project structure:
.
├── Dockerfile
├── docker-compose.yml
├── package.json
├── package-lock.json
├── src/
│ ├── server.js
│ ├── routes/
│ └── controllers/
└── .dockerignore
package.json:
{
"name": "node-app",
"version": "1.0.0",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}
Dockerfile:
FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
docker-compose.yml:
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./src:/app/src
environment:
- NODE_ENV=development
Python Flask Application
Project structure:
.
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── app.py
├── templates/
├── static/
└── .dockerignore
requirements.txt:
flask==2.0.1
python-dotenv==0.19.0
Dockerfile:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
ENV FLASK_APP=app.py
ENV FLASK_ENV=development
CMD ["flask", "run", "--host=0.0.0.0"]
docker-compose.yml:
version: '3'
services:
app:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
Best practice: Use Docker Compose for development in both ecosystems. It simplifies managing volumes for hot reloading, environment variables, and connections to other services like databases.
Handling Database Connections
Connecting to databases from containerized applications follows similar patterns in both ecosystems.
Connecting to Databases
Node.js with PostgreSQL:
// Using node-postgres
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres'
});
async function query(text, params) {
const result = await pool.query(text, params);
return result.rows;
}
Python with PostgreSQL:
# Using psycopg2
import os
import psycopg2
def get_db_connection():
return psycopg2.connect(
host=os.environ.get('DB_HOST', 'localhost'),
port=os.environ.get('DB_PORT', 5432),
database=os.environ.get('DB_NAME', 'myapp'),
user=os.environ.get('DB_USER', 'postgres'),
password=os.environ.get('DB_PASSWORD', 'postgres')
)
def query(text, params=None):
conn = get_db_connection()
cur = conn.cursor()
cur.execute(text, params or ())
rows = cur.fetchall()
cur.close()
conn.close()
return rows
Using Docker Compose for Local Development with Databases
The Docker Compose setup is nearly identical for both ecosystems:
version: '3'
services:
app:
build: .
ports:
- "5000:5000" # or 3000:3000 for Node.js
volumes:
- .:/app
environment:
- DB_HOST=db
- DB_USER=postgres
- DB_PASSWORD=postgres
- DB_NAME=myapp
depends_on:
- db
db:
image: postgres:13-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Note for JS developers: While the database connection code looks different, the patterns are similar - both use connection pools, parameterized queries, and environment variables for configuration. The key difference is that Python doesn't have as mature an ecosystem of ORMs comparable to Sequelize or TypeORM (though SQLAlchemy and Django ORM are powerful alternatives).
Common Pitfalls and Solutions
When transitioning from Node.js to Python containers, be aware of these common issues:
File Permissions
Python applications may handle file permissions differently than Node.js applications:
Pitfall: Files created inside a Python container might be owned by root, causing permission issues when mounted to the host.
Solution: Create a non-root user in your Dockerfile and set appropriate permissions:
# Create a non-root user
RUN adduser --disabled-password --gecos "" appuser
USER appuser
# Or when working with specific directories
RUN mkdir -p /app/data && chown -R appuser:appuser /app/data
PYTHONPATH and Import Issues
Python's import system can be confusing for JavaScript developers:
Pitfall: Python imports might not work the same way in containers as they do locally.
Solution: Set the PYTHONPATH environment variable or use proper package structure:
# In Dockerfile
ENV PYTHONPATH=/app
# Or in docker-compose.yml
environment:
- PYTHONPATH=/app
Hot Reloading Differences
Hot reloading works differently in Python:
Pitfall: Python development servers may not detect all file changes.
Solution: For Flask, ensure FLASK_ENV=development is set. For other frameworks, you might need specific tools:
# For Flask
ENV FLASK_ENV=development
# For Django
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# For FastAPI with uvicorn
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
Process Management
Python web servers handle processes differently than Node.js:
Pitfall: Running Python web applications directly without proper process management.
Solution: Use appropriate WSGI/ASGI servers:
# For Flask/Django
CMD ["gunicorn", "--workers=4", "--bind=0.0.0.0:5000", "app:app"]
# For ASGI applications (FastAPI, Starlette)
CMD ["uvicorn", "main:app", "--workers", "4", "--host", "0.0.0.0"]
Complete Examples: Side by Side
Let's look at complete, production-ready setups for both ecosystems.
Node.js Express Application
Dockerfile:
# Build stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Production stage
FROM node:18-slim
WORKDIR /app
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/src ./src
# Create a non-root user
RUN adduser --disabled-password --gecos "" nodejs
USER nodejs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD node -e "const http = require('http'); const options = { hostname: 'localhost', port: 3000, path: '/health', timeout: 2000 }; const req = http.get(options, (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.end()"
# Production command
CMD ["node", "src/server.js"]
docker-compose.production.yml:
version: '3'
services:
app:
image: my-node-app:1.0
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
db:
image: postgres:13-alpine
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=production_password
volumes:
postgres_data:
Python Flask Application
Dockerfile:
# Build stage
FROM python:3.9 AS build
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Production stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=build /app/wheels /wheels
COPY --from=build /app/requirements.txt .
RUN pip install --no-cache --no-index --find-links=/wheels -r requirements.txt && \
rm -rf /wheels
COPY . .
# Create a non-root user
RUN adduser --disabled-password --gecos "" pyuser
USER pyuser
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD curl --fail http://localhost:5000/health || exit 1
# Production command with Gunicorn
CMD ["gunicorn", "--workers=4", "--bind=0.0.0.0:5000", "app:app"]
docker-compose.production.yml:
version: '3'
services:
app:
image: my-flask-app:1.0
restart: always
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- DB_HOST=db
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:5000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
db:
image: postgres:13-alpine
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=production_password
volumes:
postgres_data:
Best practice: Regardless of whether you're using Node.js or Python, the core Docker best practices remain the same:
- Use multi-stage builds for smaller production images
- Run as a non-root user for security
- Include health checks for monitoring container health
- Use appropriate process management for production
- Set proper restart policies for resilience
Transition Guide for JavaScript Developers
Here are some tips to help you transition from Node.js to Python containers:
Conceptual Mapping
| Node.js Concept | Python Equivalent |
|---|---|
| npm/yarn | pip, pipenv, or poetry |
| package.json | requirements.txt or setup.py |
| Express | Flask or Django |
| Nodemon | Flask debug mode or watchdog |
| Jest/Mocha | pytest or unittest |
| ESLint | pylint or flake8 |
| PM2 | Gunicorn or Supervisor |
| Sequelize/TypeORM | SQLAlchemy or Django ORM |
Recommended Approach
- Start with Flask: It's more Express-like than Django and easier to learn
- Use Docker Compose for development: It simplifies environment setup
- Begin with simple applications: Avoid complex dependencies initially
- Learn Python's package structure: It's different from Node.js's module system
- Start with requirements.txt: Before moving to more complex tools like Poetry
Key Python Libraries for Node.js Developers
- Flask/FastAPI: Express-like web frameworks
- SQLAlchemy: Powerful ORM (like Sequelize)
- Marshmallow/Pydantic: Schema validation (like Joi/zod)
- pytest: Testing framework (like Jest)
- requests: HTTP client (like axios/node-fetch)
- celery: Task queue (like Bull)
Key Takeaways
- Container fundamentals are the same between Node.js and Python, making the transition easier
- Python uses requirements.txt instead of package.json for dependency management
- Development servers in Python need explicit configuration for hot reloading
- Production Python web applications use WSGI/ASGI servers (like Gunicorn) rather than running directly
- Docker Compose workflows are nearly identical between the ecosystems
- Multi-stage builds work similarly but with different optimization targets
- Python applications often follow a more explicit pattern for database connections
- Both ecosystems share best practices for container security and optimization
With this knowledge, you're well-equipped to transfer your Node.js container expertise to Python projects!
Looking Ahead
In our next session, we'll explore Docker Compose in more detail and see how it can help manage complex application setups with multiple services. We'll build on the knowledge from this comparison to develop a multi-container application using Python.
Discussion Questions
- How does Python's approach to dependency management compare to Node.js's? What advantages or disadvantages do you see in each approach?
- Why might Python's production server setup (with WSGI/ASGI servers) be beneficial compared to Node.js's direct execution model?
- How would you adapt your existing Node.js containerization practices when working with Python applications?
- What challenges might a team face when maintaining both Node.js and Python containers in the same project?
- How do the different concurrency models (Node.js event loop vs. Python's process-based concurrency) affect container configuration and resource allocation?
Additional Resources
- Docker Python Language Guide - Official Docker documentation for Python
- Docker Node.js Language Guide - Official Docker documentation for Node.js
- Flask Deployment Options - Guide to deploying Flask applications
- Python in Docker Production Guide - Best practices for Python containers
- Node.js with Docker - Guide to Docker for Node.js developers