├── app ├── config.ru ├── test │ ├── test_helper.rb │ └── app_test.rb ├── Rakefile ├── db │ ├── migrate │ │ └── 20160207203318_create_resources.rb │ └── schema.rb ├── config │ └── database.yml ├── Gemfile └── app.rb ├── docker └── vhost.conf ├── docker-compose.yml ├── Dockerfile ├── LICENSE.txt ├── .gitignore └── README.md /app/config.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require File.expand_path '../app.rb', __FILE__ 3 | 4 | run Sinatra::Application 5 | -------------------------------------------------------------------------------- /docker/vhost.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /home/app/webapp/public; 5 | 6 | passenger_enabled on; 7 | passenger_user app; 8 | 9 | passenger_ruby /usr/bin/ruby2.2; 10 | } 11 | -------------------------------------------------------------------------------- /app/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | require 'minitest/autorun' 4 | require 'rack/test' 5 | 6 | require File.expand_path '../../app.rb', __FILE__ 7 | 8 | include Rack::Test::Methods 9 | 10 | def app 11 | Sinatra::Application 12 | end 13 | -------------------------------------------------------------------------------- /app/Rakefile: -------------------------------------------------------------------------------- 1 | require './app' 2 | require 'rake/testtask' 3 | require 'sinatra/activerecord/rake' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.test_files = FileList['test/*_test.rb'] 8 | t.verbose = true 9 | end 10 | 11 | task default: :test 12 | -------------------------------------------------------------------------------- /app/db/migrate/20160207203318_create_resources.rb: -------------------------------------------------------------------------------- 1 | class CreateResources < ActiveRecord::Migration 2 | def change 3 | create_table :resources do |t| 4 | t.string :name, null: false, default: '' 5 | 6 | t.timestamps null: false 7 | end 8 | 9 | add_index :resources, :name, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | api: 2 | build: . 3 | ports: 4 | - "4567:80" 5 | links: 6 | - "db" 7 | environment: 8 | - DATABASE_HOST=db 9 | - DATABASE_NAME=sinatra 10 | - DATABASE_USER=sinatra 11 | - DATABASE_PASSWORD=sinatra 12 | db: 13 | image: "postgres" 14 | environment: 15 | - POSTGRES_USER=sinatra 16 | - POSTGRES_PASSWORD=sinatra 17 | ports: 18 | - "5432:5432" 19 | -------------------------------------------------------------------------------- /app/config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | pool: 5 4 | timeout: 5000 5 | 6 | development: 7 | <<: *default 8 | database: db/development.sqlite3 9 | 10 | test: 11 | <<: *default 12 | database: db/test.sqlite3 13 | 14 | production: 15 | adapter: postgresql 16 | encoding: unicode 17 | pool: 5 18 | host: <%= ENV['DATABASE_HOST'] || 'db' %> 19 | database: <%= ENV['DATABASE_NAME'] || 'sinatra' %> 20 | username: <%= ENV['DATABASE_USER'] || 'sinatra' %> 21 | password: <%= ENV['DATABASE_PASSWORD'] || 'sinatra' %> 22 | -------------------------------------------------------------------------------- /app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Fix Sinatra version to ensure compatibility with upcoming releases 4 | gem 'sinatra', '1.4.6' 5 | 6 | # Use JSON extension provided by the Sinatra::Contrib project 7 | gem 'sinatra-contrib', '~> 1.4.2' 8 | 9 | # Use ActiveRecord as the ORM 10 | gem 'sinatra-activerecord', '~> 2.0' 11 | 12 | # Use rake to execute ActiveRecord's tasks 13 | gem 'rake' 14 | 15 | group :production do 16 | # Use Postgresql for ActiveRecord 17 | gem 'pg' 18 | end 19 | 20 | group :development, :test do 21 | # Use SQLite for ActiveRecord 22 | gem 'sqlite3' 23 | end 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/passenger-ruby22:0.9.18 2 | 3 | MAINTAINER Jan David 4 | 5 | # Set correct environment variables. 6 | ENV HOME /root 7 | 8 | # Use baseimage-docker's init process. 9 | CMD ["/sbin/my_init"] 10 | 11 | # Enable nginx and Passenger 12 | RUN rm -f /etc/service/nginx/down 13 | 14 | # Remove the default site 15 | RUN rm /etc/nginx/sites-enabled/default 16 | 17 | # Create virtual host 18 | ADD docker/vhost.conf /etc/nginx/sites-enabled/app.conf 19 | 20 | # Prepare folders 21 | RUN mkdir /home/app/webapp 22 | 23 | # Run Bundle in a cache efficient way 24 | WORKDIR /tmp 25 | COPY app/Gemfile /tmp/ 26 | COPY app/Gemfile.lock /tmp/ 27 | RUN bundle install 28 | 29 | # Add our app 30 | COPY app /home/app/webapp 31 | RUN chown -R app:app /home/app 32 | 33 | # Clean up when done. 34 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 35 | -------------------------------------------------------------------------------- /app/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/json' 3 | require 'sinatra/activerecord' 4 | 5 | set :database_file, 'config/database.yml' 6 | 7 | class Resource < ActiveRecord::Base 8 | validates :name, presence: true, uniqueness: true 9 | end 10 | 11 | get '/' do 12 | json Resource.select('id', 'name').all 13 | end 14 | 15 | get '/:id' do 16 | resource = Resource.find_by_id(params[:id]) 17 | 18 | if resource 19 | json resource 20 | else 21 | halt 404 22 | end 23 | end 24 | 25 | post '/' do 26 | resource = Resource.create(params) 27 | 28 | if resource 29 | halt 206, json(resource) 30 | else 31 | halt 500 32 | end 33 | end 34 | 35 | patch '/:id' do 36 | resource = Resource.find_by_id(params[:id]) 37 | 38 | if resource 39 | resource.update(name: params[:name]) 40 | else 41 | halt 404 42 | end 43 | end 44 | 45 | delete '/:id' do 46 | resource = Resource.find_by_id(params[:id]) 47 | 48 | if resource 49 | resource.destroy 50 | else 51 | halt 404 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Jan David Nose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160207203318) do 15 | 16 | create_table "resources", force: :cascade do |t| 17 | t.string "name", default: "", null: false 18 | t.datetime "created_at", null: false 19 | t.datetime "updated_at", null: false 20 | end 21 | 22 | add_index "resources", ["name"], name: "index_resources_on_name", unique: true 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/test/app_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../test_helper.rb', __FILE__ 2 | 3 | class TestApp < MiniTest::Test 4 | def setup 5 | Resource.create(name: 'Resource #1') 6 | end 7 | 8 | def test_index 9 | resources = Resource.select('id', 'name') 10 | 11 | get '/' 12 | assert last_response.ok? 13 | assert_equal resources.to_json, last_response.body 14 | end 15 | 16 | def test_show 17 | resource = Resource.first 18 | 19 | get "/#{resource.id}" 20 | assert last_response.ok? 21 | assert_equal resource.to_json, last_response.body 22 | end 23 | 24 | def test_create 25 | post '/', name: 'Created Resource' 26 | 27 | resource = Resource.find_by_name 'Created Resource' 28 | 29 | refute_nil resource 30 | assert_equal 'Created Resource', resource.name 31 | end 32 | 33 | def test_update 34 | resource = Resource.first 35 | 36 | patch "/#{resource.id}", name: 'Updated Resource' 37 | 38 | assert_equal 'Updated Resource', resource.reload.name 39 | end 40 | 41 | def test_delete 42 | resource = Resource.first 43 | count = Resource.count 44 | 45 | delete "/#{resource.id}" 46 | 47 | assert_equal count - 1, Resource.count 48 | assert_raises ActiveRecord::RecordNotFound do 49 | resource.reload 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux.gitignore 2 | *~ 3 | 4 | # temporary files which can be created if a process still has a handle open of a deleted file 5 | .fuse_hidden* 6 | 7 | # KDE directory preferences 8 | .directory 9 | 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | 13 | 14 | ### OSX.gitignore 15 | .DS_Store 16 | .AppleDouble 17 | .LSOverride 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | 23 | # Thumbnails 24 | ._* 25 | 26 | # Files that might appear in the root of a volume 27 | .DocumentRevisions-V100 28 | .fseventsd 29 | .Spotlight-V100 30 | .TemporaryItems 31 | .Trashes 32 | .VolumeIcon.icns 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | 42 | ### SublimeText.gitignore 43 | # cache files for sublime text 44 | *.tmlanguage.cache 45 | *.tmPreferences.cache 46 | *.stTheme.cache 47 | 48 | *.sublime-workspace 49 | *.sublime-project 50 | 51 | # sftp configuration file 52 | sftp-config.json 53 | 54 | 55 | ### Windows.gitignore 56 | # Windows image file caches 57 | Thumbs.db 58 | ehthumbs.db 59 | 60 | # Folder config file 61 | Desktop.ini 62 | 63 | # Recycle Bin used on file shares 64 | $RECYCLE.BIN/ 65 | 66 | # Windows Installer files 67 | *.cab 68 | *.msi 69 | *.msm 70 | *.msp 71 | 72 | # Windows shortcuts 73 | *.lnk 74 | 75 | 76 | ### Ruby.gitignore 77 | *.gem 78 | *.rbc 79 | /.config 80 | /coverage/ 81 | /InstalledFiles 82 | /pkg/ 83 | /spec/reports/ 84 | /spec/examples.txt 85 | /test/tmp/ 86 | /test/version_tmp/ 87 | /tmp/ 88 | 89 | ## Specific to RubyMotion: 90 | .dat* 91 | .repl_history 92 | build/ 93 | 94 | ## Documentation cache and generated files: 95 | /.yardoc/ 96 | /_yardoc/ 97 | /doc/ 98 | /rdoc/ 99 | 100 | ## Environment normalization: 101 | /.bundle/ 102 | /vendor/bundle 103 | /lib/bundler/man/ 104 | 105 | Gemfile.lock 106 | .ruby-version 107 | .ruby-gemset 108 | 109 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 110 | .rvmrc 111 | 112 | 113 | ### Sinatra 114 | /app/db/*.sqlite3 115 | /app/db/*.sqlite3-journal 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerize Sinatra 2 | 3 | This project is indented as an example how to combine a 4 | **[Sinatra](http://sinatrarb.com)** app serving a **JSON API** with both 5 | **[Docker](http://docker.com)** and 6 | **[ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html)**. It 7 | has been developed as part of a blog series on this topic. You can read the 8 | articles here: 9 | 10 | - [Part 1: Dockerizing Sinatra](http://coding.jandavid.de/2016/01/22/dockerizing-sinatra/) 11 | - [Part 2: Making Docker an art with Compose](http://coding.jandavid.de/2016/01/29/making-docker-an-art-with-compose/) 12 | - [Part 3: How to set up Sinatra with ActiveRecord](http://coding.jandavid.de/2016/02/08/how-to-set-up-sinatra-with-activerecord/) 13 | - [Part 4: Dockerized databases with Sinatra](http://coding.jandavid.de/2016/02/12/dockerized-databases-with-sinatra) 14 | 15 | ## Sinatra 16 | 17 | The **Sinatra** app is inside the directory `/app`. It consists of a `Gemfile` 18 | declaring its dependencies, a rackup file `config.ru` and the actual application 19 | itself in `app.rb`. 20 | 21 | ## Docker 22 | 23 | The application extends [phusion's](http://www.phusion.nl) **Docker** image 24 | [phusion/passenger-ruby22](https://github.com/phusion/passenger-docker), and 25 | adds instructions to install the app and configure the bundled **nginx** server 26 | (see `docker/vhost.conf`). 27 | 28 | It also uses a database that is provided by the 29 | [official PostgreSQL image](https://hub.docker.com/_/postgres/). Using 30 | **docker-compose**, both containers can be started simultaneously. 31 | 32 | ## Usage 33 | 34 | You can use **docker-compose** to start the application. The following command 35 | starts all containers: 36 | 37 | ``` 38 | $ docker-compose up 39 | ``` 40 | 41 | If you want to inspect the database, you can connect to it using **psql**: 42 | 43 | ``` 44 | psql -U sinatra sinatra 45 | ``` 46 | 47 | # License 48 | 49 | Copyright (c) 2016 Jan David Nose 50 | 51 | Permission is hereby granted, free of charge, to any person obtaining a copy 52 | of this software and associated documentation files (the "Software"), to deal 53 | in the Software without restriction, including without limitation the rights 54 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 55 | copies of the Software, and to permit persons to whom the Software is 56 | furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in 59 | all copies or substantial portions of the Software. 60 | 61 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 62 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 63 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 64 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 65 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 66 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 67 | THE SOFTWARE. 68 | --------------------------------------------------------------------------------