Talks

Containerizing Rails: Techniques, Pitfalls, & Best Practices

Containerizing Rails: Techniques, Pitfalls, & Best Practices

by Daniel Azuma

This video presentation by Daniel Azuma at RailsConf 2018 discusses containerizing Ruby on Rails applications with an emphasis on best practices, techniques, and common pitfalls. The session covers the fundamentals of Docker and Kubernetes, catering to varied experience levels, although it is not a beginner tutorial. Key points include:

  • Understanding Containers: Containers provide isolation by sharing the underlying operating system kernel while isolating filesystem, memory, and process space.
  • Docker Images: An image serves as a template for creating containers, primarily consisting of filesystem snapshots, and is built using a Dockerfile. Understanding and optimizing the base image used is crucial for effective application deployment.
  • Best Practices for Image Creation:
    • Minimizing Size: Important for resource efficiency during build and runtime. Use techniques like combining commands in a single RUN instruction to avoid unnecessary buildup of data layers.
    • Multistage Builds: This allows building an application in one stage and then creating a smaller image with only the required runtime files.
    • Application Settings: Configure locale settings to prevent encoding issues and avoid running applications as root for improved security.
    • Use Exec Form: It is advisable to use the exec form of commands in Dockerfiles when launching processes to ensure proper handling of signals.
  • Production Considerations: Containers help ensure predictability and resource management by setting fixed constraints for CPU and memory. Logs should be directed outside of containers for accessibility and monitoring.

In conclusion, effectively containerizing a Rails application hinges on a combination of understanding Docker best practices, optimizing images, and adhering to security protocols. This session was sponsored by Google Cloud Platform.

