• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Dockerizing and Orchestrating Legacy Ruby Systems on Modern Linode Infrastructure

Dockerizing and Orchestrating Legacy Ruby Systems on Modern Linode Infrastructure

Assessing Legacy Ruby Application Dependencies

Before embarking on containerization, a thorough audit of the legacy Ruby application’s dependencies is paramount. This involves identifying not only direct gem dependencies but also system-level libraries, external services, and specific runtime versions (e.g., Ruby interpreter, Node.js for asset compilation). For older Rails applications, this might include specific versions of database drivers, caching mechanisms (Memcached, Redis), and background job processors (Sidekiq, Resque).

A common pitfall is assuming a `Gemfile` captures all requirements. System libraries like `imagemagick`, `libpq-dev`, or `build-essential` are often installed directly on the host and are critical for gem compilation or application functionality. Tools like `bundle viz` can help visualize gem dependencies, but manual inspection and testing are indispensable.

Crafting the Dockerfile for a Ruby Application

The `Dockerfile` is the blueprint for your container image. For a legacy Ruby application, it needs to be robust and reproducible. We’ll start with a minimal base image and layer dependencies carefully.

Consider a typical Rails application. We’ll need to install Ruby, essential build tools, and then our application’s gems. Using a specific Ruby version is crucial for compatibility.

Example Dockerfile for a Rails Application

# Use an official Ruby runtime as a parent image
FROM ruby:2.7.6

# Set the working directory in the container
WORKDIR /app

