Skip to content

Commit 4462804

Browse files
committed
feat: implement docker compose deployer
- Add deployWithCompose() and cleanupWithCompose() methods - Support SKAFFOLD_COMPOSE_FILE environment variable for custom compose file paths - Automatically replace image names in compose files with skaffold-built images - Add comprehensive unit tests with 12 test cases covering all functionality - Add complete working example in examples/docker-compose-deploy/ - Add testdata files for unit testing This implementation allows users to deploy applications using docker compose instead of individual containers. The compose file is automatically updated with the correct image tags built by skaffold. Fixes #9321
1 parent 48bae15 commit 4462804

File tree

11 files changed

+574
-5
lines changed

11 files changed

+574
-5
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM golang:1.22-alpine AS builder
2+
WORKDIR /app
3+
COPY main.go .
4+
COPY go.mod .
5+
RUN go build -o app main.go
6+
7+
FROM alpine:latest
8+
WORKDIR /root/
9+
COPY --from=builder /app/app .
10+
EXPOSE 8080
11+
CMD ["./app"]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Example: Deploying with Docker Compose
2+
3+
This example demonstrates how to use Skaffold to build and deploy applications using Docker Compose.
4+
5+
## Prerequisites
6+
7+
- Docker and Docker Compose installed
8+
- Skaffold installed
9+
10+
## Project Structure
11+
12+
- `skaffold.yaml` - Skaffold configuration with Docker Compose deployment
13+
- `docker-compose.yml` - Docker Compose configuration
14+
- `Dockerfile` - Simple application Docker image
15+
- `main.go` - Simple Go web application
16+
17+
## How it Works
18+
19+
1. Skaffold builds the Docker image for the application
20+
2. Skaffold updates the `docker-compose.yml` with the built image tag
21+
3. Skaffold runs `docker compose up` to deploy the application
22+
4. When you stop Skaffold, it runs `docker compose down` to clean up
23+
24+
## Usage
25+
26+
### Run the application
27+
28+
```bash
29+
skaffold dev
30+
```
31+
32+
This will:
33+
- Build the application image
34+
- Deploy it using Docker Compose
35+
- Watch for changes and rebuild/redeploy automatically
36+
37+
### Deploy only
38+
39+
```bash
40+
skaffold run
41+
```
42+
43+
### Clean up
44+
45+
```bash
46+
skaffold delete
47+
```
48+
49+
Or simply press `Ctrl+C` when running `skaffold dev`.
50+
51+
## Configuration
52+
53+
The key part of the `skaffold.yaml` configuration is:
54+
55+
```yaml
56+
deploy:
57+
docker:
58+
useCompose: true
59+
images:
60+
- compose-app
61+
```
62+
63+
- `useCompose: true` - Enables Docker Compose deployment
64+
- `images` - List of images to build and deploy
65+
66+
## Environment Variables
67+
68+
You can customize the Docker Compose file location:
69+
70+
```bash
71+
export SKAFFOLD_COMPOSE_FILE=custom-compose.yml
72+
skaffold dev
73+
```
74+
75+
Default is `docker-compose.yml` in the current directory.
76+
77+
## Notes
78+
79+
- The Docker Compose project name will be `skaffold-{runID}`
80+
- Skaffold automatically replaces image names in the compose file with the built tags
81+
- Volumes and networks are automatically cleaned up on `skaffold delete`
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: '3.8'
2+
services:
3+
app:
4+
image: compose-app
5+
ports:
6+
- "8080:8080"
7+
environment:
8+
- MESSAGE=Hello from Docker Compose!
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module compose-app
2+
3+
go 1.22
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"os"
8+
)
9+
10+
func main() {
11+
message := os.Getenv("MESSAGE")
12+
if message == "" {
13+
message = "Hello, World!"
14+
}
15+
16+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
17+
fmt.Fprintf(w, "%s\n", message)
18+
})
19+
20+
port := ":8080"
21+
log.Printf("Server starting on port %s", port)
22+
log.Printf("Message: %s", message)
23+
24+
if err := http.ListenAndServe(port, nil); err != nil {
25+
log.Fatal(err)
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: skaffold/v4beta13
2+
kind: Config
3+
metadata:
4+
name: docker-compose-deploy-example
5+
build:
6+
artifacts:
7+
- image: compose-app
8+
docker:
9+
dockerfile: Dockerfile
10+
deploy:
11+
docker:
12+
useCompose: true
13+
images:
14+
- compose-app

pkg/skaffold/deploy/docker/deploy.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"context"
2121
"fmt"
2222
"io"
23+
"os"
24+
"os/exec"
2325
"regexp"
2426
"strings"
2527
"sync"
@@ -34,6 +36,7 @@ import (
3436
"github.com/docker/go-connections/nat"
3537
v1 "github.com/google/go-containerregistry/pkg/v1"
3638
"github.com/pkg/errors"
39+
"gopkg.in/yaml.v3"
3740

3841
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/access"
3942
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
@@ -170,8 +173,7 @@ func (d *Deployer) deploy(ctx context.Context, out io.Writer, artifact graph.Art
170173
d.portManager.RelinquishPorts(container.Name)
171174
}
172175
if d.cfg.UseCompose {
173-
// TODO(nkubala): implement
174-
return fmt.Errorf("docker compose not yet supported by skaffold")
176+
return d.deployWithCompose(ctx, out, artifact)
175177
}
176178

177179
containerCfg, err := d.containerConfigFromImage(ctx, artifact.Tag)
@@ -352,6 +354,11 @@ func (d *Deployer) Dependencies() ([]string, error) {
352354
}
353355

354356
func (d *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool, _ manifest.ManifestListByConfig) error {
357+
// If using compose, run docker compose down
358+
if d.cfg.UseCompose {
359+
return d.cleanupWithCompose(ctx, out, dryRun)
360+
}
361+
355362
if dryRun {
356363
for _, container := range d.tracker.DeployedContainers() {
357364
output.Yellow.Fprintln(out, container.ID)
@@ -546,3 +553,137 @@ func (d *Deployer) GetStatusMonitor() status.Monitor {
546553
func (d *Deployer) RegisterLocalImages([]graph.Artifact) {
547554
// all images are local, so this is a noop
548555
}
556+
557+
// deployWithCompose deploys using docker compose
558+
func (d *Deployer) deployWithCompose(ctx context.Context, out io.Writer, artifact graph.Artifact) error {
559+
// Find compose file path (default: docker-compose.yml in current directory)
560+
composeFile := d.getComposeFilePath()
561+
562+
olog.Entry(ctx).Infof("Deploying with docker compose using file: %s", composeFile)
563+
564+
// Check if compose file exists
565+
if _, err := os.Stat(composeFile); err != nil {
566+
return fmt.Errorf("compose file not found at %s: %w", composeFile, err)
567+
}
568+
569+
// Read compose file
570+
composeData, err := os.ReadFile(composeFile)
571+
if err != nil {
572+
return fmt.Errorf("failed to read compose file: %w", err)
573+
}
574+
575+
// Parse compose file
576+
var composeConfig map[string]interface{}
577+
if err := yaml.Unmarshal(composeData, &composeConfig); err != nil {
578+
return fmt.Errorf("failed to parse compose file: %w", err)
579+
}
580+
581+
// Replace image names with skaffold-built images
582+
if err := d.replaceComposeImages(composeConfig, artifact); err != nil {
583+
return fmt.Errorf("failed to replace images in compose file: %w", err)
584+
}
585+
586+
// Write modified compose file to temp location
587+
modifiedComposeData, err := yaml.Marshal(composeConfig)
588+
if err != nil {
589+
return fmt.Errorf("failed to marshal modified compose config: %w", err)
590+
}
591+
592+
tmpComposeFile, err := os.CreateTemp("", "skaffold-compose-*.yml")
593+
if err != nil {
594+
return fmt.Errorf("failed to create temporary compose file: %w", err)
595+
}
596+
defer os.Remove(tmpComposeFile.Name())
597+
defer tmpComposeFile.Close()
598+
599+
if _, err := tmpComposeFile.Write(modifiedComposeData); err != nil {
600+
return fmt.Errorf("failed to write temporary compose file: %w", err)
601+
}
602+
tmpComposeFile.Close()
603+
604+
// Run docker compose up
605+
args := []string{"compose", "-f", tmpComposeFile.Name(), "-p", fmt.Sprintf("skaffold-%s", d.labeller.GetRunID()), "up", "-d"}
606+
cmd := exec.CommandContext(ctx, "docker", args...)
607+
cmd.Stdout = out
608+
cmd.Stderr = out
609+
610+
olog.Entry(ctx).Debugf("Running: docker %v", strings.Join(args, " "))
611+
612+
if err := cmd.Run(); err != nil {
613+
return fmt.Errorf("docker compose up failed: %w", err)
614+
}
615+
616+
olog.Entry(ctx).Infof("Successfully deployed with docker compose")
617+
return nil
618+
}
619+
620+
// getComposeFilePath returns the path to the docker compose file
621+
func (d *Deployer) getComposeFilePath() string {
622+
// Check environment variable first
623+
if path := os.Getenv("SKAFFOLD_COMPOSE_FILE"); path != "" {
624+
return path
625+
}
626+
// Default to docker-compose.yml in current directory
627+
return "docker-compose.yml"
628+
}
629+
630+
// replaceComposeImages replaces image names in compose config with skaffold-built images
631+
func (d *Deployer) replaceComposeImages(composeConfig map[string]interface{}, artifact graph.Artifact) error {
632+
services, ok := composeConfig["services"].(map[string]interface{})
633+
if !ok {
634+
return fmt.Errorf("invalid compose file: services section not found or invalid")
635+
}
636+
637+
// Iterate through services and replace image if it matches
638+
for serviceName, serviceConfig := range services {
639+
service, ok := serviceConfig.(map[string]interface{})
640+
if !ok {
641+
continue
642+
}
643+
644+
// Check if service has an image field
645+
if imageName, ok := service["image"].(string); ok {
646+
// Check if this image matches the artifact's image name
647+
if imageName == artifact.ImageName || strings.Contains(artifact.ImageName, imageName) {
648+
olog.Entry(context.Background()).Debugf("Replacing image for service %s: %s -> %s", serviceName, imageName, artifact.Tag)
649+
service["image"] = artifact.Tag
650+
}
651+
}
652+
}
653+
654+
return nil
655+
}
656+
657+
// cleanupWithCompose cleans up resources deployed with docker compose
658+
func (d *Deployer) cleanupWithCompose(ctx context.Context, out io.Writer, dryRun bool) error {
659+
composeFile := d.getComposeFilePath()
660+
projectName := fmt.Sprintf("skaffold-%s", d.labeller.GetRunID())
661+
662+
if dryRun {
663+
fmt.Fprintf(out, "Would run: docker compose -f %s -p %s down\n", composeFile, projectName)
664+
return nil
665+
}
666+
667+
olog.Entry(ctx).Infof("Cleaning up docker compose deployment")
668+
669+
// Check if compose file exists
670+
if _, err := os.Stat(composeFile); err != nil {
671+
olog.Entry(ctx).Warnf("Compose file not found at %s, skipping cleanup", composeFile)
672+
return nil
673+
}
674+
675+
// Run docker compose down
676+
args := []string{"compose", "-f", composeFile, "-p", projectName, "down", "--volumes", "--remove-orphans"}
677+
cmd := exec.CommandContext(ctx, "docker", args...)
678+
cmd.Stdout = out
679+
cmd.Stderr = out
680+
681+
olog.Entry(ctx).Debugf("Running: docker %v", strings.Join(args, " "))
682+
683+
if err := cmd.Run(); err != nil {
684+
return fmt.Errorf("docker compose down failed: %w", err)
685+
}
686+
687+
olog.Entry(ctx).Infof("Successfully cleaned up docker compose deployment")
688+
return nil
689+
}

0 commit comments

Comments
 (0)