LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 1415 min read

Docker in Production: Security and Best Practices

Make images and containers production-ready: a non-root user, a real web server, health checks, resource limits, read-only filesystems, log limits and a deployment checklist.

On this page 10 sections

Introduction

A container that works on your laptop is not automatically ready for real users. Production adds new requirements: security, reliability, predictable resource use and logs that do not fill the disk. This final lesson upgrades Priya’s Quotes API to meet them.

What You Will Learn
  • How to write a production Dockerfile with a non-root user, a real web server and a HEALTHCHECK.
  • How to run containers with restart policies, resource limits, a read-only filesystem and log limits.
  • The image habits that keep production safe and predictable.
  • How a typical single-server deployment works, with a checklist to follow.

A Production Dockerfile

Flask’s built-in server is for development only. Priya adds gunicorn, a production-grade Python web server, to requirements.txt:

requirements.txt
flask==3.1.3
gunicorn==26.2.0
Dockerfile
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Run as an unprivileged user instead of root
RUN useradd --create-home --uid 10001 appuser
USER appuser
EXPOSE 5000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1
# A production web server instead of Flask's development server
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
LineWhy
PYTHONDONTWRITEBYTECODE=1Do not write .pyc files at run time
PYTHONUNBUFFERED=1Send output straight to docker logs instead of buffering it
useradd … / USER appuserRun the app as an unprivileged user (see below)
HEALTHCHECKDocker checks every 10 seconds that the app answers; start-period gives it time to boot
gunicorn … --workers 2 app:appA production server with two worker processes, serving the app object from app.py
docker build -t quotes-api:2.0 .

Do Not Run as Root

By default a container’s processes run as root. Compare the old and the new image:

docker run --rm quotes-api:1.0 whoami # root
docker run --rm quotes-api:2.0 whoami # appuser
docker run --rm quotes-api:2.0 id # uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)

Containers are isolated, but not perfectly. If an attacker breaks into an app running as root, any weakness in that isolation gives them root-level power. As an ordinary user they can do far less. It costs two lines in the Dockerfile — add them to every image you build.

Running It Safely

docker run -d --name api \
-p 5000:5000 \
--restart unless-stopped \
--memory 256m --cpus 0.5 \
--read-only --tmpfs /tmp \
--log-opt max-size=10m --log-opt max-file=3 \
quotes-api:2.0
OptionEffect
--restart unless-stoppedRestart after crashes and server reboots
--memory 256mHard memory limit; the process is killed (exit 137) if it goes over, instead of starving the server
--cpus 0.5At most half of one CPU core
--read-onlyThe container’s filesystem cannot be modified, so an attacker cannot plant files
--tmpfs /tmpA small in-memory folder for the temporary files gunicorn needs
--log-opt max-size / max-fileRotate logs: at most three files of 10 MB, so logs can never fill the disk

Check the result: docker logs api now shows gunicorn starting two workers (and no development-server warning), docker ps shows (healthy) after the first check, docker stats --no-stream api shows the 256 MiB memory limit, and any attempt to write a file in /app fails with “Read-only file system”.

Mini Challenge · Spot What's Missing

A container on Priya’s server keeps writing large logs and filled the disk last month. Which option was missing from its docker run command?

Choose one answer

Image Best Practices

  • Pin base images to a version (python:3.14-slim, not python:latest), and rebuild regularly to pick up security fixes.
  • Use small bases (slim, alpine, distroless) and multi-stage builds, so there is less to attack.
  • Use .dockerignore and never put secrets in images (lesson 9).
  • One main process per container; run other services in their own containers.
  • Scan images for known vulnerabilities, for example with docker scout or open-source tools such as Trivy.
  • Tag every release with a version and deploy exact tags, never latest (lesson 13).

Deploying to a Server

A simple, common production setup for one server looks like this:

  • A Linux server (cloud VM) with Docker Engine installed (lesson 2) and a firewall allowing only SSH, HTTP and HTTPS.
  • Images built by CI/CD and pushed to a registry with version tags (lesson 13).
  • A compose.yaml on the server describing the app, its database (with a volume) and a reverse proxy such as Caddy or Nginx for HTTPS.
  • Deploying a new version: change the tag in compose.yaml, then docker compose pull and docker compose up -d.
  • Regular database backups copied off the server, and monitoring of disk space and container health.

The “Multi-Service Deployment with a Caddy Reverse Proxy” project walks through exactly this setup. When you outgrow one server, Kubernetes runs the same images across many machines.

Production Checklist

AreaCheck
ImagePinned slim base, multi-stage if compiled, .dockerignore, no secrets
UserUSER set to a non-root user
Server processA production server (gunicorn, uvicorn, node, nginx), not a dev server
HealthHEALTHCHECK in the Dockerfile
ReliabilityRestart policy unless-stopped
ResourcesMemory and CPU limits
DataNamed volumes for anything stateful, with backups
LogsLog rotation limits
NetworkOnly the reverse proxy published; databases internal
ReleasesVersion tags, a registry and a rollback plan
Mini Challenge · Put It in Order

Put a safe production release in order.

  1. docker compose up -d and watch health status and logs
  2. Scan it and run the tests
  3. Push it to the registry
  4. Update the tag on the server and run docker compose pull
  5. Build the image with a new version tag

Frequently Asked Questions

CI/CD (for example GitHub Actions) to build and push images automatically, a cloud platform such as AWS to run them, and later Kubernetes for running containers across many servers.

No. USER changes who runs the app inside the container. Rootless mode runs the Docker daemon itself without root on the host. Both improve security and can be combined.

A common starting point is two to four per CPU core available to the container, then adjust based on measurements.

Key Takeaways

  • Use a production server, a non-root USER and a HEALTHCHECK in your Dockerfile.
  • Run with a restart policy, memory and CPU limits, a read-only filesystem and log rotation.
  • Keep images small, pinned, scanned, versioned and free of secrets.
  • A single server typically runs Compose with a reverse proxy, volumes and backups.

Summary

Congratulations — you have gone from your first docker run to production-ready containers: images, Dockerfiles, networking, volumes, configuration, Compose, multi-stage builds, debugging, registries and security. Put it all into practice with the projects.

Course Complete!

You have finished the Docker course. Build the hands-on projects next to practise everything you have learned.

Back to Course