# Install essential build tools and system libraries
# Adjust these based on your application's specific needs
RUN apt-get update -qq && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    yarn \
    imagemagick \
    && rm -rf /var/lib/apt/lists/*

# Install gems
# Copy the Gemfile and Gemfile.lock first to leverage Docker cache
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock

RUN bundle install --jobs $(nproc) --retry 3

# Copy the rest of the application code
COPY . .

# Precompile assets if using Rails
RUN bundle exec rails assets:precompile

# Expose the port the app runs on
EXPOSE 3000

# Define the command to run your app
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

Key Considerations:

  • Base Image: Pinning to a specific Ruby version (e.g., `ruby:2.7.6`) ensures consistency. Avoid `latest` tags in production.
  • System Dependencies: The `apt-get install` command is critical. This is where you’ll add libraries like `libpq-dev` for PostgreSQL, `imagemagick` for image processing, or `redis-tools` if your app interacts with Redis directly.
  • Gem Caching: Copying `Gemfile` and `Gemfile.lock` before the rest of the application code allows Docker to cache the `bundle install` layer if only application code changes, significantly speeding up rebuilds.
  • Asset Precompilation: For Rails apps, `rails assets:precompile` should be run during the build process to avoid doing it on container startup.
  • `CMD` vs. `ENTRYPOINT`: `CMD` is suitable for running the Rails server. `ENTRYPOINT` might be used for more complex startup scripts that need to run before the server.

Containerizing Supporting Services (Databases, Caching)

Legacy applications often rely on external services like databases (PostgreSQL, MySQL) and caching layers (Redis, Memcached). These can also be containerized, simplifying deployment and management on Linode.

For databases, using official images is recommended. Configuration for persistence is crucial; this involves Docker volumes or Linode’s block storage.

Example `docker-compose.yml` for Development/Testing

version: '3.8'

services:
  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydatabase
    ports:
      - "5432:5432"

  redis:
    image: redis:6
    ports:
      - "6379:6379"

  app:
    build: .
    command: bundle exec rails server -b 0.0.0.0
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis

volumes:
  postgres_data:

This `docker-compose.yml` file defines three services: a PostgreSQL database, a Redis cache, and the Ruby application itself. The `app` service depends on `db` and `redis`, ensuring they are started first. The `volumes` section ensures data persistence for PostgreSQL.

Orchestration with Docker Swarm on Linode

For production deployments on Linode, orchestrating containers is essential for scalability, high availability, and management. Docker Swarm is a native clustering and orchestration tool that integrates well with Docker. Linode offers managed Kubernetes, but for simpler setups or teams already familiar with Docker, Swarm is a viable option.

The process involves setting up a Swarm manager node and then joining worker nodes. Linode Compute Instances can serve as these nodes.

Setting up a Docker Swarm Cluster

On your designated manager node (a Linode instance):

# Initialize Swarm
docker swarm init --advertise-addr 

# Note the join token outputted by the command. It will look like:
# Swarm initialized: current node (abcdef123456) is now a manager.
#
# To add a worker to this swarm, run the following command:
#     docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 192.168.1.100:2377
#
# To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

On each worker node (other Linode instances):

# Join the Swarm using the token from the manager
docker swarm join --token  :2377

Verify the cluster status from the manager node:

docker node ls

Deploying the Ruby Application as a Docker Swarm Service

Once the Swarm is set up, you can deploy your application using Docker Compose files, which Swarm understands. This is done by creating a “stack”.

Creating a Swarm Stack File (`docker-compose.yml`)

version: '3.8'

services:
  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydatabase
    deploy:
      replicas: 1 # For production, consider multiple replicas for HA
      restart_policy:
        condition: on-failure
    ports:
      - "5432:5432" # Expose only if needed externally, otherwise use internal network

  redis:
    image: redis:6
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    ports:
      - "6379:6379" # Expose only if needed externally

  app:
    build: . # This assumes your Dockerfile is in the same directory
    ports:
      - "80:3000" # Map host port 80 to container port 3000
    deploy:
      replicas: 3 # Scale the application to 3 instances
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgres://myuser:mypassword@db:5432/mydatabase
      REDIS_URL: redis://redis:6379

volumes:
  postgres_data:
    driver: local # Or use a named volume for better management

In this Swarm-specific `docker-compose.yml`:

  • `deploy` section: This is crucial for Swarm. It defines `replicas` for high availability and `restart_policy` for self-healing. `update_config` allows for rolling updates.
  • `ports` mapping: We map host port 80 to the application’s port 3000. Swarm will handle load balancing across the `app` service replicas.
  • Service Discovery: Services within the Swarm can communicate using their service names (e.g., `db`, `redis`) as hostnames, thanks to Docker’s built-in DNS.
  • Environment Variables: Connection strings for `DATABASE_URL` and `REDIS_URL` point to the service names, leveraging Swarm’s networking.

Deploying the Stack

# On the Swarm manager node
docker stack deploy -c docker-compose.yml my_ruby_app

This command deploys all services defined in the `docker-compose.yml` file as a Swarm stack. Docker Swarm will then ensure the desired number of replicas for each service are running and healthy.

Monitoring and Logging in a Swarm Environment

Effective monitoring and logging are critical for maintaining production systems. For Docker Swarm, a common pattern is to use a centralized logging driver and a monitoring stack.

Centralized Logging with Fluentd/Elasticsearch/Kibana (EFK) or Loki/Promtail/Grafana

You can deploy a logging agent (like Promtail for Loki, or Fluentd) as a DaemonSet on Swarm. This agent runs on each node and collects logs from containers, forwarding them to a central logging backend (Elasticsearch or Loki). Kibana or Grafana can then be used for visualization and querying.

A simplified logging setup might involve configuring the Docker daemon to log to `syslog` on the host, and then having a separate `syslog` collector on a dedicated logging server. However, for robust containerized logging, dedicated solutions are preferred.

Service Health Checks and Metrics

Docker Swarm services can define health checks within the `deploy` section of the `docker-compose.yml`. These checks allow Swarm to determine if a container is healthy and to restart or replace unhealthy ones.

# ... within the 'app' service definition ...
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
      # Add healthcheck
      health_check:
        test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/health || exit 1"] # Example for Rails app
        interval: 30s
        timeout: 10s
        retries: 3
        start_period: 60s

For metrics, integrating with Prometheus is a common approach. You would deploy Prometheus and configure it to scrape metrics from your application (if it exposes them) or from the Docker Swarm nodes themselves. Grafana can then visualize these metrics.

Advanced Considerations: Database Migrations and Zero-Downtime Deployments

Managing database migrations in a containerized, orchestrated environment requires careful planning to avoid downtime and data corruption.

Database Migrations Strategy

A common strategy is to run migrations as a separate, one-off task *before* deploying new application code. This can be achieved using a dedicated migration job within your Swarm stack.

# ... in your docker-compose.yml ...
services:
  # ... db and redis services ...

  app:
    build: .
    ports:
      - "80:3000"
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
    environment:
      DATABASE_URL: postgres://myuser:mypassword@db:5432/mydatabase
      REDIS_URL: redis://redis:6379
    # No direct dependency on migrations service

  db_migrations:
    build: . # Use the same app build context
    command: ["bundle", "exec", "rails", "db:migrate"]
    environment:
      DATABASE_URL: postgres://myuser:mypassword@db:5432/mydatabase
      REDIS_URL: redis://redis:6379
    deploy:
      replicas: 1
      restart_policy:
        condition: none # This is a one-off task
    depends_on:
      - db # Ensure DB is ready before running migrations

To deploy this:

# Deploy migrations first
docker stack deploy -c docker-compose.yml my_ruby_app_migrations

# Wait for migrations to complete (check logs)
docker service logs my_ruby_app_migrations_db_migrations

# Remove the migration stack
docker stack rm my_ruby_app_migrations

# Now deploy the main application stack with new code
docker stack deploy -c docker-compose.yml my_ruby_app

This ensures migrations are applied to the database before the new application code, which might expect those schema changes, starts running.

Zero-Downtime Deployments

Docker Swarm’s rolling update strategy (`update_config`) is key to zero-downtime deployments. By setting `parallelism` and `delay`, Swarm gradually replaces old containers with new ones, ensuring that at least some instances of your application are always available.

For truly seamless zero-downtime, especially with stateful applications or complex background jobs, consider integrating a load balancer (like HAProxy or Nginx, also containerized and managed by Swarm) that can gracefully drain connections from old containers before they are terminated.

The process would involve:

  • Deploying the new version of the application service with `parallelism: 1` and a `delay`.
  • Swarm starts one new container.
  • It waits for the `delay` and the health checks to pass.
  • It then terminates one old container.
  • This repeats until all old containers are replaced.

This approach, combined with careful database migration management, allows legacy Ruby systems to run reliably and scalably on modern Linode infrastructure.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala