Skip to main content

Command Palette

Search for a command to run...

⚙️ Production Ready Docker Aplications: Mini Projects - A 🐳🚀

Deploying a Single-Service Container (Monolithic Setup)

Updated
5 min read
⚙️ Production Ready Docker Aplications: Mini Projects - A 🐳🚀
D

DevOps/Cloud Engineer focused on building, automating, and scaling cloud native systems. I write about Linux, CI/CD pipelines, Docker, Terraform, Kubernetes, and AWS, sharing practical insights from real-world DevOps workflows.

Project A: Single-Service Production Container (Python App)

Goal: Build a production ready containerized service using Docker best practices: secure, optimized, observable, and resilient.

Project Structure

Why this matters
• Clean separation of concerns
• No secrets committed
• Easy CI/CD integration later

Step1: Create a Python Service

(app/main.py) (app/requirements.txt)

app/main.py

from http.server import BaseHTTPRequestHandler, HTTPServer
import os

class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(404)
self.end_headers()

if name == "main":
port = int(os.getenv("APP_PORT", 8080))
server = HTTPServer(("0.0.0.0", port), HealthHandler)
print("Starting server on port", port)
server.serve_forever()

app/requirements.txt

No external deps yet (kept minimal intentionally)

**Production Insight:
**• Logs go to stdout/stderr
• Health endpoint included for orchestration readiness

Step 2: Secrets Handling (No Hardcoding)

.env.example

APP_PORT=8080
APP_ENV=production

Note: Never commit real .env files. Only .env.example goes to Git.

Step 3: .dockerignore (Security + Performance)

.dockerignore

.git
.gitignore
pycache/
.env
.env.*
*.log
node_modules

Why this is critical:
• Prevents secrets leaking into images
• Reduces build context size
• Smaller → safer → faster images

Step 4: Multi-Stage Production Dockerfile

Dockerfile

#Build Stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY app/requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

#Runtime Stage
FROM python:3.12-slim

#Security: create non-root user
RUN useradd -m appuser
WORKDIR /app

#Copy only what we need
COPY --from=builder /install /usr/local
COPY app/ .

#Environment
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8080

#Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --retries=3
CMD curl -f http://localhost:8080/health || exit 1

#Drop privileges
USER appuser

EXPOSE 8080
CMD ["python", "main.py"]

Security (Shift-Left)

• Multi-stage build
• Minimal base image (slim)
• Non-root user
• No secrets in image
• No unnecessary packages

Security is not an afterthought, it is shift-left and baked into every Dockerfile.

Step 5: Build the Image

docker build -t python-prod:1.0 .

While building the image we received error:
ERROR:
failed to build: failed to solve: failed to compute cache key: failed to calculate checksum of ref 4f55ec3b-efa6-49d1-914e-357d8a02e471::k4khh8rjb1cm1wa92ayu97qar: "/install": not found
this is a classic multi-stage Dockerfile issue

The Problem (Why the build failed)
COPY --from=builder /install /usr/local
"/install": not found

What Happened?
• In the builder stage, /install is only created if pip installs something
• Your requirements.txt is empty
• So pip install ... --prefix=/install does not create /install
• When the runtime stage tries to copy it → Docker fails

Solution: Add a Dependency, Update app/requirements.txt
gunicorn==21.2.0

Now rebuild

docker build -t python-prod:1.0 .

Check image size

docker images python-prod

Run with Production Controls

Run it:

chmod +x docker-run.sh
./docker-run.sh

Why we are getting application status as “Unhealthy“, Nothing is “broken” in our app. The healthcheck is failing,

What’s Actually Wrong (Root Cause)
Our container is running: Starting server on port 8080

But Docker reports: Up 2 minutes (unhealthy)

Why?
• Dockerfile has this healthcheck:
HEALTHCHECK --interval=30s --timeout=3s --retries=3
CMD curl -f http://localhost:8080/heath || exit 1

However, python:3.12-slim does NOT include curl

So Docker tries to run: curl http://localhost:8080/health
And inside the container: /bin/sh: curl: not found

That makes the container unhealthy, even though the app itself is working.

Minimal images:
• Smaller attack surface
• Missing common tools (curl, wget, bash)

Solution: Use Python for Healthcheck (No extra packages. No bloat)

Update the /app/healthcheck.py

import urllib.request
import sys
URL = "http://127.0.0.1:8080/health"
try:
with urllib.request.urlopen(URL, timeout=3) as response:
if response.status == 200:
sys.exit(0)
else:
sys.exit(1)
except Exception as e:
sys.exit(1)

Why container was unhealthy earlier

• Healthcheck ran before server fully started
localhost DNS resolution glitch → fixed by 127.0.0.1
• Duplicate COPY confused file layout
• Short start-period

Step 6: Rebuild Again (New build with 1.1 tag

(Best practice: always stop the container first and then remove it)

docker stop python-prod
docker rm -f python-prod

#Build and Run
docker build -t python-prod:1.1 .
docker run -d --name python-prod --env-file .env --memory="256m" --cpus="0.5" --restart unless-stopped -p8080:8080 python-prod:1.1

verify the status
docker inspect --format='{{.State.Health.Status}}' python-prod

Access the site on browser: http://<server_ip:8080> & http://<server_ip:8080/health>

Tip: Even though the application was running correctly, the container was marked unhealthy. The issue wasn’t the app, it was the healthcheck relying on a binary (curl) that doesn’t exist in minimal images. This reinforced an important production lesson: healthchecks must match the base image, not assumptions.

Note: Minimal images improve security, but require deliberate operational design, more secure, smaller → but missing common tools like curl.

Step: 8 Restart Policy

Step 7: Git Repository Setup

Note: .dockerignore has NOTHING to do with Git.

Why .env is still going to Git:
• .dockerignore → only affects Docker build context
• .gitignore → controls what Git tracks and commits

So if .env is excluded in .dockerignore but not in .gitignore, Git will still track it.

Tip: Add .env to .gitignore

Key Takeaway from this Project

• Minimal Python image
• Non-root user
• Proper healthcheck
• Clean production-ready container