Running Gamocosm in Containers - Gamocosm/Gamocosm GitHub Wiki

Containers are like lightweight virtual machines and can provide even more fine grained isolation between processes/services. Docker is a popular tool for managing containers, but I prefer Podman instead. Podman is daemonless, meaning containers can be run as unprivileged users. Podman's command line interface is mostly compatible with Docker's, so Podman can be used as a drop-in replacement for most applications. As such, I recommend trying Podman if possible, but these commands will mostly work with s/podman/docker/ instead.

There are a lot of podman/docker commands. I recommend getting used to reading the man pages; simply type man podman- - hit tab twice to get autocomplete suggestions - fill in the command, and hit enter. You can hit u and d to go up and down respectively.

Running Gamocosm's Dependencies in Containers

We'll get started by putting PostgreSQL and Redis (for Gamocosm's Sidekiq workers) in containers, which should be straightforward. As of 2021 December 21, Gamocosm uses PostgreSQL 13.5 and Redis 6.2.6.

To start Redis, run:

podman run --detach --rm --publish 127.0.0.1:6380:6379 docker.io/redis:6.2.6

Again, I recommend reading the man pages to learn about the commands and their flags, but I'll explain the ones in the above command here:

  • --detach: Start the container in the background
  • --rm: Remove the container after it exits (whether by an error or we stop it)
  • --publish 127.0.0.1:6380:6379: Bind to 127.0.0.1 (localhost) on the host and listen on port 6380; forward to port 6379 in the container (note that Redis' default port is 6379. 6380 was chosen on the host port to show the difference).
  • docker.io/redis:6.2.6 is the container image. As mentioned, podman is mostly compatible with docker; podman can use images in the Docker repository.

You can read more about this image on Docker Hub.

To start a PostgreSQL container, run:

podman run --detach --rm --publish 127.0.0.1:5433:5432 --env "POSTGRES_USER=$DATABASE_USER" --env "POSTGRES_PASSWORD=$DATABASE_PASSWORD" docker.io/postgres:13.5

The additional --env flags set environment variables on the container. Note that the above command assumes that $DATABASE_USER and DATABASE_PASSWORD are defined in your current (host's) shell (e.g. you ran source load_env.sh). The double quotes allows $DATABASE_USER and $DATABASE_PASSWORD to be expanded before it is passed to the podman command/process. You can read more about this image on Docker Hub. In particular, note that setting the POSTGRES_PASSWORD is required, but POSTGRES_USER is optional (defaults to postgres).

Now, if you run podman ps, you should see the result of the previous two commands. If you don't see them both, try podman ps --all to include failed containers. Note that random names were generated for these containers; you can set a name by using the flag --name <name>.

At this point, you effectively have PostgreSQL and Redis accessible as if they were running normally on the host, albeit on the nondefault ports 5433 and 6380 respectively. In other words, if you followed/follow the normal setup instructions on Gamocosm's README, you nolonger need the steps pertaining to PostgreSQL and Redis (note that you still need (sudo) dnf install libpq-devel as that is for the Ruby PostgreSQL client library - for Gamocosm to connect to the PostgreSQL server). You would just change the following in gamocosm.env:

  • DATABASE_HOST=localhost: PostgreSQL is accessible via a TCP port now, instead of the default Unix domain socket.
  • DATABASE_PORT=5433: The nondefault port we exposed on the host.
  • SIDEKIQ_REDIS_PORT=6380: The nondefault ort we exposed on the host.

You would still install rbenv on your host machine (without root or sudo), use rbenv to install Ruby, install Bundler, and install Gamocosm's gem dependencies. You would still source load_env.sh and run rails ... and sidekiq ... in your shell (at this point). This should work, and you should make sure it works before trying more advanced container setups.

Furthermore, I actually recommend stopping at this point; i.e. just using containers to avoid running global PostgreSQL and Redis services. At my current understanding and experience, I do not think that it is a good idea to do development inside containers, for reasons that will be explained at the bottom. Containers are ok for tests, and Gamocosm does run continuous integration (CI) tests using containers, which is what we'll look at next.

Tests inside Containers

You can view Gamocosm's CircleCI config file to see exactly what it does. Mostly, it just runs scripts inside the podman/ folder - these scripts are only a few lines each; don't be afraid to read them. You can run the shell scripts inside podman/ yourself without modifying anything (e.g. podman/podman.env does not need to be changed). There are two differences between what we did above, and the CI setup:

  • The CI creates a container network.
  • The CI builds an image for Gamocosm and uses it to run tests.

Networking is fairly simple to understand:

  • podman network create <name> creates a network with a given name.
  • Passing --network <name> to podman run or podman create attaches the container to the network.
  • Containers attached to the same user defined network can communicate with each other using their names as hostnames (e.g. see podman.env and run-postgresql.sh).

Note that with this CI setup, the PostgreSQL and Redis services/ports are not exposed to the host; the services are only accessible within the virtual network.

An image is built from the Containerfile with [build-image.sh][15] Skipping a couple steps, Gamocosm's Gemfile and Gemfile.lock are copied into the new image to install the dependencies. The Containerfile then also copies the entire Gamocosm project directory into the container so that it is available when the image is run.

setup-db.sh only needs to be run once per PostgreSQL container. Since the CI creates new containers every time, it needs to run setup-db.sh every time. The last step is to actually run the tests with [run-tests.sh][14]. You can run the tests multiple times, but remember you have to rebuild the image to see any sources you changed! Fortunately, podman is smart enough to only repeat the image build from the line where it copies Gamocosm's source into the container.

Issues Developing Gamocosm in a Container

As we saw with the tests, we must rebuild the Gamocosm image whenever we change the source. We could mount the source into the container instead of building it into the image. However, there are at least a couple subtle issues:

  • Changing the dependencies (Gemfile or Gemfile.lock) still requires building a new image.
  • The tests don't require a Sidekiq runner, but running a development server (in general) does. Rails logs to the log/ directory in the Gamocosm project directory, so if we mounted it, it would need to be mounted as writable. However, we can't have multiple writable mounts; we wouldn't be able to mount to a Sidekiq container as well.

I've looked into changing Rails' log directory so that the source can be mounted as read-only, but there isn't an obviously clean/built-in way to change the log directory. I am still looking into making logging more flexible, but at this point I believe using containers for development - using the same container for changing sources - is not The Way:

  • it's awkward to have the same image for different (changing) application sources, and
  • it's inconvenient to rebuild an image every time you change the source.

Deploying Gamocosm in a Container

For deploying Gamocosm in production, we are ok with building an image for each deployment (assuming we aren't deploying as frequently as we change sources during development), so containers work fine. I am in the process of ironing out the details carefully.. but everything (or almost everything) you need to know should be covered above. For now, this section is left as an exercise to the reader...

⚠️ **GitHub.com Fallback** ⚠️