Use Docker and SSH to Work Around Old Ubuntu Dev Environment Compatibility Issues

Ubuntu 18.04's glibc is too old for newer VS Code Remote SSH Server and some Node.js binaries. Run the dev environment in Docker and SSH into it.

The Problem

I recently got access to a remote development machine that was still on Ubuntu 18.04 LTS, with glibc 2.27. The system itself worked, but newer development tools were already running into that system-library boundary:

  • Newer versions of VS Code connect through Remote SSH by starting VS Code Server on the remote machine. That server has its own glibc requirement, and Ubuntu 18.04’s glibc 2.27 is too old, so the remote install or startup can fail.
  • Newer Linux x64 prebuilt binaries of Node.js also depend on a newer glibc. On Ubuntu 18.04, installing through nvm or a downloaded binary may hit errors such as GLIBC_2.28 not found; the alternatives are using an older Node.js version or compiling it yourself.
  • fastfetch is not painless on Ubuntu 18.04 either. You need to compile the executable yourself.

So the problem is not just that one package refuses to install. The deeper issue is that the old system libraries no longer match the default assumptions of newer toolchains.

If you fully control the machine, upgrading the system is the cleanest answer. But internship, lab, or company-provided development machines often come with extra constraints: you may not be allowed to change the OS version, and you may not want to mutate the original environment into something hard to roll back. That leads to the second route: keep the host unchanged, put the development environment inside a Docker container, and SSH directly into that container.

Two Options

There are two choices here:

  1. Upgrade the remote host directly.
  2. Run a Docker container on the remote host, then SSH directly into that container.

The first option changes the whole machine. It is easiest to maintain afterward, but only if you have permission and can accept the impact of the upgrade. The second option only uses Docker to start a newer environment on top of the old system. It is less invasive and easier to copy to the next machine.

Prompt notation

> - LocalHost
$ - Remote Host (Ubuntu 18)
# - Docker Container

Below, I first include the system-upgrade path, then focus on the Docker SSH forwarding setup.

Option 1: Upgrade the Remote Host

If you control the whole machine, upgrading Ubuntu is the most direct fix. The following commands upgrade the system to the next LTS release, with fairly detailed interactive prompts along the way.

# Update packages$ sudo apt-get update$ sudo apt-get upgrade$ sudo apt-get dist-upgrade# Install the upgrade tool$ sudo apt install update-manager-core# Edit the upgrade config file, set the last line to lts, then save and exit$ sudo vim /etc/update-manager/release-upgradesPrompt=lts# Run the upgrade. It will ask whether to reboot, or you can reboot manually$ sudo do-release-upgrade$ lsb_release -aNo LSB modules are available.Distributor ID: UbuntuDescription:    Ubuntu 20.04.6 LTSRelease:        20.04Codename:       focal

Option 2: SSH into a Docker Container

The Docker approach is simple: the old system only runs Docker and forwards ports; the real development environment lives inside the container. Inside the container, you can use a newer Ubuntu, Debian, or a project-specific base image. From your local machine, you still connect through SSH.

The benefit is similar to using Docker as a development environment in general: the environment can be standardized, the image can be copied, and the host stays cleaner. The difference is that this setup also runs an SSH service inside the container, so tools like VS Code Remote SSH can connect to it as if it were a normal server.

Build an SSH-Ready Image

First, create a directory for building the Docker image, then copy directories such as .ssh, .config, and .vscode-server into it. Most of these settings can be reused, especially SSH keys and common shell configuration.

SSH Forwarding
SSH Forwarding

The image needs to do a few things:

  • Install necessary tools: vim, SSH server, fish shell, and fzf.
  • Configure SSH to allow root login and set a temporary password.
  • Copy the host’s SSH key into the container, so logging in does not require a password.
  • Expose port 22, which the host can later map to another port.
Security boundary

The root:root password and PermitRootLogin yes setting below are suitable for a temporary internal development environment. For long-term use, it is better to create a separate user, disable password login, and keep only key-based login.

