Blog:

Containerized Rails App Development

Avatar
Allan Andall
January 18, 2023

Containerized Rails App Development

In this blog post, we will be using Docker Compose on a Rails app. You will first need to install Docker and Docker Compose on your machine.

Docker and Docker Compose

Docker is a tool that allows you to package and deploy applications in a standardized way, using containers. Docker Compose is a tool that will enable you to define and run multi-container Docker applications, using a single configuration file. Basically docker composer group docker containers into a manageable stack of services. It can be especially useful for development, as it allows you to quickly spin up a consistent environment for your app, without having to manually install and configure these dependencies on your local machine.

Dockerize Rails App

Create a file named Dockerfile.dev in the root of your application. This file will be used to build a Docker image of your application. Add the following contents to the Dockerfile.dev:


FROM ruby:2.7.1

# Install dependencies
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client

# Set an environment variable to store the application's directory
ENV APP_HOME /myapp

# Create the application's directory and set it as the working directory
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# Copy the Gemfile and Gemfile.lock into the working directory
COPY Gemfile Gemfile.lock $APP_HOME/

# Install the gems
RUN bundle install

# Copy the application code into the working directory
COPY . $APP_HOME

# Expose the default Rails server port
EXPOSE 3000

# Run the Rails server when the container starts
CMD ["rails", "server", "-b", "0.0.0.0"]

This Dockerfile does the following:

  1. It specifies that we want to use the ruby:2.7.1 image as the base for our Docker image. This image comes with the Ruby runtime and other necessary dependencies pre-installed.

  2. It sets the working directory to /app, which is where we will be storing our Rails code.

  3. It copies the Gemfile and Gemfile.lock files from the host machine into the Docker container.

  4. It installs the dependencies specified in the Gemfile by running bundle install.

  5. It copies the rest of the code from the host machine into the Docker container.

  6. It exposes the default Rails port, which is 3000.

  7. It sets the default command to start the Rails server, which is bundle exec rails server -b 0.0.0.0.

A Dockerfile is a text file that contains instructions for building a Docker image. It is used to automate the process of building an image, allowing you to specify the base image, dependencies, and other details needed to create a custom image for your application. It is not necessarily required to have a different Dockerfile for each environment, but it is common to customize the Dockerfile depending on the environment in which the image will be used. For example, you might use a different base image depending on the environment, or install different dependencies or configure different environment variables.

Use Docker Compose to Manage Docker Containers

In the context of developing Rails app, Docker Compose can be used to set up and run all of the necessary dependencies for your application in a single command

Next create a docker-compose.yml file in the root of your project, which will contain the configuration for your application’s containers.

Here, we will have a sample rails app myapp which have a database (Postgres), a message queue (RabbitMQ), and a cache (Redis) as dependencies.


version: '3'
services:
  db:
    image: postgres
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: mypass
  redis:
    image: redis
  rabbitmq:
    image: rabbitmq
  web:
    build:
      context: ./
      dockerfile: Dockerfile.dev
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
      - rabbitmq

In this configuration, we have defined five services (containers):

  • db: a PostgreSQL database
  • redis: a Redis server
  • rabbitmq: a RabbitMQ message broker
  • web: our Rails app

The db service is running a Postgres container, and we have specified some environment variables to set the username and password for the Postgres user. The redis and rabbitmq services are running the official Redis and RabbitMQ images, respectively. Finally, the web service is building a container from the current directory (i.e. the root of the Rails app), and it is depending on the db, redis, and rabbitmq services to be up and running before it starts.

Data Persistence

By default, the data stored in a Docker container is not persisted when the container is stopped or removed. This means that if you stop or remove the db, redis, or rabbitmq containers, any data stored in them will be lost.

To persist the data, you can use Docker volumes to store the data outside of the containers. This will allow the data to survive even if the containers are stopped or removed.

To add a volume to a service in Docker Compose, you can use the volumes key in the service’s configuration. For example, to add a volume for the db service, you could use the following configuration:


services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

This will create a volume named pgdata and mount it at /var/lib/postgresql/data inside the db container. Any data stored in this location will be persisted to the volume, even if the db container is stopped or removed.

You can use a similar approach to add volumes for the redis and rabbitmq services. Just make sure to mount the volumes at the appropriate locations for each service.


services:
  redis:
    volumes:
      - redisdata:/data
  rabbitmq:
    volumes:
      - rabbitmqdata:/var/lib/rabbitmq
volumes:
  redisdata:
  rabbitmqdata:

With these volumes in place, the data stored in your db, redis, and rabbitmq containers will be persisted even if the containers are stopped or removed.

Network Isolation

In Docker, you can use network isolation to create separate networks for your containers. This can be useful if you want to create a separate network for your Rails app and its dependencies.

To create a separate network for your containers, you can use the networks key in your docker-compose.yml file. For example, to create a network named myapp_network and add all of your services to it, you could use the following configuration:


services:
  db:
    networks:
      - myapp_network
  redis:
    networks:
      - myapp_network
  rabbitmq:
    networks:
      - myapp_network
  web:
    networks:
      - myapp_network
networks:
  myapp_network:

Your updated docker-compose.yml may look like below


version: '3'
services:
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: mypass
    ports:
      - 5432:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - myapp_network

  redis:
    image: redis
    volumes:
      - redisdata:/data

  rabbitmq:
    image: rabbitmq:3.8.9-management
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - myapp_network

  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - myapp_data:/myapp/data
    ports:
      - "3000:3000"
    networks:
      - myapp_network
    depends_on:
      - db
      - redis
      - rabbitmq

