Having some application that is ran within a container. Besides the rest it starts listening a unix socket within a pre mounted volume directory. Previously file is pre cleaned up if exists any.
The problem is that if application doesn't properly close the socket on exit (as expected next app instance removes previous socket) eventually the socket couldn't be removed from within the container failing with operation not supported
.
Anyway the same time the same problem socket could be freely removed from the host shell.
As example
main.go
package main
import (
"fmt"
"net"
"os"
"strconv"
"time"
)
func main() {
args := os.Args
socket := args[1]
err := os.Remove(socket)
if err != nil && !os.IsNotExist(err) {
panic(fmt.Errorf("socket remove error\n%w", err))
}
listener, err := net.Listen("unix", socket)
if err != nil {
panic(err)
}
fmt.Printf("listener %p\n", listener)
if len(args) > 2 {
sleepText := args[2]
sleepSeconds, err := strconv.Atoi(sleepText)
if err != nil {
panic(fmt.Errorf("wrong sleep seconds number %s\n%w", sleepText, err))
}
fmt.Printf("waiting %d seconds\n", sleepSeconds)
time.Sleep(time.Second * time.Duration(sleepSeconds))
}
}
Dockerfile
FROM golang:1.23.4-alpine3.21 AS builder
WORKDIR /app
COPY . .
RUN go mod init socktest
RUN go build .
ENTRYPOINT ["/app/socktest"]
Building
docker build -t socktest/test.local .
Running repeatedly
VOLUME=$HOME/tmp/socktest
docker run --rm -it -v "$VOLUME:$VOLUME" socktest/test.local $VOLUME/socket
eventually will fail with operation not supported
trying pre clean up previous socket.
What is interesting
- Adding after socket creation code to release the socket solves the problem
listener, err := net.Listen("unix", socket)
if err != nil {
panic(err)
}
// Release socket on exit
defer listener.Close()
As mentioned socket could be still freely removed from host shell
It is needed 3 application run cycles to fall into an issue.
- socket is created
- previous socket removed + recreated new one
- fails to remove second socket
If we will force application to wait a little and remove meanwhile the socket manually from within the container with the same volume. So the issue is not reproduced neither on the app flow nor at
rm
action it self regardless repeating the flow
docker run --rm -it -v "$VOLUME:$VOLUME" socktest/test.local $VOLUME/socket 15 # wait 15 seconds
# in other terminal
docker run --rm -it -v "$VOLUME:$VOLUME" --entrypoint rm socktest/test.local $VOLUME/socket
- It even works well if we remove the socket after the app is finished right within the container but at a separate process (not the same that would recreate the new socket instance)
for i in `seq 1 10`
do
docker run --rm -it -v "$VOLUME:$VOLUME" socktest/test.local $VOLUME/socket
docker run --rm -it -v "$VOLUME:$VOLUME" --entrypoint rm socktest/test.local $VOLUME/socket
done
It looks it matters that the socket is both was removed and recreated within the same containerized process to cause the issue.
So what in fact makes these socket files behave the different way (change any properties or attributes) after ungraceful socket release. And how actually gracefully remove it anyway afterwards within the container?
I believe macOS Docker Desktop is used matters here to be mentioned.
Thank you