I’m often surprised by the number of people that I encounter with little to no experience using Docker - both in my professional career and community interactions. Considering I was once in that same position, and considering that I maintain a project that makes use of Docker, I thought it might be nice to share a guide.

This guide is intended as a quickstart to what Docker is, what problem it solves, and how to use it. I’ll cover these topics as we bootstrap and execute an etheos container.

Docker Background

Have you ever wanted to execute some software on your system, without that software being affected by the various system settings that are scattered around? Enter: Docker. Containerization solutions existed long before Docker was released; however, Docker’s release in 2013 caused the concept of containerization to explode in popularity.

The advantage is obvious - many applications run with some assumptions about system configuration and generally load configuration files from shared directories. These configuration files can have unknown side-effects on other software that references them. The software we want to run can also be impacted by system configuration that we’re completely unaware of.

Containers offer the additional benefit of idempotency. If we’re executing a container, we know there will be no side-effects between independent runs of said container.

How do containers work?

Containers can easily be treated as somewhat of a black box. They’re similar to a Virtual Machine, but generally execute as a process on the host system. They use virtual mounts to interface with the host file system and the container runtime to enable networking. All of these topics are more advanced concepts - for the purposes of this discussion, we will limit our understanding of containers to “an isolated process running on a computer somewhere”.

Containers and Images

Two domain concepts in the world of Docker are Containers (which we’ve already discussed) and Images. There is an important distinction - an Image is a pre-built set of instructions for the container runtime, while the container is a running instance of an image. When we want to run some piece of software as a container, we always start with an image definition. The image definition defines the software that will run, ports that are exposed, and commands executed within the container. The container instance is the execution of that predefined image.

Using docker

To get started, install Docker on your system. This can be done by following the instructions at docker.com for your platform. On Windows, install Docker Desktop instead. On Windows, I highly recommend using the WSL2 backend. This will enable you to run docker from either Windows via cmd/Powershell or in a WSL2 shell.

To verify that docker is installed and working, you may run docker run hello-world as a sanity check:

Running etheos

Most docker containers are intended to execute an application that has some sort of networking component. Generally, this is a web server or web server application. In this introduction, we’ll be using a docker image that I maintain for the etheos fork of eoserv.

For starters, let’s try executing the same docker run command that we just executed for the hello-world image, but this time, we’ll specify the published etheos image. Note that when a hostname is not specified in the image name, the container runtime assumes a default value of hub.docker.com.

docker run darthchungis/etheos

Well, that didn’t work!

Environment Variables

As expected, there are some defaults set by the etheos project that must be overridden, otherwise we’ll see a number of failures. The first thing that comes up is the database. For now, we’ll override the default of SQL Server with a SQLite database, but we’ll expand on this topic later.

Configuration overrides will largely depend on the application being run within a container. Every application and framework is a little different. In the case of etheos, we can either mount in configuration files that override the settings we want to change, or we can set environment variables to do the overrides one at a time. For other docker images, you will need to consult the documentation for which settings are required.

It is much simpler to override environment variables, so we’ll start with that. The syntax for overriding an environment variable is to pass the -e VARIABLE=VALUE argument for each variable we want to override. For etheos, we need to set the DBType value. Let’s try that with -e ETHEOS_DBTYPE=sqlite. Note that environment variable syntax for etheos requires the ETHEOS_ prefix for the setting keys.

docker run -e ETHEOS_DBTYPE=sqlite darthchungis/etheos

Excellent! Our override worked, we can address the next error.

Volumes

The whole point of a container is isolation, and that includes the file system. How are we supposed to get our data files into the container?

Docker provides a concept known as “volume mounts”, which allows you to create a mapping between the host file system and the container runtime, so you can make files and directories available within the container’s filesystem.

For the current error, we’ll need to get some data files and provide a volume mount telling the container filesystem where to find them. The way that we do this is with the -v C:\path\on\host:/path/in/container argument. Download the data files zip, extract it somewhere easy to find, and try adding that to our run command.

docker run -e ETHEOS_DBTYPE=sqlite -v C:\Users\ethan\sample-data\sample-data:/etheos/data darthchungis/etheos

Now we’re getting somewhere!

What we just did was map the host directory containing our sample data files (in the expected structure) onto the path /etheos/data, which is preconfigured in the etheos image as the source for data files. We could specify another container path for this, but then would also need to update the associated configuration settings to load the maps, pubs, etc. from the expected locations.

You may be wondering - what happens to the existing /etheos/data directory that may have already existed in the container? Simply put, the mount that we specify on the command line takes precedence. No merging takes place - if we mount an empty directory, then we’ll override any existing file structure with an empty directory tree. Fortunately, you can specify multiple mounts, so files/directories may be mounted on a more granular basis.

Unfortunately, if you scroll down all the way, you’ll see that the container still errors out, this time with a message about installing database tables.

That’s easily fixed with another configuration override: changing the InstallSql setting.

docker run -e ETHEOS_DBTYPE=sqlite -e ETHEOS_INSTALLSQL=./install.sql -v C:\Users\ethan\sample-data\sample-data:/etheos/data darthchungis/etheos

Finally, we see the message we’ve been waiting for, indicating that the server is up and listening for connections.

Networking

Herein lies the next problem - how do we connect to it? If you open up your client and try connecting to localhost:8078, you’ll see an error:

What gives? We have the container running, we should be able to connect to our server and poke around!

Unfortunately for us, we failed to specify an important property in our run command: the -p argument, which is used to specify ports published on the host machine. -p takes an argument in the form of hostPort:containerPort. In our case, it is fine to publish the same port that we’re running on in our container. Let’s try adding in our -p argument and see where that gets us.

