diff --git a/docker_mounts.go b/docker_mounts.go
index 7954b2b102..9609d92caa 100644
--- a/docker_mounts.go
+++ b/docker_mounts.go
@@ -1,6 +1,9 @@
package testcontainers
import (
+ "errors"
+ "path/filepath"
+
"github.com/docker/docker/api/types/mount"
"github.com/testcontainers/testcontainers-go/log"
@@ -11,6 +14,7 @@ var mountTypeMapping = map[MountType]mount.Type{
MountTypeVolume: mount.TypeVolume,
MountTypeTmpfs: mount.TypeTmpfs,
MountTypePipe: mount.TypeNamedPipe,
+ MountTypeImage: mount.TypeImage,
}
// Deprecated: use Files or HostConfigModifier in the ContainerRequest, or copy files container APIs to make containers portable across Docker environments
@@ -32,6 +36,12 @@ type TmpfsMounter interface {
GetTmpfsOptions() *mount.TmpfsOptions
}
+// ImageMounter can optionally be implemented by mount sources
+// to support advanced scenarios based on mount.ImageOptions
+type ImageMounter interface {
+ ImageOptions() *mount.ImageOptions
+}
+
// Deprecated: use Files or HostConfigModifier in the ContainerRequest, or copy files container APIs to make containers portable across Docker environments
type DockerBindMountSource struct {
*mount.BindOptions
@@ -85,6 +95,48 @@ func (s DockerTmpfsMountSource) GetTmpfsOptions() *mount.TmpfsOptions {
return s.TmpfsOptions
}
+// DockerImageMountSource is a mount source for an image
+type DockerImageMountSource struct {
+ // imageName is the image name
+ imageName string
+
+ // subpath is the subpath to mount the image into
+ subpath string
+}
+
+// NewDockerImageMountSource creates a new DockerImageMountSource
+func NewDockerImageMountSource(imageName string, subpath string) DockerImageMountSource {
+ return DockerImageMountSource{
+ imageName: imageName,
+ subpath: subpath,
+ }
+}
+
+// Validate validates the source of the mount, ensuring that the subpath is a relative path
+func (s DockerImageMountSource) Validate() error {
+ if !filepath.IsLocal(s.subpath) {
+ return errors.New("image mount source must be a local path")
+ }
+ return nil
+}
+
+// ImageOptions returns the image options for the image mount
+func (s DockerImageMountSource) ImageOptions() *mount.ImageOptions {
+ return &mount.ImageOptions{
+ Subpath: s.subpath,
+ }
+}
+
+// Source returns the image name for the image mount
+func (s DockerImageMountSource) Source() string {
+ return s.imageName
+}
+
+// Type returns the mount type for the image mount
+func (s DockerImageMountSource) Type() MountType {
+ return MountTypeImage
+}
+
// PrepareMounts maps the given []ContainerMount to the corresponding
// []mount.Mount for further processing
func (m ContainerMounts) PrepareMounts() []mount.Mount {
@@ -118,6 +170,8 @@ func mapToDockerMounts(containerMounts ContainerMounts) []mount.Mount {
containerMount.VolumeOptions = typedMounter.GetVolumeOptions()
case TmpfsMounter:
containerMount.TmpfsOptions = typedMounter.GetTmpfsOptions()
+ case ImageMounter:
+ containerMount.ImageOptions = typedMounter.ImageOptions()
case BindMounter:
log.Printf("Mount type %s is not supported by Testcontainers for Go", m.Source.Type())
default:
diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md
index be65c53029..f3f3ca19a3 100644
--- a/docs/features/common_functional_options.md
+++ b/docs/features/common_functional_options.md
@@ -15,6 +15,23 @@ _Testcontainers for Go_ exposes an interface to perform this operation: `ImageSu
Using the `WithImageSubstitutors` options, you could define your own substitutions to the container images. E.g. adding a prefix to the images so that they can be pulled from a Docker registry other than Docker Hub. This is the usual mechanism for using Docker image proxies, caches, etc.
+#### WithImageMount
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+Since Docker v28, it's possible to mount an image to a container, passing the source image name, the relative subpath to mount in that image, and the mount point in the target container.
+
+This option validates that the subpath is a relative path, raising an error otherwise.
+
+
+[Image Mount](../../modules/ollama/examples_test.go) inside_block:mountImage
+
+
+In the code above, which mounts the directory in which Ollama models are stored, the `targetImage` is the name of the image containing the models (an Ollama image where the models are already pulled).
+
+!!!warning
+ Using this option fails the creation of the container if the underlying container runtime does not support the `image mount` feature.
+
#### WithEnv
- Since testcontainers-go :material-tag: v0.29.0
diff --git a/docs/features/files_and_mounts.md b/docs/features/files_and_mounts.md
index 118ee1ccbb..bae6caa6e1 100644
--- a/docs/features/files_and_mounts.md
+++ b/docs/features/files_and_mounts.md
@@ -20,6 +20,20 @@ It is possible to map a Docker volume into the container using the `Mounts` attr
It is recommended to copy data from your local host machine to a test container using the file copy API
described below, as it is much more portable.
+## Mounting images
+
+Since Docker v28, it is possible to mount the file system of an image into a container using the `Mounts` attribute at the `ContainerRequest` struct. For that, use the `DockerImageMountSource` type, which allows you to specify the name of the image to be mounted, and the subpath inside the container where it should be mounted, or simply call the `ImageMount` function, which does exactly that:
+
+
+[Image mounts](../../lifecycle_test.go) inside_block:imageMounts
+
+
+!!!warning
+ If the subpath is not a relative path, the creation of the container will fail.
+
+!!!info
+ Mounting images fails the creation of the container if the underlying container runtime does not support the `image mount` feature, which is available since Docker v28.
+
## Copying files to a container
If you would like to copy a file to a container, you can do it in two different manners:
diff --git a/lifecycle.go b/lifecycle.go
index d938af2c8c..72363cccca 100644
--- a/lifecycle.go
+++ b/lifecycle.go
@@ -521,6 +521,20 @@ func (c ContainerLifecycleHooks) Terminated(ctx context.Context) func(container
}
func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req ContainerRequest, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) error {
+ var mountErrors []error
+ for _, m := range req.Mounts {
+ // validate only the mount sources that implement the Validator interface
+ if v, ok := m.Source.(Validator); ok {
+ if err := v.Validate(); err != nil {
+ mountErrors = append(mountErrors, err)
+ }
+ }
+ }
+
+ if len(mountErrors) > 0 {
+ return errors.Join(mountErrors...)
+ }
+
// prepare mounts
hostConfig.Mounts = mapToDockerMounts(req.Mounts)
diff --git a/lifecycle_test.go b/lifecycle_test.go
index bde9b96aef..02de785103 100644
--- a/lifecycle_test.go
+++ b/lifecycle_test.go
@@ -29,6 +29,32 @@ func TestPreCreateModifierHook(t *testing.T) {
require.NoError(t, err)
defer provider.Close()
+ t.Run("mount-errors", func(t *testing.T) {
+ // imageMounts {
+ req := ContainerRequest{
+ // three mounts, one valid and two invalid
+ Mounts: ContainerMounts{
+ {
+ Source: NewDockerImageMountSource("nginx:latest", "var/www/html"),
+ Target: "/var/www/valid",
+ },
+ ImageMount("nginx:latest", "../var/www/html", "/var/www/invalid1"),
+ ImageMount("nginx:latest", "/var/www/html", "/var/www/invalid2"),
+ },
+ }
+ // }
+
+ err = provider.preCreateContainerHook(ctx, req, &container.Config{}, &container.HostConfig{}, &network.NetworkingConfig{})
+ require.Error(t, err)
+
+ var errs []error
+ var joinErr interface{ Unwrap() []error }
+ if errors.As(err, &joinErr) {
+ errs = joinErr.Unwrap()
+ }
+ require.Len(t, errs, 2) // one valid and two invalid mounts
+ })
+
t.Run("No exposed ports", func(t *testing.T) {
// reqWithModifiers {
req := ContainerRequest{
diff --git a/modules/ollama/examples_test.go b/modules/ollama/examples_test.go
index f14ac19aee..c5a12d491c 100644
--- a/modules/ollama/examples_test.go
+++ b/modules/ollama/examples_test.go
@@ -2,6 +2,7 @@ package ollama_test
import (
"context"
+ "encoding/json"
"fmt"
"log"
"net/http"
@@ -243,3 +244,103 @@ func ExampleRun_withLocal() {
// Intentionally not asserting the output, as we don't want to run this example in the tests.
}
+
+func ExampleRun_withImageMount() {
+ cli, err := testcontainers.NewDockerClientWithOpts(context.Background())
+ if err != nil {
+ log.Printf("failed to create docker client: %s", err)
+ return
+ }
+
+ info, err := cli.Info(context.Background())
+ if err != nil {
+ log.Printf("failed to get docker info: %s", err)
+ return
+ }
+
+ // skip if the major version of the server is not v28 or greater
+ if info.ServerVersion < "28.0.0" {
+ log.Printf("skipping test because the server version is not v28 or greater")
+ return
+ }
+
+ ctx := context.Background()
+
+ ollamaContainer, err := tcollama.Run(ctx, "ollama/ollama:0.5.12")
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ defer func() {
+ if err := testcontainers.TerminateContainer(ollamaContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+
+ code, _, err := ollamaContainer.Exec(ctx, []string{"ollama", "pull", "all-minilm"})
+ if err != nil {
+ log.Printf("failed to pull model %s: %s", "all-minilm", err)
+ return
+ }
+
+ fmt.Println(code)
+
+ targetImage := "testcontainers/ollama:tc-model-all-minilm"
+
+ err = ollamaContainer.Commit(ctx, targetImage)
+ if err != nil {
+ log.Printf("failed to commit container: %s", err)
+ return
+ }
+
+ // start a new fresh ollama container mounting the target image
+ // mountImage {
+ newOllamaContainer, err := tcollama.Run(
+ ctx,
+ "ollama/ollama:0.5.12",
+ testcontainers.WithImageMount(targetImage, "root/.ollama/models/", "/root/.ollama/models/"),
+ )
+ // }
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ defer func() {
+ if err := testcontainers.TerminateContainer(newOllamaContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+
+ // perform an HTTP request to the ollama container to verify the model is available
+
+ connectionStr, err := newOllamaContainer.ConnectionString(ctx)
+ if err != nil {
+ log.Printf("failed to get connection string: %s", err)
+ return
+ }
+
+ resp, err := http.Get(connectionStr + "/api/tags")
+ if err != nil {
+ log.Printf("failed to get request: %s", err)
+ return
+ }
+
+ fmt.Println(resp.StatusCode)
+
+ type tagsResponse struct {
+ Models []struct {
+ Name string `json:"name"`
+ } `json:"models"`
+ }
+
+ var tags tagsResponse
+ err = json.NewDecoder(resp.Body).Decode(&tags)
+ if err != nil {
+ log.Printf("failed to decode response: %s", err)
+ return
+ }
+
+ fmt.Println(tags.Models[0].Name)
+
+ // Intentionally not asserting the output, as we don't want to run this example in the tests.
+}
diff --git a/mounts.go b/mounts.go
index a68e468b39..2e1d2c7e63 100644
--- a/mounts.go
+++ b/mounts.go
@@ -1,12 +1,16 @@
package testcontainers
-import "errors"
+import (
+ "errors"
+ "path/filepath"
+)
const (
MountTypeBind MountType = iota // Deprecated: Use MountTypeVolume instead
MountTypeVolume
MountTypeTmpfs
MountTypePipe
+ MountTypeImage
)
var (
@@ -18,6 +22,7 @@ var (
_ ContainerMountSource = (*GenericBindMountSource)(nil) // Deprecated: use Files or HostConfigModifier in the ContainerRequest, or copy files container APIs to make containers portable across Docker environments
_ ContainerMountSource = (*GenericVolumeMountSource)(nil)
_ ContainerMountSource = (*GenericTmpfsMountSource)(nil)
+ _ ContainerMountSource = (*GenericImageMountSource)(nil)
)
type (
@@ -110,6 +115,15 @@ func VolumeMount(volumeName string, mountTarget ContainerMountTarget) ContainerM
}
}
+// ImageMount returns a new ContainerMount with a GenericImageMountSource as source
+// This is a convenience method to cover typical use cases.
+func ImageMount(imageName string, subpath string, mountTarget ContainerMountTarget) ContainerMount {
+ return ContainerMount{
+ Source: NewGenericImageMountSource(imageName, subpath),
+ Target: mountTarget,
+ }
+}
+
// Mounts returns a ContainerMounts to support a more fluent API
func Mounts(mounts ...ContainerMount) ContainerMounts {
return mounts
@@ -124,3 +138,38 @@ type ContainerMount struct {
// ReadOnly determines if the mount should be read-only
ReadOnly bool
}
+
+// GenericImageMountSource implements ContainerMountSource and represents an image mount
+type GenericImageMountSource struct {
+ // imageName refers to the name of the image to be mounted
+ // the same image might be mounted to multiple locations within a single container
+ imageName string
+ // subpath is the path within the image to be mounted
+ subpath string
+}
+
+// NewGenericImageMountSource creates a new GenericImageMountSource
+func NewGenericImageMountSource(imageName string, subpath string) GenericImageMountSource {
+ return GenericImageMountSource{
+ imageName: imageName,
+ subpath: subpath,
+ }
+}
+
+// Source returns the name of the image to be mounted
+func (s GenericImageMountSource) Source() string {
+ return s.imageName
+}
+
+// Type returns the type of the mount
+func (GenericImageMountSource) Type() MountType {
+ return MountTypeImage
+}
+
+// Validate validates the source of the mount
+func (s GenericImageMountSource) Validate() error {
+ if !filepath.IsLocal(s.subpath) {
+ return errors.New("image mount source must be a local path")
+ }
+ return nil
+}
diff --git a/mounts_test.go b/mounts_test.go
index b1ac51d305..d155618b12 100644
--- a/mounts_test.go
+++ b/mounts_test.go
@@ -42,6 +42,38 @@ func TestVolumeMount(t *testing.T) {
}
}
+func TestImageMount(t *testing.T) {
+ t.Parallel()
+
+ t.Run("valid-image-mount", func(t *testing.T) {
+ t.Parallel()
+ m := testcontainers.ImageMount("nginx:latest", "var/www/html", "/var/www/html")
+ // the source is a GenericImageMountSource, which does implement the Validator interface
+ if v, ok := m.Source.(testcontainers.Validator); ok {
+ require.NoError(t, v.Validate())
+ }
+
+ require.Equal(t, testcontainers.ContainerMount{
+ Source: testcontainers.NewGenericImageMountSource("nginx:latest", "var/www/html"),
+ Target: "/var/www/html",
+ }, m)
+ })
+
+ t.Run("invalid-image-mount", func(t *testing.T) {
+ t.Parallel()
+ m := testcontainers.ImageMount("nginx:latest", "../var/www/html", "/var/www/invalid")
+ // the source is a GenericImageMountSource, which does implement the Validator interface
+ if v, ok := m.Source.(testcontainers.Validator); ok {
+ require.Error(t, v.Validate())
+ }
+
+ require.Equal(t, testcontainers.ContainerMount{
+ Source: testcontainers.NewGenericImageMountSource("nginx:latest", "../var/www/html"),
+ Target: "/var/www/invalid",
+ }, m)
+ })
+}
+
func TestContainerMounts_PrepareMounts(t *testing.T) {
volumeOptions := &mount.VolumeOptions{
Labels: testcontainers.GenericLabels(),
@@ -160,6 +192,25 @@ func TestContainerMounts_PrepareMounts(t *testing.T) {
},
},
},
+ {
+ name: "Image mount",
+ mounts: testcontainers.ContainerMounts{
+ {
+ Source: testcontainers.NewDockerImageMountSource("my-custom-image:latest", "data"),
+ Target: "/data",
+ },
+ },
+ want: []mount.Mount{
+ {
+ Source: "my-custom-image:latest",
+ Type: mount.TypeImage,
+ Target: "/data",
+ ImageOptions: &mount.ImageOptions{
+ Subpath: "data",
+ },
+ },
+ },
+ },
}
for _, tt := range tests {
tt := tt
diff --git a/options.go b/options.go
index b9cc6ed9b0..9afbcd7e39 100644
--- a/options.go
+++ b/options.go
@@ -357,6 +357,25 @@ func WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Stra
}
}
+// WithImageMount mounts an image to a container, passing the source image name,
+// the relative subpath to mount in that image, and the mount point in the target container.
+// This option validates that the subpath is a relative path, raising an error otherwise.
+func WithImageMount(source string, subpath string, target ContainerMountTarget) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ src := NewDockerImageMountSource(source, subpath)
+
+ if err := src.Validate(); err != nil {
+ return fmt.Errorf("validate image mount source: %w", err)
+ }
+
+ req.Mounts = append(req.Mounts, ContainerMount{
+ Source: src,
+ Target: target,
+ })
+ return nil
+ }
+}
+
// WithEntrypoint completely replaces the entrypoint of a container
func WithEntrypoint(entrypoint ...string) CustomizeRequestOption {
return func(req *GenericContainerRequest) error {
diff --git a/options_test.go b/options_test.go
index 88690e0599..34ac401b3d 100644
--- a/options_test.go
+++ b/options_test.go
@@ -552,6 +552,63 @@ func TestWithDockerfile(t *testing.T) {
require.Equal(t, map[string]*string{"ARG1": nil, "ARG2": nil}, req.BuildArgs)
}
+func TestWithImageMount(t *testing.T) {
+ cli, err := testcontainers.NewDockerClientWithOpts(context.Background())
+ require.NoError(t, err)
+
+ info, err := cli.Info(context.Background())
+ require.NoError(t, err)
+
+ // skip if the major version of the server is not v28 or greater
+ if info.ServerVersion < "28.0.0" {
+ t.Skipf("skipping test because the server version is not v28 or greater")
+ }
+
+ t.Run("valid", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ err := testcontainers.WithImageMount("alpine", "root/.ollama/models/", "/root/.ollama/models/")(&req)
+ require.NoError(t, err)
+
+ require.Len(t, req.Mounts, 1)
+
+ src := req.Mounts[0].Source
+
+ require.Equal(t, testcontainers.NewDockerImageMountSource("alpine", "root/.ollama/models/"), src)
+ require.Equal(t, "alpine", src.Source())
+ require.Equal(t, testcontainers.MountTypeImage, src.Type())
+
+ dst := req.Mounts[0].Target
+ require.Equal(t, testcontainers.ContainerMountTarget("/root/.ollama/models/"), dst)
+ })
+
+ t.Run("invalid", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ err := testcontainers.WithImageMount("alpine", "/root/.ollama/models/", "/root/.ollama/models/")(&req)
+ require.Error(t, err)
+ })
+
+ t.Run("invalid-dots", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ err := testcontainers.WithImageMount("alpine", "../root/.ollama/models/", "/root/.ollama/models/")(&req)
+ require.Error(t, err)
+ })
+}
+
func TestWithReuseByName_Succeeds(t *testing.T) {
t.Parallel()
req := &testcontainers.GenericContainerRequest{}
diff --git a/validator.go b/validator.go
new file mode 100644
index 0000000000..a888586e83
--- /dev/null
+++ b/validator.go
@@ -0,0 +1,7 @@
+package testcontainers
+
+// Validator is an interface that can be implemented by types that need to validate their state.
+type Validator interface {
+ // Validate validates the state of the type.
+ Validate() error
+}