I will describe how to use docker to help you run shells in arbitrary linux distributions, preserving your changes.
I will try to keep this short, there is a lot of information about Docker on the internet of course.
Docker is a container system, it runs a linux system as a filesystem (image) using your existing linux operating system. On other OSes, it shares a lightweight linux virtual machine between your different linux containers.
The first step is to install docker. Use your distribution package. On Windows or Mac, use the installer from the docker hub.
Create an account with docker here:
This is where you search for images.
On linux, you will want to add yourself to the docker
group and
reboot/relogin, and enable the docker service:
sudo usermod -a -G docker $USER
sudo systemctl enable --now docker.service
Once you are logged back in, use the:
docker login
command to log into the hub. You may need the docker-credential-helpers
package to store your password.
On Windows and Mac you will want to use the GUI to configure things like RAM and CPU count for your virtual machine and log into the hub. Be generous with your allotment, but keep in mind that the docker VM generally needs fewer resources than a traditional VM.
Now that you have taken care of the preliminaries, I wil describe how to run
shells in arbitrary linux distributions. For the purposes of this guide, I am
using my username rkitover
, which you will substitute for your own.
The example distribution I will use is Ubuntu 19.04 "Disco".
The name of this image on the hub is ubuntu:disco
, when you search for images
on the docker hub you will see the name you need to use.
You can use docker pull
to load an image into your store, or run an image name
directly and it will be automatically downloaded, you will use the latter
option.
We need to create the environment to use from our shell launcher script.
Open a root shell in the image like so:
docker run --name disco -h disco --detach-keys="ctrl-@" -it ubuntu:disco bash -l
docker will download the image and open a root shell.
What this command does:
-
the container name is set to
disco
-
the hostname of the container in the docker virtual network is set to
disco
-
the hotkey to detach from the shell is set to
CTRL-2
-
the
-i
option means you want an interactive session -
the
-t
option allocates a tty -
ubuntu:disco
is the image name on the docker hub, local image names will be checked first -
the rest is the command to run, in this case a
bash
login shell
Now you are going to do some setup to use the image as a development/testing environemnt. You would do something similar for other distributions, depending on your needs and the needed commands.
useradd rkitover
apt update
apt -y upgrade
apt -y install vim tmux tree git build-essential cmake silversearcher-ag sudo locales
cd /etc/sudoers.d
echo 'rkitover ALL = NOPASSWD: ALL' > rkitover
cd
locale-gen en_US.UTF-8
locale-gen ru_RU.UTF-8
That should be good, add your user, install some packages (most importantly sudo and locales), add yourself to sudo, generate locales.
Make sure your UID
is the same as on the host, generally 1000
.
Notice that you did not set up a home directory, that's because you are going to use the docker volumes feature to mount your existing host home directory inside the image. And this is why your UID needs to match.
Now you will need to save your set up image.
Docker is primarily used by building images from scripts called Dockerfiles, to run canned services, in this case you are doing something entirely different.
Run:
docker ps -a
You will see something like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52ef73929c74 ubuntu:disco "bash -l" 13 minutes ago Exited (0) 2 seconds ago disco
this shows that you had a container named disco
running using the image
ubuntu:disco
and that it has exited.
You are going to save the modified image in your namespace and remove the container.
docker commit disco rkitover/ubuntu:disco
docker rm disco
Now running docker ps -a
again will show an empty table.
You need to make some changes to your host $HOME
configuration for shells in
docker to work properly. These changes are harmless.
Edit your ~/.bash_profile
, at the top put:
export SHELL=/bin/bash
export LANG=en_US.UTF-8
or whatever locale you want and set up in the previous step.
Make sure it has something like this in the middle:
. ~/.bashrc
at the bottom put:
cd ~
You may need to add logic to your ~/.bashrc
to detect if you are running in
some container and adjust your aliases and such accordingly. For example I have
this:
if ! grep -q docker /proc/1/cgroup; then
alias tmux='systemd-run --quiet --scope --user tmux'
fi
Put the following script in your ~/bin
or wherever you keep such things:
mkdir -p ~/bin
cd ~/bin
curl -LO 'https://gist.githubusercontent.com/rkitover/fdf8bc9ca55248752507336d580f7dbb/raw/9b793aa1d9a51548c147005154845b792d340124/docker-shell'
chmod +x docker-shell
#!/bin/sh
image=$1
shift
name=${image##*:}
container_exists() {
[ -n "$(docker ps -q -a -f "name=$name" "$@" 2>/dev/null)" ]
}
set -- \
-e DISPLAY="$DISPLAY" \
-e XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \
"$@"
# if running, exec another shell in the container
if container_exists; then
exec docker exec \
--detach-keys="ctrl-@" \
"$@" \
-it -u $USER "$name" bash -l
fi
# otherwise launch a new container
docker run --name "$name" -h "$name" \
"$@" \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v /run/user/$(id -u):/run/user/$(id -u) \
--device=/dev/dri \
-v "$HOME":"$HOME" \
--detach-keys="ctrl-@" \
-it -u $USER $USER/"$image" bash -l
# after the main shell exits, commit image and remove container
if container_exists -f "status=exited"; then
docker commit "$name" $USER/"$image"
docker rm "$name"
fi
Some notes:
-
the
-e
option sets an environment variable in the shell session -
the
-v
option binds a directory on your host to the container, like a bind mount -
the
--device
option passes through a host device, this particular setup will allow running X apps with GPU acceleration on many setups, more on that later
Now try it out, run:
docker-shell ubuntu:disco
you should get a bash shell in your new Ubuntu environment if everything went well, with a working locale and sudo.
Running the command again in another terminal while this shell is active will run another shell in the same container.
Once the first shell exits, the image will be committed and the container will be removed.
You can pass any other arguments to the script, they will be passed to docker run
for the first shell and to docker exec
for subsequent shells.
If you detach with CTRL-2
, then you can attach again with:
docker attach disco
If you use this feature, you will have to commit your image and clean up the container yourself as described previously.
You can commit images to any tag or prefix with this script, and they will be stored under your user namespace. E.g.:
docker-shell development/ubuntu:latest
in this case during initial setup you would commit the image to
$USER/development/ubuntu:latest
.
You may want to install a cron job to clean up dangling images and dead containers, something like this:
#!/bin/sh
docker inspect -f '{{if not .State.Running}}{{.Id}} {{ end }}' $(docker ps -aq) | grep -Ev '^$' | \
xargs docker rm >/dev/null 2>&1
docker image prune -f >/dev/null 2>&1
To use GPU acceleration for X11 apps, install the mesa-utils
package or
equivalent, and more on that here:
http://wiki.ros.org/docker/Tutorials/Hardware%20Acceleration
Now when a user complains "your software doesn't work on my distribution X" you have a powerful recourse to investigate the problem.
There are endless possibilities with this software.