Building a Rails project

This example will show how to create a simple Rails project using vagga.

Creating the project structure

First, let’s create a directory for our new project:

$ mkdir -p ~/projects/vagga-rails-tutorial && cd ~/projects/vagga-rails-tutorial

Now we need to create our project’s structure, so let’s create a new container and tell it to do so.

Create the vagga.yaml file and add the following to it:

containers:
  rails:
    setup:
    - !Ubuntu xenial
    - !Install 
      - zlib1g
    - !BuildDeps 
      - zlib1g-dev
    - !GemInstall [rails:5.0] 
    environ:
      HOME: /tmp ❸
  • ❶ – rails depends on nokogiri, which depends on zlib.
  • ❷ – tell gem to install rails.
  • ❸ – The rails new command, which we are going to use shortly, will complain if we do not have a $HOME. After our project is created, we won’t need it anymore.

We explicitly installed rails version 5.0. You can change to a newer version if it is available (5.1, for example) but your project may be slightly different.

And now run:

$ vagga _run rails rails new . --skip-bundle

This will create a new rails project in the current directory. The --skip-bundle flag tells rails new to not run bundle install, but don’t worry, vagga will take care of it for us.

Now that we have our rails project, let’s change our container fetch dependencies from Gemfile:

containers:
  base:
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install
      - zlib1g
      - libsqlite3-0 ❶
      - nodejs ❶
    - !BuildDeps
      - zlib1g-dev
      - libsqlite3-dev
    - !GemInstall
      - ffi
      - nokogiri
      - sqlite3
  rails:
    setup:
    - !Container base
    - !GemBundle 
  • ❶ – we need sqlite for the development database and nodejs for the asset pipeline (specifically, the uglifier gem).
  • ❷ – install dependencies from Gemfile using bundle install.

We are using two containers here, base and rails, for a good reason: some gems require building modules that can take some time to compile, so building them on the base container will avoid having to build them every time we need to rebuild our main container.

To test if everything is Ok, let’s create a command to run our project:

commands:
  run: !Command
    container: rails
    description: start rails development server
    run: rails server

Run the project:

$ vagga run

Now visit localhost:3000 to see rails default page.

Note

You may need to remove “tmp/pids/server.pid” in subsequent runs, otherwise, rails will complain that the server is already running.

Configuring the database from environment

By default, the rails new command will setup sqlite as the project database and store the configuration in config/databse.yml. However, we will use an environment variable to tell rails where to find our database. To do so, delete the rails database file:

$ rm config/database.yml

And then set the enviroment variable in our vagga.yaml:

containers:
  rails:
    setup:
      # ...
    environ:
      DATABASE_URL: sqlite3:db/development.sqlite3

This will tell rails to use the same file that was configured in database.yml.

Now if we run our project, everything should be the same.

Adding some code

Before going any further, let’s add some code to our project:

$ vagga _run rails rails g scaffold article title:string:index body:text

Rails scaffolding will generate everything we need, we just have to run the migrations:

$ vagga _run rails rake db:migrate

Now we need to tell rails to use our articles index page as the root of our project. Change config/routes.rb as follows:

# config/routes.rb

Rails.application.routes.draw do
  root 'articles#index'
  resources :articles
  # ...
end

Run the project now:

$ vagga run

You should see the articles list page rails generated for us.

Caching with memcached

Many projects use memcached to speed up things, so let’s try it out.

First, add dalli, a pure ruby memcached client, to our Gemfile:

gem 'dalli'

Then, open config/environments/development.rb, find the line that says config.cache_store = :memory_store and change it as follows:

# config/environments/production.rb
# ...
# config.cache_store = :memory_store
if ENV['MEMCACHED_URL']
  config.cache_store = :mem_cache_store, ENV['MEMCACHED_URL']
else
  config.cache_store = :memory_store
end
# ...

Create a container for memcached:

containers:
  # ...
  memcached:
    setup:
    - !Alpine v3.5
    - !Install [memcached]

Create the command to run with caching:

commands:
  # ...
  run-cached: !Supervise
    description: Start the rails development server alongside memcached
    children:
      cache: !Command
        container: memcached
        run: memcached -u memcached -vv ❶
      app: !Command
        container: rails
        environ:
          MEMCACHED_URL: memcached://127.0.0.1:11211 ❷
        run: |
            if [ ! -f 'tmp/caching-dev.txt' ]; then
              touch tmp/caching-dev.txt ❸
            fi
            rails server
  • ❶ – run memcached as verbose so we see can see the cache working
  • ❷ – set the cache url
  • ❸ – creating this file will tell rails to activate cache in development

Now let’s change some of our views to use caching:

<!-- app/views/articles/show.html.erb -->
<%# ... %>
<% cache @article do %>
  <p>
    <strong>Title:</strong>
    <%= @article.title %>
  </p>

  <p>
    <strong>Body:</strong>
    <%= @article.body %>
  </p>
<% end %>
<%# ... %>
<!-- app/views/articles/index.html.erb -->
<%# ... %>
<table>
  <%# ... %>
  <tbody>
    <% @articles.each do |article| %>
      <% cache article do %>
        <tr>
          <td><%= article.title %></td>
          <td><%= article.body %></td>
          <td><%= link_to 'Show', article %></td>
          <td><%= link_to 'Edit', edit_article_path(article) %></td>
          <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>
</table>
<%# ... %>

