Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions cmd/nerdctl/compose/compose_up_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,73 @@ services:
base.Cmd("images").AssertOutNotContains(testutil.CommonImage)
base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertExitCode(1)
}

func TestComposeTmpfsVolume(t *testing.T) {
testCase := nerdtest.Setup()

testCase.Setup = func(data test.Data, helpers test.Helpers) {
containerName := data.Identifier("tmpfs")
composeYAML := fmt.Sprintf(`
services:
tmpfs:
container_name: %s
image: %s
command: sleep infinity
volumes:
- type: tmpfs
target: /target-rw
tmpfs:
size: 64m
- type: tmpfs
target: /target-ro
read_only: true
tmpfs:
size: 64m
mode: 0o1770
`, containerName, testutil.CommonImage)

composeYAMLPath := data.Temp().Save(composeYAML, "compose.yaml")

helpers.Ensure("compose", "-f", composeYAMLPath, "up", "-d")
nerdtest.EnsureContainerStarted(helpers, containerName)

data.Labels().Set("composeYAML", composeYAMLPath)
data.Labels().Set("containerName", containerName)
}

testCase.SubTests = []*test.Case{
{
Description: "rw tmpfs mount",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-rw", "/proc/mounts")
},
Expected: test.Expects(0, nil,
expect.All(
expect.Contains("/target-rw"),
expect.Contains("rw"),
expect.Contains("size=65536k"),
),
),
},
{
Description: "ro tmpfs mount with mode",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-ro", "/proc/mounts")
},
Expected: test.Expects(0, nil,
expect.All(
expect.Contains("/target-ro"),
expect.Contains("ro"),
expect.Contains("size=65536k"),
expect.Contains("mode=1770"),
),
),
},
}

testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down")
}

testCase.Run(t)
}
33 changes: 32 additions & 1 deletion pkg/composer/serviceparser/serviceparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,14 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
if err != nil {
return nil, err
}
c.RunArgs = append(c.RunArgs, "-v="+vStr)

switch v.Type {
case "tmpfs":
c.RunArgs = append(c.RunArgs, "--tmpfs="+vStr)
default:
c.RunArgs = append(c.RunArgs, "-v="+vStr)
}

c.Mkdir = mkdir
}

Expand Down Expand Up @@ -778,6 +785,7 @@ func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Proj
"ReadOnly",
"Bind",
"Volume",
"Tmpfs",
); len(unknown) > 0 {
log.L.Warnf("Ignoring: volume: %+v", unknown)
}
Expand All @@ -800,6 +808,29 @@ func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Proj
return "", nil, fmt.Errorf("volume target must be an absolute path, got %q", c.Target)
}

if c.Type == "tmpfs" {
var opts []string

if c.ReadOnly {
opts = append(opts, "ro")
}
if c.Tmpfs != nil {
if c.Tmpfs.Size != 0 {
opts = append(opts, fmt.Sprintf("size=%d", c.Tmpfs.Size))
}
if c.Tmpfs.Mode != 0 {
opts = append(opts, fmt.Sprintf("mode=%o", c.Tmpfs.Mode))
}
}

s := c.Target
if len(opts) > 0 {
s = fmt.Sprintf("%s:%s", s, strings.Join(opts, ","))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not specific to nerdctl, but what happens when c.Target contains a colon?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand you are commenting on the behavior when multiple paths are specified for the target.

In this case, the target contains commas (,), and each path becomes a mount target.

For example, I verified the behavior using the following compose.yml:

Details

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    command: ["sleep", "infinity"]
    volumes:
      - type: tmpfs
        target: /app,/app2
        read_only: false
        tmpfs:
          size: 2G
          mode: 0o1770

As shown in the results below, even when the target contains commas (,), it can still mount to multiple targets.

$ sudo nerdctl exec -it nginx mount | grep app
tmpfs on /app,/app2 type tmpfs (rw,nosuid,nodev,noexec,relatime,size=2097152k,inode64)
Details

$ sudo nerdctl compose up
...
INFO[0000] Running [/usr/local/bin/nerdctl run --cidfile=/tmp/compose-2021655771/cid -l=com.docker.compose.project=nerdctl -l=com.docker.compose.service=nginx -l=com.docker.compose.config-hash=0ac25952231d47d034f2ad52022064442f5b1df9857ef9b3a2548496795a3112 -d --name=nginx --pull=never --net=nerdctl_default --hostname=nginx --restart=no --tmpfs=/app,/app2:size=2147483648 nginx:latest sleep infinity]
...

Copy link
Member

@AkihiroSuda AkihiroSuda Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment was about colon (:), not comma (,).

Anyway, docker doesn't seem to support it either

$ docker run --tmpfs "/foo:bar" busybox ls -ld "/foo:bar"
docker: Error response from daemon: invalid tmpfs option ["bar"]

Run 'docker run --help' for more information

$ docker run --tmpfs "/foo\:bar" busybox ls -ld "/foo:bar"
docker: Error response from daemon: invalid tmpfs option ["bar"]

Run 'docker run --help' for more information

}

return s, mkdir, nil
}

if c.Source == "" {
// anonymous volume
s := c.Target
Expand Down
37 changes: 37 additions & 0 deletions pkg/composer/serviceparser/serviceparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,43 @@ services:
}
}

func TestTmpfsVolumeLongSyntax(t *testing.T) {
t.Parallel()

if runtime.GOOS == "windows" {
t.Skip("test is not compatible with windows")
}

const dockerComposeYAML = `
services:
foo:
image: nginx:alpine
volumes:
- type: tmpfs
target: /target
read_only: true
tmpfs:
size: 2G
mode: 0o1770
`
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()

project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)

fooSvc, err := project.GetService("foo")
assert.NilError(t, err)

foo, err := Parse(project, fooSvc)
assert.NilError(t, err)

t.Logf("foo: %+v", foo)
for _, c := range foo.Containers {
assert.Assert(t, in(c.RunArgs, "--tmpfs=/target:ro,size=2147483648,mode=1770"))
}
}

func TestParseNetworkMode(t *testing.T) {
t.Parallel()
const dockerComposeYAML = `
Expand Down
Loading