docker run -e ETHEOS_DBTYPE=sqlite -e ETHEOS_INSTALLSQL=./install.sql -v C:\Users\ethan\sample-data\sample-data:/etheos/data -p 8078:8078 darthchungis/etheos

Much better. Now we have a fully isolated instance of etheos running on our local system that we can connect to!

Persistence

Now that we’ve gone ahead and created a working server, and possibly created an account/interacted with the world, let’s shut everything down. This may be done via the CTRL+C key combo.

Now that our container is shut down, let’s pretend that we want to start up our server again. Obviously, we’ll run the same docker run command again. I’ve captured the output while setting the ETHEOS_MAPS=1 config override to make the output less noisy.

An eagle-eyed reader may notice that our database has gone from one account to zero. This might be regarded as a generally Not Very Good Thing.

The reason for this is that each execution of docker run creates a new instance based on the image that we specify in the run command. Because we executed run again, we get a completely fresh state. Hooray for isolation! But it doesn’t help much in case we want to continue where we left off in our game world.

So, how do we get the container in which we created a character back again? We need to examine the list of containers and start the one that was previously stopped via CTRL+C. We can do this by popping open the docker desktop interface, and selecting “Containers”. I’ve been doing a lot of local development so we’ll kind of have to guess here.

Equivalent command:

docker ps -a

xenodochial_davinci seems like a good enough guess - let’s verify by selecting it, and viewing the logs in the Docker Desktop window.

Equivalent command:

docker logs xenodochial_davinci

This looks to be correct - we can see from the output that we have 1 account and 1 character, which matches what we did before! Let’s try starting that up again.

Equivalent command:

docker start xenodochial_davinci

And would you look at that, we’re back where we left off.

Persistence - the better way

Now, let’s pretend for a moment that we’re good stewards of our system, and regularly clean up after ourselves (lol). Or, in a more realistic example, let’s pretend that we’re trying to run our server in a resource-constrained environment (bigger lol, you can run etheos on a potato). Or even more realistically, we accidentally delete the container that has our database.

Shock and horror! We’ve lost all our wonderful progress and need to start from scratch. We could configure a database using the appropriate environment variable overrides, but that doesn’t address getting files out of the container (e.g. stdout/error logs or world dump files).

As previously mentioned, it is possible to mount multiple volumes into a container. When we mount volumes, they are read/write by default. That means that if we wanted to keep using SQLite, we can just mount a volume pointing at the database location, and it will be available to us in Windows. Let’s give it a try - I’ll even throw in the --name argument, which gives our container the specified name (rather than xenodochial_davinci), and the --rm argument, which automatically deletes our container when it stops!

docker run --name etheos_demo --rm `
    -e ETHEOS_DBTYPE=sqlite -e ETHEOS_INSTALLSQL=./install.sql -e ETHEOS_DBHOST=/etheos/database/database.sdb `
    -v C:\Users\ethan\sample-data\database:/etheos/database -v C:\Users\ethan\sample-data\sample-data:/etheos/data `
    -p 8078:8078 darthchungis/etheos

note: replace the backticks with backslash \ on Linux shells, or execute as a single-line command

If we take a look at our filesystem, we should see the database.sdb file in the specified path!

Create an account, CTRL+C the container, and verify the container has disappeared via docker ps -a. If we execute the same command again, we should see two things: 1) our install.sql no longer executes, and 2) our output shows that the account still exists. This is due to the fact that we’re mapping in the database file that was written by the container to our filesystem - since it hasn’t changed since our last execution, we can easily pick up where we left off!

Running in the background

So far, we’ve been executing a container and waiting for it to complete (which we force via CTRL+C). But what if we want to use the same terminal for other tasks? docker run provides an argument -d which allows us to run a container and immediately “detach” so it continues executing in the background. This is a good option when we don’t really care to watch output scroll by.

Since we now have to use docker to send signals to our container, we need docker ps to give us the container name (unless you ran it with the --name argument) and docker stop {name} to stop it. docker rm -v {name} will delete a stopped container, and also remove any volumes associated with it. Note that this does not modify the host filesystem - it only removes the virtual mounts that are left dangling once the container no longer exists.

Speaking of output - you may have noticed running these example commands that the output is incomplete or lags behind the actions taken after connecting to the server. This is due to docker’s default output streaming behavior - it is not a TTY like most shells. To force a TTY shell, pass the -t argument to the run command. I usually use this in conjunction with the -i argument to keep stdin open; however, this will cause some weird behavior when running detached. Generally, you want either -d or -it in your docker run command.

Conclusion

Full example command:

docker run --name etheos_demo --rm -d`
    -e ETHEOS_DBTYPE=sqlite -e ETHEOS_INSTALLSQL=./install.sql -e ETHEOS_DBHOST=/etheos/database/database.sdb `
    -v C:\Users\ethan\sample-data\database:/etheos/database -v C:\Users\ethan\sample-data\sample-data:/etheos/data `
    -p 8078:8078 darthchungis/etheos

Hopefully at this point you have enough to get started with Docker for local development. It really is a fantastic technology and vastly speeds up the process of developing locally once you understand the basics. I mostly use it for running a local database instance without needing a dedicated VM.

Next, I hope to cover more topics such as building and pushing images, and advanced topics such as docker compose and kubernetes, both of which build on container runtimes but provide vastly more functionality. Hopefully, it won’t take another year and a half to find the motivation to finish writing something ;)

Until next time - happy dockering!

Special thanks to Richard Leek for proofreading this post