Run the project with caching:

$ vagga run-cached

Try adding some records. Keep an eye on the console to see rails talking to memcached.

We should try Postgres too

We can test our project against a Postgres database, which is probably what we will use in production.

First, add gem pg to our Gemfile

gem 'pg'

Then add the system dependencies for gem pg

containers:
  base:
    setup:
    - !Ubuntu xenial
    - !UbuntuUniverse
    - !Install
      - zlib1g
      - libsqlite3-0
      - nodejs
      - libpq5 ❶
    - !BuildDeps
      - zlib1g-dev
      - libsqlite3-dev
      - libpq-dev ❷
    - !GemInstall
      - ffi
      - nokogiri
      - sqlite3
      - pg
  rails:
    setup:
    - !Container base
    - !GemBundle
    environ:
      DATABASE_URL: sqlite3:db/development.sqlite3
  • ❶ – runtime dependency
  • ❷ – build dependency

Create the database container

containers:
  # ...
  postgres:
    setup:
    - !Ubuntu xenial
    - !EnsureDir /data
    - !Sh |
        addgroup --system --gid 200 postgres ❶
        adduser --uid 200 --system --home /data --no-create-home \
            --shell /bin/bash --group --gecos "PostgreSQL administrator" \
            postgres
    - !Install [postgresql-9.5]
    environ:
      PGDATA: /data
      PG_PORT: 5433
      PG_DB: test
      PG_USER: vagga
      PG_PASSWORD: vagga
      PG_BIN: /usr/lib/postgresql/9.5/bin
    volumes:
      /data: !Persistent
        name: postgres
        owner-uid: 200
        owner-gid: 200
        init-command: _pg-init ❷
      /run: !Tmpfs
        subdirs:
          postgresql: { mode: 0o777 }
  • ❶ – Use fixed user id and group id for postgres
  • ❷ – Vagga command to initialize the volume

Note

The database will be persisted in .vagga/.volumes/postgres.

Now add the command to initialize the database:

commands:
  # ...
  _pg-init: !Command
    description: Init postgres database
    container: postgres
    user-id: 200
    group-id: 200
    run: |
      set -ex
      ls -la /data
      $PG_BIN/pg_ctl initdb
      $PG_BIN/pg_ctl -w -o '-F --port=$PG_PORT -k /tmp' start
      $PG_BIN/createuser -h 127.0.0.1 -p $PG_PORT $PG_USER
      $PG_BIN/createdb -h 127.0.0.1 -p $PG_PORT $PG_DB -O $PG_USER
      $PG_BIN/psql -h 127.0.0.1 -p $PG_PORT -c "ALTER ROLE $PG_USER WITH ENCRYPTED PASSWORD '$PG_PASSWORD';"
      $PG_BIN/pg_ctl stop

And then add the command to run with Postgres:

commands:
  # ...
  run-postgres: !Supervise
    description: Start the rails development server using Postgres database
    children:
      app: !Command
        container: rails
        environ:
          DATABASE_URL: postgresql://vagga:vagga@127.0.0.1:5433/test
        run: |
            rake db:migrate
            rails server
      db: !Command
        container: postgres
        user-id: 200
        group-id: 200
        run: exec $PG_BIN/postgres -F --port=$PG_PORT

Now run:

$ vagga run-postgres

We can also add some default records to the database, so we don’t start with an empty database. To do so, add the following to db/seeds.rb:

# db/seeds.rb
if Article.count == 0
  Article.create([
    { title: 'Article 1', body: 'Lorem ipsum dolor sit amet' },
    { title: 'Article 2', body: 'Lorem ipsum dolor sit amet' },
    { title: 'Article 3', body: 'Lorem ipsum dolor sit amet' }
  ])
end

Now change the run-postgres command to seed the database:

commands:
  # ...
  run-postgres: !Supervise
    description: Start the rails development server using Postgres database
    children:
      app: !Command
        container: rails
        environ:
          DATABASE_URL: postgresql://vagga:vagga@127.0.0.1:5433/test
        run: |
            rake db:migrate
            rake db:seed ❶
            rails server
      db: !Command
        # ...
  • ❶ – populate the database.

Now, we run run-postgres, we will already have our database populated.

Running on Alpine linux

Alpine is a distribution focused on containers, being able to produce smaller smaller images than other distributions.

To run our project on alpine, we just need two more containers:

containers:
  # ...
  base-alpine:
    setup:
    - !Alpine v3.5
    - !Install
      - zlib
      - sqlite-libs
      - nodejs
      - libpq
      - tzdata ❶
      - ruby-bigdecimal ❶
      - ruby-json ❶
    - !BuildDeps
      - zlib-dev
      - sqlite-dev
      - postgresql-dev
      - libffi-dev
    - !GemInstall
      - ffi
      - nokogiri
      - sqlite3
      - pg

  rails-alpine:
    setup:
    - !Container base-alpine
    - !GemBundle
    environ:
      DATABASE_URL: sqlite3:db/development.sqlite3
  • ❶ – Rails needs these packages to work properly on Alpine. The packages
    ruby-bigdecimal and ruby-json could be added in Gemfile as well.

With our containers set up, we just need a command:

commands:
  # ...
  run-alpine: !Command
    container: rails-alpine
    description: Start the rails development server on Alpine container
    run: rails server

The command is almost identical to the first run, with the only difference being the container used.