Skaffold’s file sync feature lets you push code changes to a running container without rebuilding the image, making the inner development loop dramatically faster.
Let’s see it in action. Imagine you have a simple Go web server.
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Skaffold Sync!")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
And your skaffold.yaml looks like this:
apiVersion: skaffold/v2beta29
kind: Config
metadata:
name: skaffold-sync-example
build:
artifacts:
- image: my-go-app
docker:
dockerfile: Dockerfile
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
profiles:
- name: dev
activation:
- k8s.context: docker-desktop
build:
artifacts:
- image: my-go-app
sync:
manual: {} # This is the key!
The Dockerfile is straightforward:
FROM golang:1.19-alpine
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["/app/main"]
And k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: my-go-app
template:
metadata:
labels:
app: my-go-app
spec:
containers:
- name: my-go-app
image: my-go-app
ports:
- containerPort: 8080
Now, run skaffold dev --profile dev. Skaffold will build your image, deploy it, and then start watching your local files. If you change main.go to "Hello, Skaffold Sync World!" and save, you’ll see Skaffold output like this:
...
[dev] Successfully pushed my-go-app:<tag>
[dev] Successfully deployed to Kubernetes.
[dev] Watching for changes...
[dev] Syncing changed files...
[dev] /path/to/your/project/main.go
[dev] Waiting for changes...
A few seconds later, if you curl localhost:8080 (assuming you have port-forwarding set up via Skaffold or manually), you’ll see the updated message without any Kubernetes pod restarts. The sync section in your skaffold.yaml is what enables this.
The problem Skaffold file sync solves is the slow feedback loop in containerized development. Typically, any code change requires a full image rebuild and redeploy, which can take minutes for even moderately complex applications. This often leads developers to build code outside containers, debug locally, and only containerize for final testing, missing out on container-specific bugs and the benefits of developing in the environment.
Skaffold sync works by establishing a connection between your local filesystem and the running container. When Skaffold detects a file change, it doesn’t trigger a docker build. Instead, it uses a privileged sidecar container (or, in some cases, mounts the local directory directly into the pod if your Kubernetes setup allows and you configure it) to copy the changed files into the running container’s filesystem. The application process inside the container then needs to be restarted or signaled to reload. For compiled languages like Go or Java, this often means Skaffold executes a build command inside the running container to recompile the code, and then restarts the application process. For interpreted languages like Python or Node.js, it might just restart the interpreter process.
The sync configuration in skaffold.yaml is where you define this behavior. The simplest form is manual: {}, which tells Skaffold to sync files but relies on the container’s entrypoint or command to handle the recompilation and restart. More advanced configurations allow you to specify exact commands to run inside the container after sync. For example:
sync:
auto:
- src: 'src/**/*.py'
dest: /app
preSync: ["echo 'Syncing Python files...'"]
postSync: ["kill -HUP 1"] # Example: Signal a process to reload
Here, auto: enables automatic syncing. src is a glob pattern for your local files, and dest is the target directory inside the container. preSync and postSync are commands executed before and after the file transfer, respectively. The postSync command is crucial for making the changes effective. For a Python app, kill -HUP 1 can often signal the main process to reload its configuration or code. For Go, you might execute go build and then restart the binary.
The most surprising true thing about Skaffold file sync is that it often bypasses the Docker daemon entirely for file transfers after the initial build. When Skaffold syncs, it’s not asking Docker to rebuild an image. It’s interacting directly with the running container’s filesystem, either through a sidecar or a mounted volume. This direct access is what makes it so much faster than a traditional build-and-deploy cycle.
What most developers miss is the power of postSync commands. It’s not just about getting the files into the container; it’s about making the application aware of those files. For compiled languages, you can instruct Skaffold to run a build command inside the container, which is still faster than a full image rebuild because the base image layers are already present. For interpreted languages, you can send signals to the running process to reload gracefully. Mastering postSync unlocks the true potential of rapid iteration.
The next thing you’ll likely want to explore is Skaffold’s ability to manage dependencies between sync operations and rebuilds, or how to configure sync for different application architectures like microservices.