Docker CLI
To start showcasing the Docker CLI, I'm going to work through a few examples of setting up and working with this repo with some docker
commands.
Setup
If you plan to follow along, and you haven't already, take a moment to clone down this repo:
git clone https://github.com/noahjahn/docker-workshop.git && cd docker-workshop
Ensure you can run docker
from the terminal (you should see a printed out Docker version and build number):
docker --version
This project contains docs written in Markdown, which get parsed and built into static HTML files by Vitepress. Bun is currently used for the package manager as well as for any build and run-time tasks.
INFO
I chose Bun, hoping that most of us wouldn't already have it installed on our system. The goal being: we can see how to take advantage of Docker to still run this project without having to install any host or OS level dependencies. Outside of Docker, of course.
Install bun packages
With any JavaScript project, we'll need to install some packages defined in the package.json
docker run oven/bun i
INFO
oven/bun
is the official Bun Docker image
INFO
If you don't already have the oven/bun
Docker image pulled down then the current latest
tag will be pulled down, otherwise you'll run an existing downloaded image that matches the oven/bun
tag
WARNING
docker run
doesn't pull the latest image for you, so you may be running an older oven/bun
image version. You can make sure to download the latest image by running docker pull oven/bun
You should receive an error saying something about the package.json
file could not be found. This is because we are executing bun i
inside the container, which doesn't know anything about the current directory that we executed the command from. So, even though there is a package.json
file in the repo, the location that the bun i
command is actually executed does not contain the package.json
file.
List containers
The container we ran is now in a stopped state and can be seen by running:
docker ps --all
TIP
--all
can be shortened to just -a
. It means to show all containers, whereas by default the command will only show running containers
Remove containers
I like to remove my stopped containers after I'm finished with them, so I don't have a bunch of containers in this stopped state, just to keep things cleaned up. You'll want to copy the container ID from the previous command to paste in place of CONTAINER
below:
docker container rm CONTAINER
TIP
Whenever I use the docker run
command, I will add the --rm
flag, which will automatically remove the container and it's associated anonymous volumes when it exists
Volumes
The container needs to know about the package.json
that we have in the repo. One way to approach this is to create a bind-mount docker volume. But we need to know what the working directory is to for the bun image that we're using.
INFO
The bind-mount volume is a great feature to use in local development. The directory that is bind-mounted will stay up-to-date with any changes that are made in that directory in the container.
A quick way to figure out the working directory of the Docker image you're using would be to execute the pwd
command inside the container. Specifically with this container though, the entrypoint
is configured to be the bun
executable. So we'll need to make sure to override that in our docker run
command:
docker run --rm --entrypoint= oven/bun pwd
docker run --rm --entrypoint=pwd oven/bun
We should see the output to be /home/bun/app
.
INFO
You can also override the working directory of the image with the --workdir
, or -w
for short, flag
Now that we know where we should create the bind-mount volume we can do that with the --volume
or -v
flag:
docker run --rm -v ./:/home/bun/app oven/bun i
TIP
The left-hand side of the :
specifies the path on the host machine, your computer, to be mounted to the path on the right-hand side inside the container
User
DANGER
If you notice, after running ls -al
, the node_modules
directory and the bun.lockb
file are going to have the owner and group root
. Typically, containers should be run as a non-root user even in production. Locally, it's a great idea to run the container as the same user as your user ID on the host computer.
DANGER
It's also important to note that, in general, you shouldn't have to execute docker commands as root with sudo
.
You can specify the user you want the container to run as with the --user
flag. You can pass in either usernames (that exist within the container) or UIDs. If you want files to be created with your host users' ID, then you should pass in your UID. Check what your UID is by running the following:
echo $UID
INFO
In some cases, a user is going to already be created in the image with the 1000 user ID, so when the container is running, it may have a different name than your username on your host computer.
We should install packages again, so they're created with the correct user ID. First, we'll need to remove the files that were created as the root user:
sudo rm -rf ./node_modules
sudo chown $UID:$UID bun.lockb
Now, we can run the docker run command with the --user
flag
docker run --rm -v ./:/home/bun/app --user $UID oven/bun i
To confirm permissions are correct, run the ls -al
command again, and you should see the node_modules
directory is created as your user.
Publish
At this point, the packages to run vitepress
to build, serve, or preview these docs locally should all be downloaded and in place now. Looking at the package.json
file, there is a docs:dev
script, that will run the vitepress
development server for the docs.
We can run the docs:dev
script using bun run
with our docker run
command:
docker run --rm -v ./:/home/bun/app --user $UID oven/bun docs:dev
Once the docs build, the server will say that it's running on port 5173. But, if you try to go to http://localhost:5173 in your browser, you won't see anything running there. The reason being is we haven't mapped the required ports between the host computer and the container that is executing.
You can ctrl+c
or cmd+c
to send the interrupt to stop the container
Vitepress told us the port that is being used (5173), so we can map that to our host computer (as long as nothing else is on that port of course):
docker run --rm -v ./:/home/bun/app -p 5173:5173 --user $UID oven/bun docs:dev
TIP
Similarly to how the bind-mount volume works, the left-hand side of the :
specifies the port on the host machine to be mapped to the port on the right-hand side inside the container
Visiting http://localhost:5173 in your browser should show you these docs now!
Interactive and pseudo-TTY
The Vitepress dev server provides some interactive commands that can be used while the server is running, but the container doesn't know about any "attached" terminal or interface device without us explicitly telling it. So, of course, there is a few more flags
If you haven't already, you can send the interrupt signal with ctrl+c
or cmd+c
again in your terminal that's running the container
We can allocate a pseudo-TTY with the -t
flag:
docker run --rm -v ./:/home/bun/app -p 5173:5173 --user $UID -t oven/bun docs:dev
You should see some colors on the terminal now, as well as press h to show help
from vitepress
. The container still isn't accepting any input from you though, it's still just displaying its stdout.
Send the interrupt signal with ctrl+c
or cmd+c
again in your terminal that's running the container.
We'll also tell the container to be interactive with the -i
flag:
docker run --rm -v ./:/home/bun/app -p 5173:5173 --user $UID -it oven/bun docs:dev
TIP
The -it
flags often get used together.
Now typing h
with the Vitepress container terminal in focus, should actually allow you to interact with the server.
Exec
Sometimes, you won't need to run a separate docker container for a command. For example: if you have an existing container running, that is executing a script that is writing files to a temporary location in the container, and you want to list the files in that temporary directory, you can do so using docker exec
.
First though, you'll need to grab the container ID. In a new terminal, run:
docker ps
Copy the ID of the bun container that is running and paste it in place of CONTAINER
below:
docker exec CONTAINER ls -al /tmp
This will execute ls -al /tmp
inside the container and then close the shell once it's complete. It will not stop or exit the container.
run vs exec
docker run
will always start a new container with the command you specified whereas docker exec
expects a container to already be running. The command passed in to the docker exec
command will be executed in the already running container.