Skaffold can build independent services from a multi-module monorepo, but it’s not always obvious how to configure it without building everything every time.

Let’s say you have a monorepo structured like this:

monorepo/
├── services/
│   ├── service-a/
│   │   ├── src/
│   │   ├── pom.xml
│   │   └── Dockerfile
│   ├── service-b/
│   │   ├── src/
│   │   ├── pom.xml
│   │   └── Dockerfile
│   └── service-c/
│       ├── src/
│       ├── pom.xml
│       └── Dockerfile
├── shared/
│   └── utils/
│       ├── src/
│       └── pom.xml
└── pom.xml (root)

You want Skaffold to only build and deploy service-a when you make changes to service-a/, even though service-b and service-c are in the same repository.

Here’s how you’d configure skaffold.yaml to achieve this, focusing on service-a as an example:

apiVersion: skaffold/v2beta25
kind: Config
deploy:
  kubectl:
    manifests:
      - k8s/service-a.yaml # Assuming you have separate manifests per service
build:
  local:
    push: false
  artifacts:
    - image: my-registry/service-a
      context: services/service-a
      docker:
        dockerfile: Dockerfile
      sync:
        manual: {} # Disable auto-sync for simplicity in this example, or configure as needed
      # This is the key part: defining how Skaffold knows what to build
      buildArgs:
        # Example for Maven: If your Dockerfile uses ARG for version or other build info
        # VERSION: "1.0.0"

The context field is crucial. It tells Skaffold the root directory from which to execute the build commands and find the Dockerfile. By setting context: services/service-a, you’re telling Skaffold that services/service-a is the working directory for building this specific artifact.

Now, imagine your services/service-a/Dockerfile looks like this:

FROM maven:3.8.5-openjdk-11 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/service-a-1.0.0.jar service-a.jar
EXPOSE 8080
CMD ["java", "-jar", "service-a.jar"]

When you run skaffold dev, Skaffold will:

  1. Detect changes within the services/service-a directory (or any files it’s configured to watch within that context).
  2. Change its working directory to services/service-a.
  3. Execute the build using the Dockerfile found at services/service-a/Dockerfile.
  4. Tag the resulting image as my-registry/service-a.
  5. Deploy using the specified Kubernetes manifests, referencing the my-registry/service-a image.

To handle multiple services independently, you simply add more artifacts to the build section:

apiVersion: skaffold/v2beta25
kind: Config
deploy:
  kubectl:
    manifests:
      - k8s/service-a.yaml
      - k8s/service-b.yaml
build:
  local:
    push: false
  artifacts:
    - image: my-registry/service-a
      context: services/service-a
      docker:
        dockerfile: Dockerfile
    - image: my-registry/service-b
      context: services/service-b
      docker:
        dockerfile: Dockerfile

Skaffold’s intelligent change detection will then monitor the files relevant to each artifact’s context. If you only change files in services/service-b/src, Skaffold will rebuild and redeploy only service-b.

What if you have shared modules?

This is where it gets interesting. If service-a depends on shared/utils, and your services/service-a/Dockerfile (or your build process within it, like mvn package) copies shared/utils, Skaffold needs to be aware of this dependency.

The simplest, though potentially less efficient, way is to ensure your context for service-a is broad enough to include any necessary shared code that the Docker build needs to COPY. If your Dockerfile uses a COPY ../../shared/utils ./shared-utils or similar, and services/service-a is your context, this will work.

However, Skaffold’s primary trigger for rebuilding an artifact is changes within its specified context directory. If shared/utils changes, and it’s outside the services/service-a context, Skaffold won’t automatically trigger a rebuild of service-a based on that change alone.

To handle shared module changes effectively, you have a few strategies:

  1. Wider Context (Simple but potentially noisy): If service-a’s Dockerfile directly copies code from shared/utils, you can set the context for service-a to be the monorepo root: context: .. Then, within the Dockerfile, you’d specify paths relative to the root, e.g., COPY shared/utils ./shared-utils.

    • Pros: Simple to configure.
    • Cons: Skaffold will now watch all files in the monorepo root for changes relevant to service-a. A change in service-b could trigger a rebuild of service-a if the Dockerfile is structured to copy it, which defeats the purpose of independent builds.
  2. Multi-Stage Dockerfiles with Shared Build: A more robust approach is to have a multi-stage build where the shared dependencies are built first, and then copied into the service’s build stage. Skaffold’s context would still point to the service, but the Dockerfile would handle the dependency.

    Example services/service-a/Dockerfile:

    # Stage 1: Build shared utils
    FROM maven:3.8.5-openjdk-11 AS utils-builder
    WORKDIR /app
    COPY ../../shared/utils/pom.xml .
    COPY ../../shared/utils/src ./src
    RUN mvn clean package -DskipTests
    
    # Stage 2: Build service-a
    FROM maven:3.8.5-openjdk-11 AS app-builder
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    # Copy the built JAR from shared utils
    COPY --from=utils-builder /app/target/utils-1.0.0.jar ../../shared/utils/target/utils-1.0.0.jar # Path might vary
    RUN mvn clean package -DskipTests
    
    # Stage 3: Final image
    FROM openjdk:11-jre-slim
    WORKDIR /app
    COPY --from=app-builder /app/target/service-a-1.0.0.jar service-a.jar
    EXPOSE 8080
    CMD ["java", "-jar", "service-a.jar"]
    

    In this scenario, Skaffold’s context: services/service-a is still used. When shared/utils changes, the Docker build process itself will pick it up because the Dockerfile explicitly copies it. However, Skaffold’s detection mechanism still relies on changes within the context. To make Skaffold aware of the shared module change, you’d need to either:

    • Include shared/utils in Skaffold’s watch configuration for service-a (if supported and desired).
    • Or, more commonly, if a change in shared/utils necessitates a rebuild of service-a, you might structure your build system (e.g., Maven’s parent POM) such that a change in shared/utils also modifies a file within services/service-a (like a version number in services/service-a/pom.xml), which Skaffold is watching.
  3. Separate Skaffold Configurations: For truly independent development and CI/CD, you might have separate skaffold.yaml files in each service’s directory, or a top-level one that only builds specific artifacts based on environment variables or command-line arguments.

    Example services/service-a/skaffold.yaml:

    apiVersion: skaffold/v2beta25
    kind: Config
    deploy:
      kubectl:
        manifests:
          - ../../k8s/service-a.yaml
    build:
      local:
        push: false
      artifacts:
        - image: my-registry/service-a
          context: . # Context is now the service directory itself
          docker:
            dockerfile: Dockerfile
    

    You would then run skaffold dev -f services/service-a/skaffold.yaml. This is great for feature branches or focused development but can be cumbersome for managing the entire monorepo.

The key takeaway is that Skaffold’s context defines the root for build commands, and its change detection primarily operates on files within that context. To achieve independent service builds in a monorepo, you must carefully define these contexts and understand how your Dockerfile or build scripts interact with shared code.

If you’ve configured multiple artifacts, each with its own context, and you run skaffold dev, Skaffold will watch files in all specified contexts. When a change is detected in services/service-a/src, only the artifact with context: services/service-a will be rebuilt and redeployed.

If you find Skaffold is rebuilding everything when you change one service, double-check your context paths and ensure they are specific enough. Also, verify that your Dockerfile isn’t inadvertently copying files from other services into its build context that Skaffold might then consider part of its dependencies.

The next thing you’ll likely encounter is managing dependencies between these independently built services, especially when one service needs to talk to another that’s also being developed and deployed.

Want structured learning?

Take the full Skaffold course →