Search code examples
dockervisual-studio-codesshdockerfilevscode-remote

How to connect to VSCode container locally using ssh?


Using VSCode Dev Container, I would like to be able to ssh from within my container to inside my container (for testing purposes).

ssh root@localhost

I have read many articles and similar questions, but I am unable to create a minimal functional example.

My Dockerfile is as follow:

FROM ubuntu:18.04

RUN apt-get update && apt-get install -y --no-install-recommends net-tools iputils-ping openssh-client openssh-server 


RUN mkdir /var/run/sshd
RUN echo 'root:screencast' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

My devcontainer.json is as follow:

{
    "name": "Ubuntu",
    "build": {
        "dockerfile": "Dockerfile",
    },
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
    },
    "extensions": [],
    "forwardPorts": [
        22
    ],
    "appPort": 22,
    "runArgs": [
        "--net",
        "host",
        "-p",
        "22:22"
    ]
}

I tested multiple combinations of parameters (forwardPorts, appPort, EXPOSE, etc.) but every time either:

  • the ssh connection is refused
  • I connect to my host and not to my container

Do you know how could I modify these files in order to be able to connect with ssh from the container's bash interpreter please?


Solution

  • There are several issues to address:

    1. Since your host is using port 22 you have to use another one. You can do this with appPort:
        "appPort": "2222:22",
    

    This notation maps host's port 2222 to container's 22.

    1. runArgs and forwardPorts are redundant.

    2. You need to add "overrideCommand": false to prevent VSCode overriding CMD declared in the Dockerfile.

    3. Your sed in Dockerfile is incorrect, default config does not contain a line PermitRootLogin prohibit-password but it contains #PermitRootLogin <some-other-value. Change sed command to this:

    RUN sed -i 's/.*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
    

    And here are modified files for convenience:

    Dockerfile:

    FROM ubuntu:18.04
    
    RUN apt-get update && apt-get install -y --no-install-recommends net-tools iputils-ping openssh-client openssh-server 
    
    
    RUN mkdir /var/run/sshd
    RUN echo 'root:test' | chpasswd
    RUN sed -i 's/.*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
    
    # SSH login fix. Otherwise user is kicked off after login
    RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
    
    ENV NOTVISIBLE "in users profile"
    RUN echo "export VISIBLE=now" >> /etc/profile
    
    EXPOSE 22
    CMD ["/usr/sbin/sshd", "-D"]
    

    devcontainer.json:

    {
        "name": "Ubuntu",
        "build": {
            "dockerfile": "Dockerfile",
        },
        "settings": {
            "terminal.integrated.shell.linux": "/bin/bash"
        },
        "extensions": [],
        "appPort": "2222:22",
        "overrideCommand": false
    }
    

    When you run the container you can connect to it with ssh root@localhost -p 2222 and password 'test'.

    Also, I don't know why you decided to go with VSCode specific way to Docker, maybe there is a solid reason to do this, but there is a better way. You can use docker-compose to create a testing environment. It is:

    • better documented;
    • widely used;
    • supported by many IDE's (including VSCode).

    Take a look at this docker-compose.yml:

    # Check out this reference https://docs.docker.com/compose/compose-file/
    # for list of available versions, their differences, and the file format in general.
    version: "3.0"
    
    # This is where you declare containers you want to run.
    services:
    
      # This is the name of the service. One cool thing about it is that is will be a DNS name
      # in the networks where this service will be present. So when you need to connect this
      # service from another container you can simply do 'ssh username@ssh-server'.
      ssh-server:
    
        # This is the name of the image to use. In this case I intentionally used a nonexistent name.
        # Because of that when Docker will build the image from the Dockerfile, it will assign this
        # name to the image. This is not required since I've added 'build' property but giving the
        # right name could come handy.
        image: myssh
    
        # This is equivalent to 'build an image from the Dockerfile in current working directory' or
        # 'docker build .'
        build:
          context: .
          dockerfile: Dockerfile
    
        # This maps host's port 2222 to container's 22. This isn't necessary unless you want to connect
        # to this container from outside (e.g. from host or another machine). Containers do not
        # require 'exposure' or any other step to reach one another within one network - they have all
        # ports open. That is why it is called port forwarding or mapping.
        ports:
          - "2222:22"
    
      # Same image as the server but with a different command to execute.
      ssh-client:
        image: myssh
        build:
          context: .
        # Just a loop to run a command every second. Won't work with password, you need a key or some hacks.
        command: bash -c 'while sleep 1; do ssh root@ssh-server ls /; done'
    

    If you save it to a directory with the Dockerfile above, you can run it with docker-compose up. Or you can integrate it with VSCode: when there is no .devcontainer directory and you click Reopen in container, you can select From 'docker-compose.yml', then select one of the services you want and it will build and start a container. It will also create .devcontainer directory with devcontainer.json in it.