Rails on Convox Example
This repository contains an example Ruby on Rails 4.2 app configured for local development and deployment to Convox.
The following is a step-by-step walkthrough of how the app was configured and why.
The Rails App
The first commit contains all the files generated by running rails new example
via the rails 4.2.6 gem.
Convox Init
The Convox CLI contains a command called convox init
, which we used to generate the files in the second commit, Dockerfile
, docker-compose.yml
, and .dockerignore
.
Since Convox uses Docker for containerization, these files are necessary to describe the application's containers and how to build them. The generated files are good defaults for most Rails apps, and they can also be edited to suit your specific needs.
Dockerfile
convox/rails Docker image
The generated Dockerfile inherits from the convox/rails Docker image, which has all the packages and configuration necessary to run your Rails app both locally and in production. This includes:
- OS libraries to support PostgreSQL, MySQL, and sqlite3 databases
- nodejs for compiling Javascript assets
- nginx for proxying client connections
- a Convox-friendly nginx config file
- a convox.rb file for logging to STDOUT
- a bin/web script for booting the app
how Dockerfile describes the build
Starting from the convox/rails
image, the generated Dockerfile executes the remaining build steps that your Rails app needs. There are basically 3 steps in this process, and they are executed in a particular order to take advantage of Docker's build caching behavior.
-
Gemfile
andGemfile.lock
are copied andbundle install
is run. This happens first because it is slow and something that's done infrequently. After running once, this step will be cached unless the cache is busted by later edits toGemfile
orGemfile.lock
. -
All the files necessary for the Rails asset pipeline are copied, and assets are built. Again, this is done early in the build process to optimize caching. The asset building step will only be run in the future if these files have changed.
-
The rest of the application source is copied over. These files will change frequently, so this step of the build will very rarely be cached.
docker-compose.yml
The docker-compose.yml file explains how to run the containers that make up your app. This generated file describes a web
container which will be your main Rails web process. The various sections of the web
configuration are described below:
build
build: .
This entry declares that the web container should use an image built from the top level of your application directory using the Dockerfile found there.
labels
labels:
- convox.port.443.protocol=tls
- convox.port.443.proxy=true
The labels section is used by Convox for configuration not covered by the official Compose spec. Here we're using it to configure how the load balancer handles traffic on port 443.
convox.port.443.protocol=tls
means that the load balancer listens on port 443 in TLS mode, accepting encrypted traffic and using your application's certificate to decrypt the messages.
convox.port.443.proxy=true
means that PROXY protocol TCP headers are injected into requests on port 443. These headers can then be used by nginx to set the HTTP headers your Rails application expects.
See the load balancer documentation for more detailed info.
ports
ports:
- 80:4000
- 443:4001
The ports section describes which ports your application listens on and which ports of the web container they map to. In this case the application is listening on ports 80 and 443 for http and https traffic. These requests get routed to the web container on ports 4000 and 4001, respectively.
.dockerignore
convox init
also generates a .dockerignore file that ignores files and directories not needed in the app's Docker image. It's important to have a good .dockerignore
to keep images small and builds, pushes and pulls fast.
Linking a database container
Up to this point the app has been using sqlite3 for its database. In a production environment, however, a database like PostgreSQL is more likely to be used. Linking a Postgres container to your app is pretty straightforward. Here's how we did it in this example.
Update the app
First we removed the sqlite3
gem and added the pg
gem.
Next we removed the config/database.yml
file. We'll be configuring the database via the DATABASE_URL
environment variable going forward.
Add a database container
We want to run a Postgres container for local development, so the next step is to add it to docker-compose.yml
.
We define a new process called database
, and use the convox/postgres
image:
database:
image: convox/postgres
ports:
- 5432
We want the database to listen for connections on port 5432. When we deploy this app an internal TCP load balancer will be created to listen on that port.
Persisting Data
Since the development database will run locally as a Docker container, it will start fresh with an empty database every time we run convox start
. While this blank-slate behavior can be nice sometimes, in this case we want to keep the data in our database across starts. This can be accomplished using Docker volumes.
By mounting a host volume onto our database container, we can keep many of the files the database creates. For convox/postgres
, we need to mount a host volume to /var/lib/postgresql/data
in the container.
The database
section of docker-compose.yml
now looks like:
database:
image: convox/postgres
ports:
- 5432
volumes:
- /var/lib/postgresql/data
NOTE: By omitting the host side of the host:container
volume specification, we let Convox choose a good location for the host volume. Development volumes are namespaced by app and stored in ~/.convox/volumes
.
Link database to web
Lastly, we need to link the database container to the web container. We do this by adding a links
section to web
:
links:
- database
This will cause a DATABASE_URL
environment variable to be injected into the web
environment, which it will use to connect to the database. You can read more about container linking here.
A linked container works well for local development. However, when you deploy this app, you'll want a "real" Postgres. To accomplish this you can provision an hosted Postgres instance via convox services, scale your database
process count in your app to 0, and set the DATABASE_URL environment variable to point to the hosted Postgres.
Running the app Locally
convox start
Developing in the container
Once you have your app up and running, you can take advantage of Convox code sync to execute rails
and rake
commands inside the container, while still editing and committing code on your host machine.
You can use the docker exec
command to get a bash
session on your web container:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
026cfe0ad0cb convox/proxy "proxy-link 443 4001 " 54 seconds ago Up 52 seconds 0.0.0.0:443->443/tcp rails-web-proxy-443
96a7d05ed465 convox/proxy "proxy-link 80 4000 t" 54 seconds ago Up 53 seconds 0.0.0.0:80->80/tcp rails-web-proxy-80
2776dea3621e rails/web "bin/web" 55 seconds ago Up 54 seconds 0.0.0.0:32807->4000/tcp, 0.0.0.0:32806->4001/tcp rails-web
e70c8bf0c74f rails/database "/docker-entrypoint.s" 55 seconds ago Up 54 seconds 0.0.0.0:32805->5432/tcp rails-database
Grab the CONTAINER ID of the rails/web process and exec onto the container:
$ docker exec -it 2776dea3621e bash
root@2776dea3621e:/app#
Once you're in the container you can run a Rails generator:
root@2776dea3621e:/app# rails g scaffold Book author:string title:string
Running via Spring preloader in process 52
invoke active_record
create db/migrate/20160808061714_create_books.rb
create /models/book.rb
invoke test_unit
create test/models/book_test.rb
create test/fixtures/books.yml
invoke resource_route
route resources :books
invoke scaffold_controller
create /controllers/books_controller.rb
invoke erb
create /views/books
create /views/books/index.html.erb
create /views/books/edit.html.erb
create /views/books/show.html.erb
create /views/books/new.html.erb
create /views/books/_form.html.erb
invoke test_unit
create test/controllers/books_controller_test.rb
invoke helper
create /helpers/books_helper.rb
invoke test_unit
invoke jbuilder
create /views/books/index.json.jbuilder
create /views/books/show.json.jbuilder
invoke assets
invoke coffee
create /assets/javascripts/books.coffee
invoke scss
create /assets/stylesheets/books.scss
invoke scss
create /assets/stylesheets/scaffolds.scss
And note that convox start
has synced the generated files back to your host where they can be edited and commited:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: config/routes.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/assets/javascripts/books.coffee
app/assets/stylesheets/books.scss
app/assets/stylesheets/scaffolds.scss
app/controllers/books_controller.rb
app/helpers/books_helper.rb
app/models/book.rb
app/views/books/
db/migrate/
test/controllers/books_controller_test.rb
test/fixtures/books.yml
test/models/book_test.rb
no changes added to commit (use "git add" and/or "git commit -a")
You can execute the generated database migration on the container as well:
root@2776dea3621e:/app# rake db:migrate
== 20160808061714 CreateBooks: migrating ======================================
-- create_table(:books)
-> 0.0590s
== 20160808061714 CreateBooks: migrated (0.0593s) =============================
You can now visit https://localhost/books and create a book record:
Since you're persisting Postgres data you can quit convox start
and run it again and the data will still be there!
Deploying the application
After installing a Rack create an app and deploy your code to it:
convox apps create myapp
convox deploy -a myapp
You should also create a Postgres service:
$ convox services create postgres --name myapp-db
$ convox services info myapp-db
Name myapp-db
Status running
Exports
URL: postgres://postgres:UWKXRYGYYRRKOSQFDDQPFUYQDOVHGX@convox-myapp-db.cbm068zjzjcr.us-east-1.rds.amazonaws.com:5432/app
Update your deployed app's environment to use the database service
$ convox env set postgres://postgres:UWKXRYGYYRRKOSQFDDQPFUYQDOVHGX@convox-myapp-db.cbm068zjzjcr.us-east-1.rds.amazonaws.com:5432/app --promote -a myapp
Don't forget to run the migrations:
$ convox run web rake db:migrate -a myapp
Now that your deployed app is using a Postgres service, it no longer needs the database container. Run the following command to stop running the container and deprovision its load balancer:
$ convox scale database --count=-1 -a myapp