We can run pre-commit hooks with Docker. Two Scenarios to choose from ?
A quick example which I created for mruby here:
https://github.com/mruby/mruby/blob/8e074f185bd940f7dd52e3296f3c9bf70c830ba4/Dockerfile#L11
Apache Airflow uses Docker and pre-commit
Airflow has 1700 lines of code in their pre-commit config:
https://github.com/apache/airflow/blob/main/.pre-commit-config.yaml
And now what Google Gemini says about it. Gemini can make mistakes, so double-check it.
Installing pre-commit hooks inside a Docker container usually means one of two things:
- You want
pre-committo run as part of your CI/CD pipeline within a Docker image. In this scenario, the Docker container is the environment where the hooks will execute to validate your code. - You want to set up your development environment to run
pre-commithooks locally, but your development setup itself is Dockerized (e.g., using Docker Compose or a devcontainer). This is a bit more nuanced, aspre-committypically interacts with your local Git repository.
Let's break down both scenarios:
Scenario 1: Installing Pre-Commit Hooks for CI/CD within a Docker Container
This is the more common and straightforward approach. You include pre-commit and your configuration in your Dockerfile so that your automated checks can run reliably.
Steps:
-
Create your
.pre-commit-config.yaml: Make sure you have a.pre-commit-config.yamlfile at the root of your project. This file defines which hooks to run.# .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 # Use the latest stable version hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black rev: 24.4.2 # Use the latest stable version hooks: - id: black # Add other hooks as needed (e.g., for Python, JavaScript, etc.) -
Add
pre-commitinstallation to your Dockerfile: You'll need to installpre-commitand then "install" the hook environments. Thepre-commit install-hookscommand will download and set up the necessary tools for the hooks defined in your.pre-commit-config.yaml.# Dockerfile # Use an appropriate base image for your project FROM python:3.9-slim-buster # Set working directory WORKDIR /app # Copy your project files, including .pre-commit-config.yaml COPY . /app/ # Install pre-commit RUN pip install pre-commit # Initialize a dummy Git repository and install hook environments # This is crucial for pre-commit to cache the hook executables # The `rm -rf .git` ensures this temporary repo doesn't affect your main repo. RUN git init . && pre-commit install-hooks && rm -rf .git # You can now run pre-commit against your files # For CI, you'd typically run it as part of a build script or directly in the Dockerfile # Example for a CI step: CMD ["/usr/local/bin/pre-commit", "run", "--all-files"] # If your app needs to run, add your usual entrypoint/cmd here # ENTRYPOINT ["python", "your_app.py"]Explanation of
RUN git init . && pre-commit install-hooks && rm -rf .git:git init .:pre-commitneeds a Git repository to set up its hooks and cache environments. We create a temporary one.pre-commit install-hooks: This command processes your.pre-commit-config.yamland downloads/installs the tools required by your hooks into~/.cache/pre-commitwithin the container. This makes your Docker image self-contained with all the necessary hook dependencies.rm -rf .git: Afterpre-commithas set up its environments, the temporary.gitdirectory is no longer needed for the image itself, so we remove it to keep the image clean.
-
Run
pre-commitin your CI/CD pipeline: In your CI/CD configuration (e.g., GitHub Actions, GitLab CI, Jenkins), you would build this Docker image and then executepre-commit run --all-fileswithin a container spun up from this image.Example (conceptual CI step):
docker build -t my-project-linted . docker run my-project-linted pre-commit run --all-filesIf
pre-commit run --all-filesexits with a non-zero status (meaning hooks failed), your CI pipeline will fail, indicating a code quality issue.
Scenario 2: Running Pre-Commit Hooks in a Dockerized Local Development Environment
This is more complex because pre-commit usually needs to interact directly with your host machine's Git repository. There are a few approaches, each with pros and cons:
Option A: Install pre-commit on the Host (Recommended for Local Dev)
This is generally the simplest and most robust way to use pre-commit for local development, even if your application runs in Docker.
- Install
pre-commiton your host machine:pip install pre-commit # Or using your system's package manager, e.g., brew install pre-commit on macOS - Navigate to your project root and run:
This creates the necessary Git hooks in yourpre-commit install.git/hooksdirectory. - Ensure hooks have access to necessary tools:
If your
pre-commithooks rely on tools that are only installed inside your Docker container (e.g., a specific Python version, a linter, or formatter), you have a few choices:- Install those tools on your host machine as well. (Simplest for common tools).
- Use
language: docker_imagehooks in your.pre-commit-config.yaml: This allowspre-committo run a specific hook inside a Docker image. This can be complex to set up, but ensures consistency. You'd need to define the Docker image and entry point for that hook. - Manually modify your
pre-commithook script: You could theoretically modify the.git/hooks/pre-commitscript to execute a Docker command that runs the actual linter/formatter inside your development container. This is generally discouraged as it deviates frompre-commit's standard usage and can be brittle.
Why this is often preferred:
pre-commitis designed to run locally on your Git repository.- It provides immediate feedback before you even commit.
- Avoids complexities of Docker-in-Docker or mounting Git directories.
Option B: Running pre-commit within a Development Container (e.g., VS Code Dev Containers)
If your entire development environment, including Git operations, happens inside a Docker container (like with VS Code Dev Containers), then installing pre-commit inside that container makes sense.
-
Add
pre-commitinstallation to your Dev Container's Dockerfile ordevcontainer.json: You'd follow steps similar to the CI/CD scenario, making surepre-commitand its hook environments are set up when the dev container builds.Example in a
devcontainer.jsonfor VS Code:{ "name": "My Project", "dockerFile": "Dockerfile", "mounts": [ "source=${localWorkspaceFolder},target=/workspace,type=bind" ], "postCreateCommand": "pre-commit install", // This runs after the container is created "postStartCommand": "pre-commit install --install-hooks" // Can also run on start if needed }And in your
Dockerfilefor the dev container:FROM python:3.9-slim-buster WORKDIR /workspace COPY . /workspace/ RUN pip install pre-commit # No need for `git init` and `rm -rf .git` here if the .git directory is mounted from hostThe key is that the
.gitdirectory from your host machine is mounted into the container, allowingpre-committo set up the hooks directly within that mounted Git repository.
Option C: Running pre-commit via docker run or docker-compose exec
This is less about "installing" hooks inside the container, and more about running the pre-commit command using your container's environment. You would typically do this manually or integrate it into a wrapper script.
- Ensure
pre-commitis installed in your Docker image (as in Scenario 1). - Manually execute the checks:
Or if you just have a Dockerfile:docker-compose run --rm my_service pre-commit run --all-files
This is useful for ad-hoc checks or for a custom script that integrates with your localdocker build -t my_linter_image . # Build the image with pre-commit installed docker run -v $(pwd):/app my_linter_image pre-commit run --all-filesgit commitprocess, but it's not the typical waypre-commit installworks.
Key Considerations:
- Caching:
pre-commitcaches its hook environments. When building a Docker image for CI/CD, runningpre-commit install-hooksduring the build process pre-populates this cache, making subsequentpre-commit runcommands faster and independent of external network access at runtime. - Git Repository:
pre-commitneeds access to a Git repository. For CI/CD, you typically copy your code into the container, and thengit inita temporary repo. For local dev, you usually mount your host's Git repo. - Performance: For local development, running
pre-commitdirectly on your host is often faster than spinning up Docker containers for each check. - Consistency: Using
pre-commitensures that all developers and CI/CD pipelines use the exact same code quality checks, regardless of their local setup.
Choose the method that best fits your workflow and the needs of your team. For CI/CD, embedding pre-commit in your Dockerfile as shown in Scenario 1 is highly recommended. For local development, installing pre-commit on the host machine and letting it manage hooks against your local .git repository (Option A) is generally the most straightforward.