To containerise a Python backend, write a small Dockerfile that installs pinned dependencies and starts the server on 0.0.0.0, then add a Docker Compose file that runs the API and a database together with a named volume and a health check. Once that works, anyone can start your project with one command, which is what reviewers, teammates and internship mentors want to see. This guide builds an illustrative FastAPI and PostgreSQL project step by step, gives you a troubleshooting table for the errors that appear most often, and ends with a 14-day plan.
What you will have at the end of this Docker tutorial
- A Python API image built from a Dockerfile that you can explain line by line.
- A
docker-compose.ymlthat starts the API and PostgreSQL, waits for the database to be ready, and keeps data between restarts. - A
.envfile pattern so passwords never enter your Git history. - A troubleshooting table you can keep open while debugging.
All code below is illustrative: it shows the shape of a working setup, and you should run and adjust it on your own machine. Install Docker Desktop (or Docker Engine plus the Compose plugin on Linux) first and confirm that docker --version and docker compose version both print a version.
Project layout for a containerised FastAPI and PostgreSQL service
notes-api/
app/
__init__.py
main.py
requirements.txt
Dockerfile
docker-compose.yml
.dockerignore
.env (never commit this)
.env.example (commit this)
Keep the application in an app package. The Dockerfile will copy only what the container needs, and a clear layout makes the CMD line easy to write.
Free 2-Week CareerSwami Internship
Online, 14 days, with a free offer letter and a free completion certificate once you finish the work. Check with your college that it fits your requirement before you enrol.
Step 1: a minimal API that proves the database connection works
A container that starts but cannot talk to its database is a common half-success. Give the API two endpoints: one that always answers, and one that touches the database.
import os
from fastapi import FastAPI, HTTPException
import psycopg
app = FastAPI()
DATABASE_URL = os.environ["DATABASE_URL"]
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/db-check")
def db_check():
try:
with psycopg.connect(DATABASE_URL, connect_timeout=3) as conn:
row = conn.execute("SELECT 1").fetchone()
return {"db": row[0]}
except Exception as exc:
raise HTTPException(status_code=503, detail=str(exc))
Reading DATABASE_URL from the environment is the key design choice. The same code then runs on your laptop, in Compose and on a server, with only the variable changing.
Step 2: pin dependencies and write a .dockerignore
Create requirements.txt. Pin versions you have actually tested; the lines below are placeholders to show the format.
fastapi==0.115.*
uvicorn[standard]==0.30.*
psycopg[binary]==3.2.*
Then create .dockerignore so the build context stays small and secrets stay out of the image:
.git
.env
__pycache__/
*.pyc
.venv/
venv/
.pytest_cache/
If you skip this file, a local virtual environment can add hundreds of megabytes to every build, and your .env could be copied into an image you later share.
Step 3: the Dockerfile, explained line by line
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
| Line | What it does | Why it matters |
|---|---|---|
FROM python:3.12-slim |
Starts from a small official Python image | Smaller downloads than the full image; pick the Python version your code targets |
PYTHONUNBUFFERED=1 |
Sends logs straight to the terminal | Without it, docker compose logs can appear empty or delayed |
COPY requirements.txt . before app code |
Copies the dependency list alone first | Docker caches the install layer, so editing code does not reinstall packages |
USER appuser |
Runs the process without root rights | Limits damage if the app is compromised |
EXPOSE 8000 |
Documents the port | It does not publish the port; that happens in Compose or with -p |
--host 0.0.0.0 |
Listens on all interfaces inside the container | The default 127.0.0.1 makes the app unreachable from your browser |
Build and test the image alone before adding Compose:
docker build -t notes-api:dev .
docker run --rm -p 8000:8000 -e DATABASE_URL=postgresql://x:y@localhost:5432/z notes-api:dev
Opening http://localhost:8000/health should return the status message. The /db-check endpoint will fail here, which is expected because no database exists yet.
Step 4: a Compose file that runs the API and PostgreSQL together
Create .env.example and copy it to .env:
POSTGRES_USER=notes
POSTGRES_PASSWORD=change-me-locally
POSTGRES_DB=notesdb
Now the docker-compose.yml:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
Why the database host is “db” and not “localhost”
Compose puts both services on a private network where each service name works as a hostname. Inside the API container, localhost means the API container itself, so a connection string pointing there will be refused. Using db is the single most important detail in this file.
Why a health check, not just depends_on
A plain depends_on only waits for the database container to start, not for PostgreSQL to accept connections. The health check plus service_healthy closes that gap. The named volume pgdata keeps your tables when you stop and restart the stack.
Step 5: commands to start, inspect and reset the stack
docker compose up --buildbuilds the image and starts both services in the foreground.docker compose psshows state; the database should read healthy.- Open
/health, then/db-check, in your browser or with curl. docker compose logs -f apifollows API logs.docker compose downstops everything and keeps data;docker compose down -valso deletes the volume, which is what you want after changing database credentials.
Troubleshooting table: common Docker build, port and connection errors
| Symptom or message | Likely cause | Fix |
|---|---|---|
port is already allocated or address already in use |
Something on your machine already uses host port 8000 or 5432 | Stop the other process, or change the left side of the mapping, e.g. "8001:8000" |
| Browser shows “connection reset” or empty reply on localhost:8000 | App bound to 127.0.0.1 inside the container | Start uvicorn with --host 0.0.0.0 |
connection refused to localhost:5432 from the API |
Connection string uses localhost | Use the service name db as host |
| API crashes at start with connection errors, then works on restart | Database not ready yet | Add the health check and condition: service_healthy |
password authentication failed |
Volume was created with older credentials; Postgres ignores new ones on an existing volume | Run docker compose down -v, then up again (this deletes data) |
KeyError: 'DATABASE_URL' |
Variable missing from the service’s environment | Check the environment block and that .env sits next to the compose file |
ModuleNotFoundError inside the container |
Package missing from requirements.txt or wrong import path | Add the package, rebuild with docker compose build --no-cache api; confirm app/__init__.py exists |
COPY failed: file not found in build context |
Path wrong or excluded by .dockerignore | Run the build from the project root and check .dockerignore |
Cannot connect to the Docker daemon |
Docker Desktop not running, or your Linux user lacks permission | Start Docker Desktop; on Linux add your user to the docker group and log in again |
| Build is very slow on every code change | Dependencies copied after app code, or a huge build context | Keep the requirements-first order and fill in .dockerignore |
| Container exits immediately with code 0 or 1 | Wrong CMD or a crash on import |
Run docker compose logs api; test the module path app.main:app |
| Code edits do not appear | Code is baked into the image | Rebuild, or for development mount ./app:/app/app and add --reload to uvicorn |
A worked example: tracing a “connection refused” failure (illustrative)
Imagine a student starts the stack and the API logs show a refused connection to localhost:5432, even though docker compose ps lists the database as healthy. The reasoning goes like this:
- The database is healthy, so the problem is not startup order.
- The error names
localhost, which inside the API container is the API itself. - The student opens
docker-compose.ymland findsDATABASE_URLending in@localhost:5432/notesdb, left over from local development. - They change the host to
db, rundocker compose up --build, and/db-checkshould now return{"db": 1}.
The habit worth copying is reading the exact hostname and port in the error before changing anything.
Containerising a backend as a Digital University Kerala student: internship and project-work notes
The university’s About page places the campus at Technopark Phase IV, Thonnakkal, Thiruvananthapuram, and describes a mission built around computing, intelligence, sustainability and entrepreneurship. A containerised backend portfolio piece fits that computing focus. The MSc in Data Science and Fintech programme page lists Python, databases and cloud computing among its foundations and tools, and that is exactly the stack this tutorial exercises. Check your own programme’s current course list before assuming a topic is assessed.
If you plan to pair this project with an internship, the rules matter. An excerpt of the DUK Internship Policy, labelled 2022-2023, says a minimum of 42 hours is needed per credit and that one week of full-time engagement counts as one credit or 42 hours. It also says internships can happen during or between semesters, that credit is not given retroactively, and that the provider must give an internship certificate on completion. Because the excerpt is dated, and may not cover online internships, treat these points as a starting guide and read the current full document.
The same excerpt mentions a template agreement for a Student Internship Program, covering students approved and referred by the Chair of the Internship Committee. The Placement and Career Guidance Cell page says the Cell works with companies, startups and research institutions on internships and live projects, so it is a sensible first contact.
The free two-week CareerSwami internship is online, runs for 14 days, and comes with a free offer letter and a completion certificate after successful completion. It is an independent programme, and nothing here suggests the university recognises it or awards credit for it. Whether any outside internship counts is a decision for your department, so ask before you start.
Verification steps for DUK students before relying on an internship
- Download the current Internship Policy from the university website and check its year, since the excerpt above is labelled 2022-2023.
- Email the Chair of the Internship Committee asking whether an online, 14-day internship can count toward credit, and keep the reply.
- Ask in writing what documents are needed: an agreement, an offer letter, a completion certificate, or a report.
- Ask your programme coordinator which semester or break the internship should fall in, and where your programme sets credits and evaluation.
- Contact the Placement and Career Guidance Cell and ask for its current internship procedure and any documents it expects from students.
A 14-day plan for containerising and documenting your backend
| Days | Goal | Output |
|---|---|---|
| 1-2 | Install Docker, build the minimal API, run it locally | Working /health endpoint |
| 3-4 | Write requirements, .dockerignore and Dockerfile | Image builds and runs alone |
| 5-6 | Add PostgreSQL through Compose with a health check and volume | Both services start with one command |
| 7-8 | Add a real table and CRUD endpoints | Data survives docker compose down |
| 9-10 | Move secrets to .env, add .env.example, trigger and fix three errors from the table |
Short written troubleshooting log |
| 11-12 | Add tests and a development override with a code mount and reload | Test command documented in the README |
| 13 | Write the README: prerequisites, run steps, ports, reset command | A stranger can run it in five minutes |
| 14 | Clean-clone test and final polish | Repository ready to share |
Pre-submission checklist for a Dockerised Python project
- Fresh clone, copy
.env.exampleto.env, rundocker compose up --build: does it work? - Is
.envexcluded from Git and from the image? - Are dependencies pinned to versions you tested?
- Does the container run as a non-root user?
- Does the README list ports, endpoints and the reset command?
- Can you explain why the database host is
dband why the health check exists?
Frequently Asked Questions
Do I need to learn Docker before learning a Python framework?
No. Build a small working API first, then containerise it. Docker packages an app that already runs, so a framework basics course should come first.
Why does my API work locally but fail inside Docker?
The usual causes are binding to 127.0.0.1 instead of 0.0.0.0, a database host set to localhost, and environment variables that exist on your machine but not in the container. Check those three before anything else.
Should I use a slim image or Alpine for Python?
Beginners usually have fewer problems with the slim Debian-based image. Alpine uses a different C library, and some packages with compiled parts need extra build steps there.
Is it safe to put the database password in docker-compose.yml?
Only for throwaway local values. Keep real values in an untracked .env file, commit a .env.example with placeholders, and use proper secret handling on any shared server.
What is the difference between EXPOSE and ports?
EXPOSE only documents the port the app uses. The ports entry in Compose (or -p in docker run) publishes it to your machine, which is what makes the browser able to reach it.
How do I reset the database without reinstalling anything?
Run docker compose down -v to remove the containers and the named volume, then docker compose up. This permanently deletes the stored data, so export anything you need first.
Will a Docker project help me in backend internship applications?
It shows that your work runs on someone else’s machine, which is useful evidence. It does not guarantee selection anywhere, so pair it with a clear README and a short explanation of your design choices.
Can a short online internship count toward my university credit?
That depends on your university’s current rules and on approval from the relevant committee. For DUK students, the policy excerpt above says credit is not retroactive, so ask in writing before the internship begins.
Related Python & Backend Engineering guides
- Indira Gandhi Institute of Development Research Students: Serving Economic Datasets Through a Python API
- Vidhyadeep University Students: Reading Database Query Plans in Your Backend Internship
- Sri Balaji University Students: Backend Internship Project Management With a Simple Kanban Board
- All Python & Backend Engineering internship guides
Ready to put this into practice?
Join the free two-week CareerSwami online internship in Python & Backend Engineering. The internship, the offer letter and the completion certificate are free.