00:00:11.269 Wow, well thank you all for coming. I know it's the last session of the afternoon, and usually, I want to be doing something other than sitting in a session at this time. So I’m really grateful that you've all come. This session is about containerizing Rails. My name is Daniel Azuma, and we’ll get started now.
00:00:30.150 During this hour, we're going to be talking about container best practices. We’ll go through how to wrap your Rails application in Docker containers. We’ll discuss how to optimize your containers for size and performance, and what should and shouldn’t go into your containers. Essentially, we'll cover how to make your Rails application work well in a container-based production system.
00:01:05.519 We’ll start off with a bit of background on Docker. I won’t assume how much experience you have with Docker or Kubernetes or related technologies, so you should be able to follow this even if you don’t have a lot of experience. However, I do want to mention that this isn't meant to be a first-time tutorial. I apologize for that; the program indicates that there would be a walkthrough for deploying an application to Kubernetes, but I had to cut that part for time.
00:01:22.500 There are some good tutorials on Kubernetes that you can find online, but what we’re focusing on today are best practices and tips for creating better containers. So, let's start off with just a little bit about me, your speaker.
00:02:08.340 My name is Daniel Azuma; I've been developing professionally for about 20 years, with about 12 of those years spent with Ruby and Ruby on Rails. I remember attending my first RailsConf in 2007. Does anyone here remember RailsConf 2007 or earlier? A few? That’s great! I was hoping many of you were newer than that, as it shows that our community continues to grow and evolve.
00:02:37.450 So, welcome! Over the past twelve years, I have worked on various projects in Ruby, including geospatial work and startups. Currently, I am part of a small boutique startup at Google, where I am on a team focused on programming language infrastructure, which we refer to as LOL, or Languages, Optimizations, and Libraries.
00:03:01.150 As you can imagine, it's a team dedicated to improving language infrastructure both internally at Google and externally for customers working with Google Cloud. I specifically work on Ruby and Elixir, and I’m excited to share insights on containers and what makes them work. I want to thank Google Cloud Platform for enabling me to be here today.
00:03:45.630 Let’s get started with some basic concepts to ensure we’re all on the same page. First, what is a container? When you think about containers, one word that should come to mind is isolation. Containers are primarily about isolation. For example, a Docker container is essentially an isolated filesystem, a slice of the CPU, a slice of memory, a network stack, user space, and process space. This isolation ensures that it is very controlled how the processes inside the container interact with those outside.
00:04:37.600 At first glance, containers might seem similar to virtual machines, but they are not; there’s no hypervisor involved here. Containers are actually a feature of Linux. They share the Linux kernel and CPU architecture with everything else that is running in Linux, including other processes and other containers. Therefore, you can run multiple containers on a Linux machine, and they remain isolated from each other.
00:05:36.159 Next, let’s cover the concept of images. What is an image? An image is basically a template for creating a container and consists of a filesystem snapshot. It includes the files and directories needed to start your container, such as your application files, operating system files, and the dependencies of your application. All of this resides in the image.
00:06:13.360 In Docker, you create an image using a recipe called a Dockerfile, which is just a simple text file that contains instructions for building your image. Here’s a simplified example of what a basic Dockerfile for a Rails app might look like.
00:07:22.430 The first line is known as the base image, which is the starting point for your Dockerfile. This is another Docker image with all the files necessary to create a container. There are different kinds of base images; one common example is the official Ruby image, which includes an operating system and an installation of Ruby.
00:08:14.910 From the base image, you can have various commands within your Dockerfile. These commands can do things like copy files into the image, run shell commands within the Docker container, or set certain properties of the image. For example, you can copy your Rails application into the image and run a command like "bundle install" to install your gem dependencies. Different commands run in order when Docker builds an image from your Dockerfile.
00:09:46.810 As you go through getting started tutorials for Docker, you'll often be directed to use specific base images. For instance, the official Ruby image has various variants targeting different operating systems and versions. It’s important to understand what goes into a base image and not treat it like a black box.
00:10:54.140 Reading the Dockerfile for your base image is a good practice as many of them are available on GitHub. This will help you understand what operating system is being installed, how the environment is set up, and whether it matches how your application wants to be structured. Knowing the various good Docker practices can help you create effective images.
00:12:17.920 When creating your images, one critical aspect to consider is size. Size matters for Docker images in terms of resource usage during runtime, as well as build time and deployment time. A large image can lead to longer upload and download times, especially when working on CI systems or deploying to production systems.
00:12:56.160 There are many techniques for optimizing the size of your images. One common operation in Dockerfiles is installing software. While installing libraries or dependencies, it’s essential to clean up unnecessary files afterward. For example, using package managers like apt-get doesn't automatically remove the excess files that are downloaded.
00:14:40.170 It's good practice to run apt-get commands to update and clean in the same command, as Docker images use layers. If you run the update and clean in separate commands, the temporary files will still remain in a previous layer of your image, increasing its size.
00:16:27.380 It's important to install everything and clean up within a single RUN command. Another optimization strategy is to avoid keeping build tools and libraries in your final image. Multistage builds in Docker are extremely useful for situations where you need build tools only temporarily.
00:18:05.380 Multistage builds allow you to have an initial heavy stage for installation and building dependencies and then to discard that stage while keeping only the essential files needed to run your application. This is a useful technique to create smaller runtime images.
00:19:34.160 Now that we've talked a lot about the size of your images, let’s consider what should be included in your containers. Here are some important elements based on my experience with Docker images and Rails apps.
00:20:52.700 Initially, you need to verify that the locale is set correctly in your environment. If your operating system locale is not set properly, it can lead to unexpected behaviors in your Rails application. Consider including the following in your Dockerfile to set the encoding.
00:21:59.100 Another critical point is about running Rails as an unprivileged user. When working in containers, the default user is often a superuser, which can cause security issues. It is essential to explicitly create an unprivileged user in your container to run Rails applications.
00:23:08.160 This provides an added layer of security in case your application encounters issues. Always aim to implement defense-in-depth when securing your applications.
00:24:15.050 Let’s now talk about entrypoints. In Docker, you can define two different forms of commands: exec form and shell form. The exec form is better practice because it directly spawns the intended process as PID 1. In contrast, using the shell form generates a shell that will not propagate signals to the intended process.
00:25:52.660 Using shell form can lead to containers that do not shut down properly when a stop signal is sent. Therefore, it's recommended to prefer exec form. If you need to use shell features, execute the command with 'exec' in front.
00:26:40.150 Furthermore, it's essential to avoid implicit assumptions in your Dockerfile that may create dependencies on the structure defined by a base image. Being explicit and transparent helps ensure a smoother build process and reduces unexpected issues.
00:27:37.530 Let’s have a quick recap concerning the application running in production and containerization principles. Remember that containers provide isolation, which is crucial for predictability and stability in a production system. Always specify resource constraints for your containers.
00:28:28.550 This approach can help prevent unforeseen resource exhaustion and system failures. Finally, each container should have a static responsibility, avoiding unnecessary complication within a single container.
00:29:43.029 Now, moving on to logging practices in containers, Rails logs should be configured properly. By default, Rails logs to a file, which is not appropriate in a containerized environment as accessing logs can be complicated. The correct approach is to direct logs to standard output so that they can be managed easily in a container.
00:30:49.140 By utilizing environment variables, you can configure Rails to log to stdout instead. This simplifies monitoring and logging in production scenarios. Ensure your logs are valuable and accessible outside of the application.
00:32:41.180 We've covered numerous valuable tips today, and I hope you learned a lot. If you missed any of my tips, I will post them along with the slides later. Thank you all for coming! I’ll be around for questions.