Search code examples
dockervisual-studio-codedocker-composevscode-devcontainerdevcontainer

vscode devcontainers - why are two images built?


im using a remote container as my dev environment. so essentially in my project i have a Dockerfile, docker-compose.yml and my devcontainer.json that vscode uses to build, start and launch into the container with my code mounted. works great so far.
The thing is, on the linux host, if do a docker ps, i see my single container, but if look at the docker images, two are created, and the last one is the one started.

my docker-compose looks like this:


  app:
    container_name: my_app 
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ../..:/workspaces:cached

and my .devcontainer.json looks like this:

    "name": "my_app_service",
    "dockerComposeFile": "docker-compose.yml",
    "service": "app",
    "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
    "customizations": { 
        "vscode": {
            "extensions": ["ms-python.python"]
        },
        "settings": {
            "terminal.integrated.shell.*": "/bin/bash"
        }
    }     

docker ps shows a single container running with:

container name: my_app
image: vsc-my_proj-64ad9a6809f39eaa523af6a319f22b90b2d1358fe3a8aebc93ac1dfaebf5f22a-uid 

docker images shows two images created, with names:

REPOSITORY: my_app_devcontainer-app 
REPOSITORY: vsc-my_proj-64ad9a6809f39eaa523af6a319f22b90b2d1358fe3a8aebc93ac1dfaebf5f22a-uid

(note in the above 'my_proj' part of the image name comes from my the vscode project folder name i believe.) i can delete the first image. but i want to understand why this is happening? doesnt seem like it should - its almost like the build is happening twice. Also, is there a way to specify an image name using docker-compose, or in vscode using the devcontainer.json (i know how to do it from docker-build command line arg).

Thanks for any help in figuring this out


Solution

  • I was wondering about the same thing, so I did the following:

    Steps

    1. I printed out the image history, only printing out the column that shows the actual docker file statements. See https://docs.docker.com/config/formatting/ and https://docs.docker.com/engine/reference/commandline/history/
    docker image history PUTIMAGE_HASH_HERE_FOR_IMAGE_NOT_ENDING_IN_UID --no-trunc --format "table {{.CreatedBy}}" > image_wo_uid.txt
    
    docker image history PUTIMAGE_HASH_HERE_FOR_IMAGE_WITH_UID_AT_END --no-trunc --format "table {{.CreatedBy}}" > image_uid.txt
    
    1. Then diff the two: diff image_wo_uid.txt image_uid.txt

    2. That gives you something like (note: reverse chronological order):

    1a2,9
    > USER vscode
    > ARG IMAGE_USER
    > RUN |3 REMOTE_USER=vscode NEW_UID=1000 NEW_GID=1000 /bin/sh -c eval $(sed -n "s/${REMOTE_USER}:[^:]*:\([^:]*\):\([^:]*\):[^:]*:\([^:]*\).*/OLD_UID=\1;OLD_GID=\2;HOME_FOLDER=\3/p" /etc/passwd);  eval $(sed -n "s/\([^:]*\):[^:]*:${NEW_UID}:.*/EXISTING_USER=\1/p" /etc/passwd);  eval $(sed -n "s/\([^:]*\):[^:]*:${NEW_GID}:.*/EXISTING_GROUP=\1/p" /etc/group);  if [ -z "$OLD_UID" ]; then   echo "Remote user not found in /etc/passwd ($REMOTE_USER).";  elif [ "$OLD_UID" = "$NEW_UID" -a "$OLD_GID" = "$NEW_GID" ]; then   echo "UIDs and GIDs are the same ($NEW_UID:$NEW_GID).";  elif [ "$OLD_UID" != "$NEW_UID" -a -n "$EXISTING_USER" ]; then   echo "User with UID exists ($EXISTING_USER=$NEW_UID).";  elif [ "$OLD_GID" != "$NEW_GID" -a -n "$EXISTING_GROUP" ]; then   echo "Group with GID exists ($EXISTING_GROUP=$NEW_GID).";  else   echo "Updating UID:GID from $OLD_UID:$OLD_GID to $NEW_UID:$NEW_GID.";   sed -i -e "s/\(${REMOTE_USER}:[^:]*:\)[^:]*:[^:]*/\1${NEW_UID}:${NEW_GID}/" /etc/passwd;   if [ "$OLD_GID" != "$NEW_GID" ]; then    sed -i -e "s/\([^:]*:[^:]*:\)${OLD_GID}:/\1${NEW_GID}:/" /etc/group;   fi;   chown -R $NEW_UID:$NEW_GID $HOME_FOLDER;  fi; # buildkit
    > SHELL [/bin/sh -c]
    > ARG NEW_GID
    > ARG NEW_UID
    > ARG REMOTE_USER
    > USER root
    

    Conclusion

    • The image ending in uid is switching to a non-root user and is taking extra measures to guarantee that the uid and guid are each 1000 (see the long RUN statement). It has a bunch of error handling to output an error if another user/group already exists at 1000.
    • I'm guessing the devcontainer programmers are doing this because they can't be sure what base image is being used and thus USER vscode might not be at 1000/1000 for uid/guid.

    Notes

    • Details can vary by Linux distro (I think), but basically, if you guarantee your user ids/group ids are at 1000 or above you are in 'non-system user' (=regular user) uid space. Example discussion: https://ubuntuforums.org/showthread.php?t=1740376
    • Some people recommend moving to 10000, not 1000, to have a lower probability of running into a pre-existing user at the 1000 range, which is quite likely, since the first non-system user will be at 1000, if no other steps are taken. Example discussion: https://news.ycombinator.com/item?id=25621610
    • I'm not sure why the extra code doesn't first determine the first available uids/guids above 1000 and use those instead of 1000, just in case 1000 is already taken.