volumes:
  db_data:
  myapp_data:
  rabbitmq_data:
  redisdata:
 
networks:
  myapp_network:

Once you have created your docker-compose.yml file, you can use the following command to start all of the containers for your application:

sh
docker-compose up

This will build and start all of the containers defined in your docker-compose.yml file. You should now be able to access your Rails app at http://localhost:3000.

Here are some basic Docker Compose commands that you may find useful:

  • docker-compose up: This command starts the services defined in the docker-compose.yml file. By default, it creates and starts all the services in the background (detached mode). You can use the -d flag to start the services in detached mode, or the -f flag to specify a different Compose file.

  • docker-compose down: This command stops and removes all the containers, networks, and volumes created by docker-compose up. You can use the -v flag to remove the volumes as well.

  • docker-compose build: This command builds the images for the services defined in the docker-compose.yml file. You can use the --no-cache flag to build the images from scratch, rather than using the cache.

  • docker-compose run: This command allows you to run a command in a running container. For example, docker-compose run web rake db:create will open a Bash shell in the web container and run rake db:create.

  • docker-compose logs: This command displays the logs for a service. You can use the -f flag to follow the logs, and the --tail flag to show the last N lines of the logs.

These are just a few of the basic Docker Compose commands that you may find useful. For a complete list of commands and options, you can refer to the Docker Compose documentation.

Optional UI for Managing Containers

Ideally, you should use docker-compose.yml to configure each containers however there several tools that provide a graphical user interface (UI) for managing Docker containers and Docker Compose projects.

One such tool is Portainer, which is an open-source management UI for Docker. With Portainer, you can easily view and manage your containers, images, and Docker Compose projects from a web-based UI.

To use Portainer, you will need to install it as a Docker container. You can do this by running the following command:


 docker run -d -p 9000:9000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer

This will start the Portainer container and expose it on port 9000. You can then access the Portainer UI by visiting http://localhost:9000 in your web browser.

Once you have Portainer set up, you can use it to start, stop, and manage your Docker containers and Docker Compose projects. You can also use it to view logs, inspect containers, and perform other tasks.

Other tools that provide a UI for managing Docker containers and Docker Compose projects include Docker Desktop, Kitematic, and Shipyard. You can choose the tool that best fits your needs and workflow.

PROS/CONS of Dockerizing a Rails app:

PROS

  1. Consistency across environments: By using Docker to containerize your application, you can ensure that your development, staging, and production environments are all running the same version of the application and its dependencies. This can help prevent issues caused by differences between environments and make it easier to debug problems when they do arise.

  2. Ease of deployment: With Docker, you can package your entire application and its dependencies into a single container image. This makes it easy to deploy your application to any environment that supports Docker, without worrying about dependencies or configuration issues.

  3. Isolation of dependencies: By running your application and its dependencies in separate containers, you can ensure that they are isolated from each other and from the host system. This can help prevent conflicts between dependencies and make it easier to upgrade or change individual components.

  4. Scalability: With Docker, it is easy to scale your application by running additional copies of your application container. This can be useful if you need to handle a large amount of traffic or if you want to run multiple copies of your application for high availability.

  5. Ease of maintenance: By using Docker, you can easily update or upgrade your application and its dependencies by creating new container images. This makes it easy to apply security patches and other updates without affecting the underlying host system.

CONS

  1. Additional complexity: Dockerizing an application can add an additional layer of complexity to your development and deployment process. You may need to learn new tools and concepts, and you may need to spend more time configuring and maintaining your Docker containers and infrastructure.

  2. Increased resource usage: Running an application in a Docker container can require more resources than running it directly on the host system. This is because each container has its own operating system and requires its own resources, such as memory and CPU. If you have a resource-constrained environment, this may be a consideration.

  3. Performance overhead: Running an application in a Docker container can introduce some performance overhead, as the application’s requests and responses must pass through the container’s virtualized environment. In most cases, this overhead is minimal and should not be noticeable, but it is worth considering if you have very high performance requirements.

  4. Limitations on host system access: When running an application in a Docker container, it may not have direct access to certain resources or features on the host system. For example, you may need to use Docker volumes or other workarounds to access host system files or devices.

While dockerizing a Rails app can bring many benefits, it is important to weigh the potential disadvantages against your specific needs and goals. In some cases, the benefits of Docker may outweigh the additional complexity, while in other cases it may not be the best fit.

Here are some of the cases where benefits may outweigh its complexity:

  1. Developing and deploying applications in different environments: As a developer, I think this is th biggest benefits on dockerizing app. If you need to develop and deploy your application in different environments, such as local development, staging, and production, using Docker can help ensure that your application is consistent across all environments. This can make it easier to debug issues and reduce the risk of problems caused by differences between environments.

  2. Managing dependencies: If your application has complex dependencies or requires specific versions of libraries or frameworks, using Docker can make it easier to manage these dependencies. By containerizing your application, you can ensure that it always has the correct dependencies and that they are isolated from the host system.

  3. Scaling applications: If you need to scale your application to handle a large amount of traffic or to ensure high availability, using Docker can make it easier to run multiple copies of your application and manage them as a group.

  4. Maintaining applications: If you need to frequently update or upgrade your application or its dependencies, using Docker can make it easier to create new container images and roll out updates without affecting the underlying host system.

Overall, if your application has complex dependencies, needs to run in different environments, or needs to be scalable or maintainable, the benefits of Docker may outweigh the additional complexity. It is important to carefully consider your specific needs and goals when deciding whether to dockerize your application.

You can checkout sample rails app created on this repo.