I wrote this for a virtual workshop I hosted for Hack Club. The workshop was an introduction to Docker and Docker Compose.
What’s Docker?
Docker is a tool for containerizing software. In some cases, it can replace a VM. Each container has its own file system. However, core system components are shared. The container uses the host’s kernel.
Speed ⚡
Due to the shared kernel, containers can run extremely fast. Imagine setting up an Ubuntu Server VM. Most would not consider the startup time insignificant. Now, try running a Docker Container.
Prerequisites
You need to have Docker installed. You can find the installation instructions here
.
If you’re on macOS or Windows, I recommend using Docker Desktop as that should easily integrate with WSL on Windows and on macOS, it should create a VM. On Linux, you probably should just run the installation script
. Run this with curl -fsSL https://get.docker.com | sh
. If you want to first see what the script does without making changes to your system, you can run curl -fsSL https://get.docker.com/ | sh -s -- --dry-run
.
A quick note, if you’re on Linux, you will probably have to use sudo
.
Running your first container
docker run -it --rm ubuntu:latest bash
Explanation:
docker run
: Run a container-it
: Allows interactive usage with the TTY- Combination of
-i
and-t
- Combination of
--rm
: Delete container when it stopsbash
: Executebash
in the container in order to run commands
Usage:
You should be in bash
now. You’re root
, so no need to prepend commands with sudo
.
Try running apt install neofetch -y
. This probably won’t work. Run apt update
and try installing neofetch
again. Running neofetch
should work now.
.-/+oossssoo+/-. root@4015f9430120
`:+ssssssssssssssssss+:` -----------------
-+ssssssssssssssssssyyssss+- OS: Ubuntu 24.04 LTS on Windows 10 x86_64
.ossssssssssssssssssdMMMNysssso. Kernel: 5.10.102.1-microsoft-standard-WSL2
/ssssssssssshdmmNNmmyNMMMMhssssss/ Uptime: 3 hours, 23 mins
+ssssssssshmydMMMMMMMNddddyssssssss+ Packages: 257 (dpkg)
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Shell: bash 5.2.21
.ssssssssdMMMNhsssssssssshNMMMdssssssss. CPU: 11th Gen Intel i7-11800H (16) @ 2.304GHz
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ GPU: 679a:00:00.0 Microsoft Corporation Basic Render Driver
ossyNMMMNyMMhsssssssssssssshmmmhssssssso Memory: 824MiB / 7811MiB
ossyNMMMNyMMhsssssssssssssshmmmhssssssso
+sssshhhyNMMNyssssssssssssyNMMMysssssss+
.ssssssssdMMMNhsssssssssshNMMMdssssssss.
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
+sssssssssdmydMMMMMMMMddddyssssssss+
/ssssssssssshdmNNNNmyNMMMMhssssss/
.ossssssssssssssssssdMMMNysssso.
-+sssssssssssssssssyyyssss+-
`:+ssssssssssssssssss+:`
.-/+oossssoo+/-.
Running Caddy (a web server) in Docker
By default, when you delete a container, all data is lost. To persist data, you can use volumes! Technically, what we’re doing here is a bind-mount
, not really a volume
. In some cases, you might want to use a volume. In addition, the --mount
flag is preferred over -v
as it’s more explicit. However, for simplicity, we’ll use -v
.
Creating an index.html file
# Create a directory for the files
mkdir caddy-website
cd caddy-website
# Create an index.html file
echo "hello world" > caddy-website/index.html
Running Caddy
docker run -d --name caddy -p 2048:80 -v $(pwd)/caddy-website:/usr/share/caddy caddy:latest
You now should be able to access the website at http://localhost:2048
!
Explanation:
-d
: Run the container in the background--name caddy
: Name the containercaddy
-p 2048:80
: Map port 2048 on the host to port 80 in the container-v $(pwd)/caddy-website:/usr/share/caddy
: Bind-mount thecaddy-website
directory to/usr/share/caddy
in the container- You could skip creating the directory and just bind-mount the file with
-v $PWD/index.html:/usr/share/caddy/index.html
- You could skip creating the directory and just bind-mount the file with
caddy:latest
: Use thecaddy:latest
image
Keep in mind:
We’re running in the background with -d
. However, this doesn’t mean the container will restart on boot, failure, or when Docker restarts. To have the container restart, add --restart unless-stopped
. There are other restart policies
as well, but unless-stopped
will probably be the one you use most.
Also, since we’re running in the background, you won’t see any output. To see the logs, run docker logs caddy
. Running docker logs -f caddy
will follow the logs, allowing you to see new logs as they’re generated.
Controlling the container
Since we’re running the container in the background, you might want to stop it. To stop the container, run docker stop caddy
. To start it again, run docker start caddy
. To remove the container, run docker rm caddy
.
Images
Now’s probably a good time to talk about images. A Docker image is made up of layers. Each layer is a change to the file system. In past examples, we’ve been using images made by others. However, you can create your own images. This is done with a Dockerfile
.
Creating a Dockerfile
Create a file called Dockerfile
with the following contents:
FROM alpine:latest
ENTRYPOINT ["echo"]
Alternatively, you can run the following commands to do the same thing:
echo "FROM alpine:latest" >> Dockerfile
echo 'ENTRYPOINT ["echo"]' >> Dockerfile
Explanation:
FROM alpine:latest
: Use thealpine:latest
image as the base image- Alpine is a lightweight Linux distribution often used in containers
ENTRYPOINT ["echo"]
: Set the entrypoint toecho
- This means that when the container starts, it will run
echo
with any arguments passed to the container
- This means that when the container starts, it will run
Building the image
Run docker build -t echo .
in the same directory as the Dockerfile
. This will build the image with the tag echo
.
- To build the image with a different tag, run
docker build -t <tag> .
- In some cases, you would want the registry to be included in the tag. For example,
docker build -t ghcr.io/username/echo .
for the GitHub Container Registry - If, for whatever reason, the Dockerfile is not, in fact, called
Dockerfile
, you can specify the file withdocker build -t echo -f <filename> .
- In some cases, you would want the registry to be included in the tag. For example,
Running the image
Run docker run --rm echo Hello, Docker!
.
--rm
: Remove the container when it stopsecho
: The image to runHello, Docker!
: The argument to pass toecho
Docker Compose
Using docker run
can get tedious. For example, if you have multiple containers that need to be run together, you would have to run multiple docker run
commands. When you want to stop them, get logs, etc., you would have to do the same. This is where Docker Compose comes in.
Note that docker-compose
has effectively been superseded by docker compose
.
Running a desktop environment in Docker
LinuxServer
is a group you may come across when self-hosting. They provide Docker images for a variety of services. In this example, we’ll be using their Webtop
image. In some cases, you can clone the repository and run docker-compose up
. However, it can be easier to just download the docker-compose.yml
in its own directory. Docker will use the directory name to name containers, which is why I try to keep Compose files in a directory dedicated to the service or the cloned repository.
Webtop is a project that provides a web interface for a desktop environment. It uses KasmVNC which allows for remote desktop access through the browser. The latency has been quite good in my experience and audio works as well. Some other nice features include file-transfer, clipboard sharing, and multiple displays. If you just want a specific application, LinuxServer also has KasmVNC images for apps like Firefox
.
Create and navigate to a directory for the Webtop service
mkdir webtop
cd webtop
Create the docker-compose.yml
file
The linxuserver/webtop
image doesn’t have a docker-compose.yml
file. It does, however, have example contents of what the file should look like in the README. I’ve edited the example slightly. Refer to the repository for the more, possibly updated, information.
services:
webtop:
# Use the Ubuntu Webtop image with the MATE desktop environment
image: lscr.io/linuxserver/webtop:ubuntu-mate
# The name of the container
container_name: webtop
environment:
# PUID and PGID are the user and group IDs of the user running the container
# These help with file permissions
# You can find these by running `id` in the terminal or by running `id -u` and `id -g`
- PUID=1000
- PGID=1000
# TZ is the timezone
- TZ=Etc/UTC
# TITLE (optional) is the title of the web interface
- TITLE=Webtop
# Set HTTP authentication (optional)
- CUSTOM_USER=user
- CUSTOM_PASS=password
volumes:
# Mount /config (set as the home directory) to the `home` in the current directory
- ./home:/config
# Optionally, allow for Docker in Docker by sharing the Docker socket
# The Webtop image has the Docker binary. If you're running an image that doesn't, install Docker in the container first.
- /var/run/docker.sock:/var/run/docker.sock
ports:
# Port 3000 is the web interface (kasmVNC)
# Port 3001 is the web interface (kasmVNC) with HTTPS
- 3000:3000
- 3001:3001
# devices:
# This optional mount should help with hardware acceleration (host should be Linux)
# - /dev/dri:/dev/dri
# The README mentions that this helps prevent web browsers from crashing
shm_size: "1gb"
# Restart the container unless stopped manually
restart: unless-stopped
Running the Docker Compose file
Run docker compose up -d
in the same directory as the docker-compose.yml
file. To run it in the foreground, remove the -d
. To remove the container, run docker compose down
.
Some cool projects to check out
- Portainer : A web interface for managing Docker containers
- Docker-OSX
: Run macOS in Docker
- Runs a container that runs QEMU in order to virtualize macOS
- It can generate serial numbers and board IDs meaning iMessage works!
- Can be accessed through X11 forwarding or VNC
- dockur/windows
: Run Windows in Docker
- Uses QEMU to virtualize Windows
- Can be accessed through a web browser or via RDP
- If you’re on an ARM64 machine, like the Raspberry Pi, dockur/windows-arm is available
- dock-droid
: Run Android in Docker
- Like the above two, uses QEMU
- Can be accessed through X11 forwarding or VNC