diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md
index 3a62eb1802..c37a9d77bd 100644
--- a/docs/features/common_functional_options.md
+++ b/docs/features/common_functional_options.md
@@ -375,6 +375,24 @@ ctr, err = mymodule.Run(ctx, "docker.io/myservice:1.2.3", testcontainers.WithHos
To understand more about this feature, please read the [Exposing host ports to the container](/features/networking/#exposing-host-ports-to-the-container) documentation.
+##### WithReadOnlyRootFilesystem
+
+- Since :material-tag: v0.40.0
+
+If you need to run a container with a read-only root filesystem for enhanced security, you can use `testcontainers.WithReadOnlyRootFilesystem`. This is equivalent to using the `--read-only` flag with `docker run`:
+
+```golang
+ctr, err = mymodule.Run(ctx, "docker.io/myservice:1.2.3", testcontainers.WithReadOnlyRootFilesystem())
+```
+
+This option mounts the container's root filesystem as read-only, preventing any writes to the root filesystem. This is useful for security hardening and ensuring that your application doesn't write to unexpected locations. If your application needs to write temporary files, you can combine this with `WithTmpfs` to provide writable temporary directories:
+
+```golang
+ctr, err = mymodule.Run(ctx, "docker.io/myservice:1.2.3",
+ testcontainers.WithReadOnlyRootFilesystem(),
+ testcontainers.WithTmpfs(map[string]string{"/tmp": "rw,noexec,nosuid,size=100m"}))
+```
+
##### WithConfigModifier
- Since :material-tag: v0.20.0
diff --git a/docs/features/common_functional_options_list.md b/docs/features/common_functional_options_list.md
index e37a698d30..bf15d90c2b 100644
--- a/docs/features/common_functional_options_list.md
+++ b/docs/features/common_functional_options_list.md
@@ -54,6 +54,7 @@ The following options are exposed by the `testcontainers` package.
### Advanced Options
- [`WithHostPortAccess`](/features/creating_container/#withhostportaccess) Since :material-tag: v0.31.0
+- [`WithReadOnlyRootFilesystem`](/features/creating_container/#withreadonlyrootfilesystem) Since :material-tag: v0.40.0
- [`WithConfigModifier`](/features/creating_container/#withconfigmodifier) Since :material-tag: v0.20.0
- [`WithHostConfigModifier`](/features/creating_container/#withhostconfigmodifier) Since :material-tag: v0.20.0
- [`WithEndpointSettingsModifier`](/features/creating_container/#withendpointsettingsmodifier) Since :material-tag: v0.20.0
diff --git a/examples/readonly/go.mod b/examples/readonly/go.mod
new file mode 100644
index 0000000000..8b00dedf51
--- /dev/null
+++ b/examples/readonly/go.mod
@@ -0,0 +1,7 @@
+module readonly-example
+
+go 1.24
+
+replace github.com/testcontainers/testcontainers-go => ../..
+
+require github.com/testcontainers/testcontainers-go v0.0.0-00010101000000-000000000000
\ No newline at end of file
diff --git a/examples/readonly/main.go b/examples/readonly/main.go
new file mode 100644
index 0000000000..5d6749ea44
--- /dev/null
+++ b/examples/readonly/main.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Example 1: Container with read-only root filesystem
+ fmt.Println("=== Example 1: Read-only root filesystem ===")
+
+ container, err := testcontainers.Run(ctx, "alpine:latest",
+ testcontainers.WithReadOnlyRootFilesystem(),
+ testcontainers.WithCmd("sh", "-c", "echo 'Attempting to write to root filesystem...' && echo 'test' > /test.txt && echo 'Write succeeded' || echo 'Write failed (expected)'"),
+ testcontainers.WithWaitStrategy(wait.ForExit()),
+ )
+ if err != nil {
+ log.Fatalf("Failed to start container: %v", err)
+ }
+ defer func() {
+ if err := testcontainers.TerminateContainer(container); err != nil {
+ log.Printf("Failed to terminate container: %v", err)
+ }
+ }()
+
+ // Get the logs
+ logs, err := container.Logs(ctx)
+ if err != nil {
+ log.Fatalf("Failed to get logs: %v", err)
+ }
+ defer logs.Close()
+
+ logBytes, err := io.ReadAll(logs)
+ if err != nil {
+ log.Fatalf("Failed to read logs: %v", err)
+ }
+
+ fmt.Printf("Container output:\n%s\n", string(logBytes))
+
+ // Example 2: Read-only root filesystem with tmpfs for writable areas
+ fmt.Println("=== Example 2: Read-only root filesystem with tmpfs ===")
+
+ container2, err := testcontainers.Run(ctx, "alpine:latest",
+ testcontainers.WithReadOnlyRootFilesystem(),
+ testcontainers.WithTmpfs(map[string]string{"/tmp": "rw,noexec,nosuid,size=100m"}),
+ testcontainers.WithCmd("sh", "-c", "echo 'Attempting to write to /tmp (tmpfs)...' && echo 'test' > /tmp/test.txt && echo 'Write to tmpfs succeeded' || echo 'Write to tmpfs failed'"),
+ testcontainers.WithWaitStrategy(wait.ForExit()),
+ )
+ if err != nil {
+ log.Fatalf("Failed to start container: %v", err)
+ }
+ defer func() {
+ if err := testcontainers.TerminateContainer(container2); err != nil {
+ log.Printf("Failed to terminate container: %v", err)
+ }
+ }()
+
+ // Get the logs
+ logs2, err := container2.Logs(ctx)
+ if err != nil {
+ log.Fatalf("Failed to get logs: %v", err)
+ }
+ defer logs2.Close()
+
+ logBytes2, err := io.ReadAll(logs2)
+ if err != nil {
+ log.Fatalf("Failed to read logs: %v", err)
+ }
+
+ fmt.Printf("Container output:\n%s\n", string(logBytes2))
+
+ // Verify the containers were configured correctly
+ inspect1, err := container.Inspect(ctx)
+ if err != nil {
+ log.Fatalf("Failed to inspect container: %v", err)
+ }
+
+ inspect2, err := container2.Inspect(ctx)
+ if err != nil {
+ log.Fatalf("Failed to inspect container: %v", err)
+ }
+
+ fmt.Printf("Container 1 ReadonlyRootfs: %t\n", inspect1.HostConfig.ReadonlyRootfs)
+ fmt.Printf("Container 2 ReadonlyRootfs: %t\n", inspect2.HostConfig.ReadonlyRootfs)
+ fmt.Printf("Container 2 Tmpfs mounts: %v\n", inspect2.HostConfig.Tmpfs)
+}
\ No newline at end of file
diff --git a/options.go b/options.go
index a930c54104..ca20bd71ee 100644
--- a/options.go
+++ b/options.go
@@ -546,3 +546,24 @@ func WithProvider(provider ProviderType) CustomizeRequestOption {
return nil
}
}
+
+// WithReadOnlyRootFilesystem sets the container's root filesystem as read-only.
+// This is equivalent to using the --read-only flag with docker run.
+func WithReadOnlyRootFilesystem() CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ if req.HostConfigModifier == nil {
+ req.HostConfigModifier = func(hostConfig *container.HostConfig) {
+ hostConfig.ReadonlyRootfs = true
+ }
+ } else {
+ // Wrap the existing modifier to also set ReadonlyRootfs
+ existingModifier := req.HostConfigModifier
+ req.HostConfigModifier = func(hostConfig *container.HostConfig) {
+ existingModifier(hostConfig)
+ hostConfig.ReadonlyRootfs = true
+ }
+ }
+
+ return nil
+ }
+}
diff --git a/options_test.go b/options_test.go
index 8e58946f68..0b5034b7ac 100644
--- a/options_test.go
+++ b/options_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"time"
+ "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
@@ -933,3 +934,43 @@ func TestWithProvider(t *testing.T) {
require.Equal(t, testcontainers.ProviderPodman, req.ProviderType)
})
}
+
+func TestWithReadOnlyRootFilesystem(t *testing.T) {
+ t.Run("sets ReadonlyRootfs to true when no existing HostConfigModifier", func(t *testing.T) {
+ req := &testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ opt := testcontainers.WithReadOnlyRootFilesystem()
+ require.NoError(t, opt.Customize(req))
+ require.NotNil(t, req.HostConfigModifier)
+
+ // Test that the modifier sets ReadonlyRootfs to true
+ hostConfig := &container.HostConfig{}
+ req.HostConfigModifier(hostConfig)
+ require.True(t, hostConfig.ReadonlyRootfs)
+ })
+
+ t.Run("preserves existing HostConfigModifier and sets ReadonlyRootfs", func(t *testing.T) {
+ req := &testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ HostConfigModifier: func(hc *container.HostConfig) {
+ hc.Privileged = true
+ },
+ },
+ }
+
+ opt := testcontainers.WithReadOnlyRootFilesystem()
+ require.NoError(t, opt.Customize(req))
+ require.NotNil(t, req.HostConfigModifier)
+
+ // Test that the modifier preserves existing settings and sets ReadonlyRootfs
+ hostConfig := &container.HostConfig{}
+ req.HostConfigModifier(hostConfig)
+ require.True(t, hostConfig.Privileged)
+ require.True(t, hostConfig.ReadonlyRootfs)
+ })
+}
diff --git a/readonly_integration_test.go b/readonly_integration_test.go
new file mode 100644
index 0000000000..570a52c4de
--- /dev/null
+++ b/readonly_integration_test.go
@@ -0,0 +1,79 @@
+package testcontainers_test
+
+import (
+ "context"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+func TestWithReadOnlyRootFilesystem_Integration(t *testing.T) {
+ ctx := context.Background()
+
+ // Test that a container with read-only root filesystem cannot write to the root filesystem
+ container, err := testcontainers.Run(ctx, "alpine:latest",
+ testcontainers.WithReadOnlyRootFilesystem(),
+ testcontainers.WithCmd("sh", "-c", "echo 'test' > /test.txt && echo 'success' || echo 'failed'"),
+ testcontainers.WithWaitStrategy(wait.ForExit()),
+ )
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, testcontainers.TerminateContainer(container))
+ }()
+
+ // Get the logs to verify the write operation failed
+ logs, err := container.Logs(ctx)
+ require.NoError(t, err)
+ defer logs.Close()
+
+ logBytes, err := io.ReadAll(logs)
+ require.NoError(t, err)
+ logContent := string(logBytes)
+
+ // The write operation should fail because the root filesystem is read-only
+ require.Contains(t, logContent, "failed")
+ require.NotContains(t, logContent, "success")
+
+ // Verify the container was actually configured with read-only root filesystem
+ inspect, err := container.Inspect(ctx)
+ require.NoError(t, err)
+ require.True(t, inspect.HostConfig.ReadonlyRootfs)
+}
+
+func TestWithReadOnlyRootFilesystem_WithTmpfs_Integration(t *testing.T) {
+ ctx := context.Background()
+
+ // Test that a container with read-only root filesystem can still write to tmpfs mounts
+ container, err := testcontainers.Run(ctx, "alpine:latest",
+ testcontainers.WithReadOnlyRootFilesystem(),
+ testcontainers.WithTmpfs(map[string]string{"/tmp": "rw,noexec,nosuid,size=100m"}),
+ testcontainers.WithCmd("sh", "-c", "echo 'test' > /tmp/test.txt && echo 'success' || echo 'failed'"),
+ testcontainers.WithWaitStrategy(wait.ForExit()),
+ )
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, testcontainers.TerminateContainer(container))
+ }()
+
+ // Get the logs to verify the write operation succeeded in tmpfs
+ logs, err := container.Logs(ctx)
+ require.NoError(t, err)
+ defer logs.Close()
+
+ logBytes, err := io.ReadAll(logs)
+ require.NoError(t, err)
+ logContent := string(logBytes)
+
+ // The write operation should succeed because /tmp is mounted as tmpfs
+ require.Contains(t, logContent, "success")
+ require.NotContains(t, logContent, "failed")
+
+ // Verify the container was configured with read-only root filesystem
+ inspect, err := container.Inspect(ctx)
+ require.NoError(t, err)
+ require.True(t, inspect.HostConfig.ReadonlyRootfs)
+}
\ No newline at end of file