{"id":24316072,"url":"https://github.com/danielstankw/dockerfile-best-practices","last_synced_at":"2025-03-10T04:47:48.613Z","repository":{"id":272825069,"uuid":"917874664","full_name":"danielstankw/Dockerfile-best-practices","owner":"danielstankw","description":"Compilation of Dockerfile best practices/ optimizations","archived":false,"fork":false,"pushed_at":"2025-01-16T20:05:52.000Z","size":9,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-16T21:22:11.078Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danielstankw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-16T20:05:41.000Z","updated_at":"2025-01-16T20:06:06.000Z","dependencies_parsed_at":"2025-01-16T21:22:13.418Z","dependency_job_id":"0029d3d6-69bd-42ef-a183-be56deac768e","html_url":"https://github.com/danielstankw/Dockerfile-best-practices","commit_stats":null,"previous_names":["danielstankw/dockerfile-best-practices"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielstankw%2FDockerfile-best-practices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielstankw%2FDockerfile-best-practices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielstankw%2FDockerfile-best-practices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielstankw%2FDockerfile-best-practices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danielstankw","download_url":"https://codeload.github.com/danielstankw/Dockerfile-best-practices/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242793058,"owners_count":20185949,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-17T12:17:58.919Z","updated_at":"2025-03-10T04:47:48.593Z","avatar_url":"https://github.com/danielstankw.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dockerfile Best Practices 🐳\n\nWriting production-ready Dockerfiles is not as simple as you could think about it. \n\nThis repository contains some best practices for writing Dockerfiles. Even though plenty of articles describe best practices, some of them are outdated or lack lesser-known details - this repo aims to breach that gap. \nThis is all guidance, not a mandate - there may sometimes be reasons not to do what is described here, but if you _don't know_ then this is probably what you should be doing.\n\n### Disclaimer ⚠️\n___\nThis is a compilation of best practices learned during my short career, read online and in books. If you find mistakes or would like to add/ clarify something feel free to create a pull request.  \nThroughout this file, you will find the following ❌ **Bad:** and ✅ **Good:**. Using the approach listed under *Bad* isn't necessarily a mistake, but it's less optimal than *Good*.\n\n## List of Content 📋\n\nThe following are included in the Dockerfile in this repository:\n\n1. [Use official Docker images whenever possible](#1-use-official-docker-images-whenever-possible)\n2. [Limit Image Layers](#2-limit-image-layers)\n3. [Do NOT use the `latest` tag, choose a specific image tag](#3-do-not-use-latest-tag-choose-specific-image-tag)\n4. [Only Store Arguments in `CMD` (cmd vs entrypoint)](#4-only-store-arguments-in-cmd-cmd-vs-entrypoint)\n5. [Use `COPY` instead of `ADD`](#5-use-copy-instead-of-add)\n6. [Combine `apt-get update` and `apt-get install`](#6-combine-apt-get-update-and-apt-get-install)\n7. [Run as a Non-Root User](#7-run-as-a-non-root-user)\n8. [Do not use a UID below 10,000](#8-do-not-use-a-uid-below-10000)\n9. [Use static UID and GID](#9-use-static-uid-and-gid)\n10. [Use multi-staged builds](#10-use-multi-staged-builds-to-reduce-final-image-size)\n11. [Use `--no-cache-dir` (🐍-specific)](#11-use---no-cache-dir--specific)\n12. [Order layers by change frequency](#12-order-layers-by-change-frequency---put-the-most-stable-commands-first)\n13. [Use `.dockerignore`](#13-use-dockerignore)\n14. [Set `WORKDIR` explicitly](#14-set-workdir-explicitly)\n15. [Use Build time arguments for flexibility](#15-use-build-time-arguments-for-flexibility)\n16. [Use `--chmod` in `COPY` instead of separate `RUN`](#16-use---chmod-in-copy-instead-of-separate-run)\n17. [Use `--no-install-recommends` (🐍-specific)](#17-use---no-install-recommends--specific)\n18. [Common performance optimizations](#18-common-performance-optimizations)\n19. [Add metadata labels for better image management](#19-add-metadata-labels-for-better-image-management)\n20. [Avoid `COPY .` whenever possible](#20-avoid-copy--whenever-possible)\n\n___\n## 1. Use official Docker images whenever possible\n\nOfficial Docker images are reliable, secure, and optimized for size and performance. Maintained by experienced contributors, they follow best practices and come with community support. Explore [Python Official Images](https://hub.docker.com/_/python/tags).\n\n## 2. Limit Image Layers\nMinimize the number of layers to keep images lightweight and faster to build. Each `RUN` instruction in your Dockerfile will end up creating an additional cache layer in your final image. The best practice is to limit the amount of layers to keep the image lightweight.\n\n✅ **Good:**\n```dockerfile\nRUN apt-get update \u0026\u0026 apt-get install -y \\\n    curl wget \\\n    \u0026\u0026 apt-get clean \\\n    \u0026\u0026 rm -rf /var/lib/apt/lists/*\n```\n❌ **Bad:**\n```dockerfile\nRUN apt-get update\nRUN apt-get install -y curl\nRUN apt-get install -y wget\nRUN apt-get clean\n```\n\u003e **Tip**: Use `\u0026\u0026` to chain commands and a single `RUN` block for efficiency.  \n\u003e **Tip2**: Order the layers from one that is less likely to change, to one that will change more often.\n\n## 3. Do NOT use the `latest` tag, choose a specific image tag\nUsing the latest can lead to unpredictable behavior when the base image updates. If you don’t specify a specific version or tag in your Dockerfile, it will default to using the latest version of the image.\n\n\u003e **Note:  Specifying a version ensures consistency but requires manual updates to benefit from the latest security patches.**\n\n✅ **Good:**\n```dockerfile\nFROM node:18.13.0\n```\n❌ **Bad:**\n```dockerfile\nFROM node:latest\n```\n\n## 4. Only Store Arguments in `CMD` (cmd vs entrypoint)\nUse `CMD` for the default behavior or runtime arguments. Avoid hardcoding in `CMD`.\n`CMD` should contain command arguments, while `ENTRYPOINT` should contain the command itself.\n\n- `ENTRYPOINT`: Defines the main command to be executed when the container starts.\n- `CMD`: Provides default arguments for the `ENTRYPOINT` or acts as the default command when `ENTRYPOINT` is not defined. It allows users to override arguments at runtime.\n\n✅ **Good:**\n```dockerfile\nENTRYPOINT [\"python\", \"main.py\"]\nCMD [\"--host=0.0.0.0\", \"--port=5000\"]\n```\n❌ **Bad:**\n```dockerfile\nCMD [\"python\", \"main.py\", \"--host=0.0.0.0\", \"--port=5000\"]\n```\n\n**Use case:**  \nTo run with default arguments: \n```bashrc\ndocker run myapp\n```\nThis will run with the defaults: `--host=0.0.0.0` and `--port=5000`.  \nIf we wish to change the port:  \n```bashrc\ndocker run myapp --host=0.0.0.0 --port=8000\n```\n\n\n## 5. Use `COPY` instead of `ADD`\n`COPY` is more explicit. Use `ADD` only when you need to automatically extract `tar` files or download a file from remote URLs.\n\n✅ **Good:**\n```dockerfile\nCOPY ./app /app/\n```\n✅ **Good:**\n```dockerfile\n# Extracting tar file and adding to the image\nADD app.tar.gz /app/\n```\n❌ **Bad:**\n```dockerfile\nADD ./app /app/  # Use COPY instead\n```\n\n## 6. Combine `apt-get update` and `apt-get install`\n\n__Prerequistite__ - *Package Index Files*  \nPackage index files are metadata files maintained by a package management system (such as `apt` in Debian-based systems) that contain information about the available software packages i.e.: package names, versions, dependencies, and sources.  \nThese files are essential for package updates. On Debian-based systems, they are located in `/var/lib/apt/lists/`\n\n__General Rule__\nAlways combine `apt-get update` with `apt-get install` to ensure you're installing the latest available packages.\n\nThe `apt-get update` command fetches the latest package lists. These lists contain information about the available packages and their versions and are stored in that cache layer.\nIf you run `apt-get install` in a new `RUN` command the package index from the previous layer is no longer accessible during installation, meaning it will use an outdated package index leading to:\n- Installing outdated packages\n- Dependency failures\n\n\u003e**Remember** Every `RUN` line in the Dockerfile is a different process.\n\n✅ **Good:**\n```dockerfile\nRUN apt-get update \u0026\u0026 apt-get install -y --no-install-recommends\\\n    package1 \\\n    package2 \\\n    \u0026\u0026 rm -rf /var/lib/apt/lists/*\n```\n❌ **Bad:**\n```dockerfile\nRUN apt-get update\nRUN apt-get install -y --no-install-recommends package1 package2 \u0026\u0026 rm -rf /var/lib/apt/lists/*\n```\n\u003e**Tip** To reduce the image size, remove the package index files after installation using: `rm -rf /var/lib/apt/lists/*`\n\n## 7. Run as a Non-Root User\nRunning containers with a non-root user is a critical security best practice that helps prevent container breakout attacks and limits potential damage from compromised applications.\n\n\u003e **Note: When setting up your container's directory structure, it's important to establish proper ownership and permissions before switching to a non-root user.**\n\n__Key points:__\n- Create a dedicated user and group with specific IDs\n- Set up directory structure and permissions before switching users\n- Use `--chown` flag with the `COPY` command to maintain correct ownership\n- Apply minimal required permissions \n- Switch to non-root user\n\n✅ **Good:**\n```dockerfile\nFROM python:3.12-slim\n\n# Create app user and group with specific IDs for consistency\nRUN groupadd -g 10001 appgroup \u0026\u0026 \\\n    useradd -u 10000 -g appgroup appuser\n\nWORKDIR /app\n\n# Set up directory structure with proper permissions first\nRUN mkdir -p /app/logs /app/data /app/config \u0026\u0026 \\\n    chown -R appuser:appgroup /app \u0026\u0026 \\\n    chmod -R 755 /app \u0026\u0026 \\\n    chmod -R 775 /app/logs  # Writable for logs\n\n# Install dependencies as root\nCOPY --chown=appuser:appgroup requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\n# Copy the application code with the correct ownership\nCOPY --chown=appuser:appgroup . .\n\n# Switch to non-root user\nUSER appuser:appgroup\n\nCMD [\"python\", \"app.py\"]\n```\n\n❌ **Bad:**\n```dockerfile\nFROM python:3.12-slim\n\n# Missing user creation and running as root\nWORKDIR /app\n\n# Incorrect permissions handling\nCOPY . .\nRUN chmod 777 -R /app  # Too permissive!\n\n# No user specification - defaults to root\nCMD [\"python\", \"app.py\"]\n```\n\n\n\n## 8. Do not use a UID below 10,000\nUIDs below 10,000 are a security risk on several systems, because if someone does manage to escalate privileges outside the Docker container their Docker container UID may overlap with a more privileged system user's UID granting them additional permissions. For best security, always run your processes as a UID above 10,000.\n\n✅ **Good:**\n```dockerfile\nRUN groupadd -g 10001 appuser \u0026\u0026 \\\n    useradd -u 10001 -g appuser appuser\n```\n❌ **Bad:**\n```dockerfile\nRUN groupadd -g 100 appuser \u0026\u0026 \\\n    useradd -u 100 -g appuser appuser\n```\n\n## 9. Use static UID and GID\n- Files and directories on a Linux system are associated with specific UIDs and GIDs, which determine who can read, write, or execute them (`rwx`).  \n- When a Docker container creates or manipulates files on a shared volume or directly on the host filesystem, the files are owned by the UID/GID of the container process that created them.\n- If the container uses dynamically assigned UIDs/GIDs (the default), the container’s UID/GID could vary between builds or deployments.\n- This variation in UIDs/GIDs makes it harder to manage file ownership consistently, especially when these files need to be accessed or modified on the host system.\n\n✅ **Good:**\n```dockerfile\nARG UID=10001\nARG GID=10001\n\nRUN groupadd -g $GID appuser \u0026\u0026 \\\n    useradd -u $UID -g appuser appuser\n```\n❌ **Bad:**\n```dockerfile\nRUN adduser --system appuser  # Random UID/GID assigned\n```\n\n## 10. Use multi-staged builds to reduce the final image size\nMulti-stage builds are a powerful technique to create smaller, more secure Docker images by separating build-time dependencies from runtime requirements.  \nAssume we have:\n```\n├── app.py              # Main application code\n├── requirements.txt    # Dependencies\n└── .gitignore\n```\n\n\u003e**NOTE: `requirements.txt` should have dependencies versions pinned ex. `requests==2.31.0`**\n\n✅ **Good: Single-Stage Build**\nSimple. Includes all build tools and dependencies in the final image, resulting in a larger final image size. \n```dockerfile\nFROM python:3.12-slim\nWORKDIR /app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\nCMD [\"python\", \"app.py\"]\n```\n\n🔥✅ **Better: Multi-Stage Build**\nUses two stages, in the first stage `builder` installs dependencies in virtual env, while in the second `runner` stage copies only the necessary files.  \n\n**Why use a virtual environment?**  \nIn multi-stage builds, we need to copy dependencies from the `builder` to the final `runner` stage. By default when installing Python packages they and related files are installed in various places. By using virtual env we know exactly where those dependancies are located and therefore copying them over from one stage to another is a simpler task. [Read more](https://pythonspeed.com/articles/multi-stage-docker-python/)\n\n```dockerfile\nFROM python:3.12-slim as builder\n\nWORKDIR /app\n# Create virtual env in /opt/venv which isolated Python packages from the system Python\nRUN python3 -m venv /opt/venv\n# Modifies PATH and puts the venv bin directory as first in PATH\nENV PATH=\"/opt/venv/bin:$PATH\"\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nFROM python:3.12-slim as runner\n\n# Copy venv installed packages from builder to runner stage\nCOPY --from=builder /opt/venv /opt/venv\n# Adds venv to PATH in runner stage\nENV PATH=\"/opt/venv/bin:$PATH\"\n\nWORKDIR /app\nCOPY . .\nCMD [\"python\", \"app.py\"]\n```\n\n**[Explanation](https://pythonspeed.com/articles/activate-virtualenv-dockerfile/)**  \nThe most important part is setting PATH: PATH is a list of directories that are searched for commands to run. `activate` simply adds the virtualenv’s `bin/` directory to the start of the list, so when the python command is executed system first checks the `/opt/venv/bin`, where it finds our venv python and uses it instead of system Python.  \n**We can replace activate by setting the appropriate environment variables: Docker’s ENV command applies both subsequent RUNs as well as to the CMD.**\n\n\n## 11. Use `--no-cache-dir` (🐍-specific)\nWhen pip installs packages, it keeps a cache of downloaded wheel files and source distributions. This cache is unnecessary in Docker images since we don't need to reinstall packages. Removing the cache reduces the final image size significantly. Containers should be immutable - once built, they shouldn't change, therefore package cache is only useful for future installations, which won't happen in an immutable container\n\n✅ **Good:**\n```dockerfile\nRUN pip install --no-cache-dir -r requirements.txt\n# Creates a minimal layer with just the installed packages\n```\n❌ **Bad:**\n```dockerfile\nRUN pip install -r requirements.txt\n# Creates larger layer with cache files (~100-200MB extra)\n```\n\n## 12. Order layers by change frequency - put the most stable commands first\nDocker uses a layer caching system during builds. Organizing layers by changing frequency dramatically improves build performance.\n\n✅ **Good:**\n1. Base Image: Rarely changes; placed first.\n2. System Dependencies: Stable; cached after the first build.\n3. Python Dependencies: Relatively stable but may change with requirements.txt; cached when requirements.txt is unchanged.\n4. Application Code: Changes most frequently; placed last to minimize cache invalidation.\n\n```dockerfile\n# Use a base Python image\nFROM python:3.11-slim\n# Set a working directory\nWORKDIR /app\n# Install system dependencies (stable)\nRUN apt-get update \u0026\u0026 apt-get install -y build-essential\n# Install Python dependencies (semi-stable)\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n# Copy application code (frequent changes)\nCOPY . .\n# Set the entry point\nCMD [\"python\", \"app.py\"]\n\n```\n\n❌ **Bad:**\n```dockerfile\n# Use a base Python image\nFROM python:3.11-slim\n# Copy application code (frequent changes)\nCOPY . .\n# Set a working directory\nWORKDIR /app\n# Install Python dependencies (semi-stable)\nRUN pip install --no-cache-dir -r requirements.txt\n# Install system dependencies (stable)\nRUN apt-get update \u0026\u0026 apt-get install -y build-essential\n# Set the entry point\nCMD [\"python\", \"app.py\"]\n```\n\n## 13. Use `.dockerignore`\nThe `.dockerignore` file prevents unnecessary files from being included in the build context, improving build performance and security.\n\n```.dockerignore\n# Version control\n.git\n.gitignore\n\n# Development artifacts\n__pycache__\n*.pyc\n*.pyo\n*.pyd\n.Python\nenv/\nvenv/\n.env*\n*.log\n\n# Development tools\n.idea/\n.vscode/\n*.swp\n*.swo\n```\n\n## 14. Set `WORKDIR` explicitly\n\n```dockerfile\nWORKDIR /app             # Good\nRUN cd /app \u0026\u0026 command   # Bad\n```\n\n## 15. Use Build-time arguments for flexibility\nBuild-time arguments (ARG) provide a way to pass configuration options to your Docker build process. This is especially useful when you need different configurations for different environments (e.g., development, staging, production) without modifying the Dockerfile itself.\n\n```dockerfile\nARG PORT=3000\nEXPOSE ${PORT}\n```\n\nYou can override it during the build process by running:\n```\ndocker build --build-arg PORT=8080 -t myapp .\n```\n\n\n## 16. Use `--chmod` in `COPY` instead of separate `RUN`\nThe `--chmod` flag in `COPY` or `ADD` allows you to set file permissions during the copy process, eliminating the need for additional `RUN` commands. This reduces the number of layers in your image and improves build performance.\n\n✅ **Good:**\n```dockerfile\nCOPY --chmod=755 script.sh .\n```\n❌ **Bad:**\n```dockerfile\nCOPY script.sh .\nRUN chmod +x script.sh\n```\n\n\n## 17. Use `--no-install-recommends` (🐍-specific)\nBy default, the` apt-get install` command installs recommended and suggested packages, which can lead to unnecessary bloat in your Docker image. Using the `--no-install-recommends` flag ensures that only the essential packages are installed, keeping your image smaller and faster to build.\n\n✅ **Good:**\n```dockerfile\nRUN apt-get update \u0026\u0026 \\\n    apt-get install -y --no-install-recommends package \u0026\u0026 \\\n    apt-get clean \u0026\u0026 rm -rf /var/lib/apt/lists/*\n```\n\n\u003e**Tip: After installing packages, always clean up apt caches to avoid unnecessary image bloat:**\n\n\n## 18. Common performance optimizations\nEach language and runtime has specific configurations that can significantly enhance performance when running in a containerized environment. These optimizations can reduce memory usage, improve execution speed, and make better use of container resources.\n\n```dockerfile\n# Node.js optimizations\nENV NODE_OPTIONS=\"--max-old-space-size=2048\" \\\n    UV_THREADPOOL_SIZE=64 \\\n    NODE_NO_WARNINGS=1\n\n# Python optimizations\nENV PYTHONUNBUFFERED=1 \\        # Ensure logs and print() are written immediately to the console without being buffered.\n    PYTHONDONTWRITEBYTECODE=1   # Disable generation of .pyc files when importing modules - save space and avoid unnecessary I/O\n\n\n# Java optimizations\nENV JAVA_OPTS=\"-XX:+UseG1GC -XX:+UseContainerSupport -XX:MaxRAMPercentage=75\"\n\n# Golang optimizations\nENV GOGC=off \\\n    GOMAXPROCS=2\n```\n\n## 19. Add metadata labels for better image management\nLabels provide descriptive metadata for your Docker images. They simplify image management, help with automation, and ensure compliance with standards. Labels are particularly useful for identifying the purpose, version, and maintainer of an image.\n\n```dockerfile\nLABEL maintainer=\"Daniel Jones \u003cdanielJones@gmail.com\u003e\" \\\n      description=\"Docker image for X application\" \\\n      version=\"1.0\" \n```\n\n## 20. Avoid `COPY .` whenever possible\nUsing `COPY .` indiscriminately copies everything from the build context into the image, including unnecessary files like .git directories, local configuration files, or temporary files - unless those are excluded in `.gitignore`. \nExplicitly specify the files and directories you need.\n\n\n# Sources 🔗\nhttps://hynek.me/about/  \nhttps://pythonspeed.com/articles/dockerizing-python-is-hard/  \nhttps://hynek.me/articles/docker-uv/  \nhttps://docs.docker.com/build/building/best-practices/  \nhttps://pythonspeed.com/articles/base-image-python-docker-images/  \nhttps://github.com/dnaprawa/dockerfile-best-practices  \nhttps://sysdig.com/learn-cloud-native/dockerfile-best-practices/ \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielstankw%2Fdockerfile-best-practices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielstankw%2Fdockerfile-best-practices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielstankw%2Fdockerfile-best-practices/lists"}