FROM artifactory.momenta.works/docker/python:3.11# Install vim and other required packagesRUN apt-get update && \    apt-get install -y vim openssh-server fish fzf# Copy SSH and shell configCOPY ./.ssh /root/.ssh/COPY ./.config /root/.config/# Skip the first-connection confirmationRUN echo "Host *\n\tHostKeyAlgorithms +ssh-rsa\n\tPubkeyAcceptedKeyTypes +ssh-rsa\n\tStrictHostKeyChecking no" > /root/.ssh/configRUN sed -i 's/^#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_configRUN echo 'root:root' | chpasswdRUN chmod 600 /root/.ssh/id_rsaRUN mkdir /var/run/sshdEXPOSE 22# Start the SSH daemon serviceENTRYPOINT service ssh start && tail -f /dev/null

Writing the entire image configuration into a Dockerfile in one pass can be annoying. One practical workflow is to start with a simple Dockerfile, create a container, enter the container shell, and install the required environment there. After the basic development environment is ready, use docker commit to package the container into a new image.

# Create the container$ docker run -it --name dev-image --hostname=dk --privileged=true --net=bridge -p 8033:22 debian:bookworm# Enter the container$ docker exec -it dev-image /bin/bash# Install the required environment$ apt-get update && \    apt update && \    apt install <xxx># Exit the container$ exit# Commit the container as a new image$ docker commit dev-image dev-image:latest

This is more like drafting the environment by hand inside the container: first find a setup that works, then freeze the stable version as an image. Once the dependencies are clear, it is much easier to go back and clean up the Dockerfile.

Build and Run the Container

After the Dockerfile is ready, build the image:

$ docker build -t dev-image:latest .

Then run the container and map the container’s port 22 to port 8033 on the Ubuntu host:

$ docker run -it --name ssh --hostname=dk --privileged=true --net=bridge -p 8033:22 dev-image:latest

The parameters mean:

  • --name ssh: set the container name to ssh.
  • --hostname=dk: set the container hostname.
  • --privileged=true: give the container privileged mode.
  • --net=bridge: use bridge networking.
  • -p 8033:22: map container port 22 to host port 8033.
Mounting volumes

If you need persistent data, mount a volume with -v, for example: -v /path/on/host:/path/in/container.

After the container starts, you can also enter it from the host first and check the SSH service:

$ docker exec -it <container-name> /bin/bash$ docker exec -it <container-name> /bin/fish

Configure Local SSH

Once the container port is mapped, you only need to add one more SSH Host locally. In the example below, ubuntu points to the host machine, while docker points to port 8033 on the same host, which is the container’s port 22.

Edit ~/.ssh/config:

# Read more about SSH config files: https://linux.die.net/man/5/ssh_configHost ubuntu    HostName 10.21.163.77    User root    IdentityFile C:/Users/gjx/.ssh/id_rsa    StrictHostKeyChecking noHost docker    HostName 10.21.163.77    User root    Port 8033

After that, run ssh docker locally, or choose docker in VS Code Remote SSH. You will be connected to the Docker container.

How It Works

This connection has only two forwarding layers:

  1. Windows first connects to port 8033 on the Ubuntu host.
  2. The Ubuntu host forwards port 8033 to port 22 inside the Docker container.

From the local machine’s perspective, docker is just a normal SSH Host. From the host’s perspective, it only forwards port traffic to a container. Once that model is clear, extending it to multiple containers is natural.

When you need to connect to multiple Docker containers, map each container to a different port, then add corresponding entries to your SSH config:

Host docker1    HostName 10.21.163.77    User root    Port 8033Host docker2    HostName 10.21.163.77    User root    Port 8034

Wrap-Up

glibc compatibility problems on old systems do not always have to be solved by upgrading the host. As long as the host can run Docker reliably, you can move the development environment into a container, then connect it back to your local workflow through port mapping and SSH config.

This is not the “purest” setup: running SSH inside a container, allowing root login, and exposing mapped ports all require care around security boundaries. But when you cannot freely upgrade the remote host and still want to use newer development tools, it is a direct and easy-to-rollback compromise.

CompactRelaxed
Normal1.70