How to Write a Production-Ready Dockerfile

A production-ready Dockerfile starts with a slim base image, never runs as root, and exposes exactly one port. These three habits - slim base, dedicated user, explicit port - are the difference between a container that ships and one that gets flagged in a security review.

DevOps Engineerdockerdockerfilepython

Start with a slim base image

python:3.12-slim gives you a working Python 3.12 runtime without the full Debian toolchain. Fewer packages means a smaller attack surface and a shorter CVE list for your security scanner to complain about.

FROM python:3.12-slim
WORKDIR /app
COPY app.py .

Set WORKDIR before any COPY or RUN steps so all subsequent commands resolve relative to that directory and the layer cache stays predictable.

Never run as root

Containers run as root by default. A container escape hands the attacker root on the host. Create a dedicated user and switch to it before the final CMD:

RUN adduser --disabled-password --gecos "" appuser
USER appuser

--disabled-password prevents interactive login; --gecos "" skips the name/room prompts. The app code runs as appuser - if an attacker escapes the process, they land as an unprivileged user, not root.

Expose the port and start the app

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

EXPOSE is documentation - it tells docker run (and orchestrators like Kubernetes) which port the container listens on. Use CMD as a list (exec form), not a string (shell form), so signals like SIGTERM reach the Python process directly instead of being swallowed by a shell.

The complete Dockerfile

FROM python:3.12-slim

WORKDIR /app
COPY app.py .

RUN adduser --disabled-password --gecos "" appuser
USER appuser

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

Build, run, and verify:

docker build -t myapp .
docker run -p 8080:8080 myapp
curl http://localhost:8080/health
# {"status": "ok"}

What to add next in production

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why use python:3.12-slim instead of python:3.12?

The slim variant strips the full Debian toolchain, compilers, and headers, resulting in a much smaller image. Fewer installed packages means a smaller attack surface and fewer vulnerabilities for scanners to flag.

How do I run a Docker container as a non-root user?

Create a dedicated system user with RUN adduser --disabled-password --gecos '' appuser, then switch to it with USER appuser before the CMD instruction. The app process runs as that user for the lifetime of the container.

What is the difference between CMD and ENTRYPOINT in a Dockerfile?

ENTRYPOINT sets the fixed executable; CMD provides default arguments that can be overridden at docker run. For a simple app, CMD alone is fine. Use the exec form (a JSON array) so the process receives signals directly instead of through a shell.

Keep learning

Fix the Nginx 502 Bad GatewayDevOps projectRecover a Crashed Linux ServiceDevOps projectCorrect a Postgres Port MismatchDevOps projectDevOps roadmapStep by step to hiredDevOps interview questionsSTAR answersAll DevOps projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →