├── .dockerignore ├── .env ├── .gitignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── Vagrantfile ├── myapp.rb ├── myapp.ru ├── myjob.rb ├── provisioning ├── dev.yml ├── prod.yml ├── prod_inventory └── roles │ ├── myapp │ ├── files │ │ └── .env │ ├── tasks │ │ └── main.yml │ └── vars │ │ └── main.yml │ └── ruby │ └── tasks │ └── main.yml └── vendor └── .keep /.dockerignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .env 3 | .git* 4 | .vagrant/* 5 | provisioning/* 6 | *.DS_Store 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | RACK_ENV=development 2 | PORT=3000 3 | MYAPP_CONFIG1=dev_config_1 4 | MYAPP_CONFIG2=dev_config_2 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### 2 | ### !!! DO NOT OVERWRITE THESE RULES !!! 3 | ### !!! THESE ARE AUTO GENERATED BY https://www.gitignore.io !!! 4 | ### !!! PUT CUSTOM IGNORE RULES AT THE BOTTOM !!! 5 | ### 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | 17 | ### Windows ### 18 | # Windows image file caches 19 | Thumbs.db 20 | ehthumbs.db 21 | 22 | # Folder config file 23 | Desktop.ini 24 | 25 | # Recycle Bin used on file shares 26 | $RECYCLE.BIN/ 27 | 28 | # Windows Installer files 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | # Windows shortcuts 35 | *.lnk 36 | 37 | 38 | ### OSX ### 39 | .DS_Store 40 | .AppleDouble 41 | .LSOverride 42 | 43 | # Icon must end with two \r 44 | Icon 45 | 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear on external disk 51 | .Spotlight-V100 52 | .Trashes 53 | 54 | # Directories potentially created on remote AFP share 55 | .AppleDB 56 | .AppleDesktop 57 | Network Trash Folder 58 | Temporary Items 59 | .apdisk 60 | 61 | 62 | ### Vagrant ### 63 | .vagrant/ 64 | 65 | 66 | ### Vim ### 67 | [._]*.s[a-w][a-z] 68 | [._]s[a-w][a-z] 69 | *.un~ 70 | Session.vim 71 | .netrwhist 72 | *~ 73 | 74 | 75 | ### Ruby ### 76 | *.gem 77 | *.rbc 78 | /.config 79 | /coverage/ 80 | /InstalledFiles 81 | /pkg/ 82 | /spec/reports/ 83 | /test/tmp/ 84 | /test/version_tmp/ 85 | /tmp/ 86 | 87 | ## Specific to RubyMotion: 88 | .dat* 89 | .repl_history 90 | build/ 91 | 92 | ## Documentation cache and generated files: 93 | /.yardoc/ 94 | /_yardoc/ 95 | /doc/ 96 | /rdoc/ 97 | 98 | ## Environment normalisation: 99 | /.bundle/ 100 | /lib/bundler/man/ 101 | 102 | # for a library or gem, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # Gemfile.lock 105 | # .ruby-version 106 | # .ruby-gemset 107 | 108 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 109 | .rvmrc 110 | 111 | 112 | ### RubyMine ### 113 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 114 | 115 | *.iml 116 | 117 | ## Directory-based project format: 118 | .idea/ 119 | # if you remove the above rule, at least ignore the following: 120 | 121 | # User-specific stuff: 122 | # .idea/workspace.xml 123 | # .idea/tasks.xml 124 | # .idea/dictionaries 125 | 126 | # Sensitive or high-churn files: 127 | # .idea/dataSources.ids 128 | # .idea/dataSources.xml 129 | # .idea/sqlDataSources.xml 130 | # .idea/dynamic.xml 131 | # .idea/uiDesigner.xml 132 | 133 | # Gradle: 134 | # .idea/gradle.xml 135 | # .idea/libraries 136 | 137 | # Mongo Explorer plugin: 138 | # .idea/mongoSettings.xml 139 | 140 | ## File-based project format: 141 | *.ipr 142 | *.iws 143 | 144 | ## Plugin-specific files: 145 | 146 | # IntelliJ 147 | out/ 148 | 149 | # mpeltonen/sbt-idea plugin 150 | .idea_modules/ 151 | 152 | # JIRA plugin 153 | atlassian-ide-plugin.xml 154 | 155 | # Crashlytics plugin (for Android Studio and IntelliJ) 156 | com_crashlytics_export_strings.xml 157 | crashlytics.properties 158 | crashlytics-build.properties 159 | 160 | 161 | ### SublimeText ### 162 | # cache files for sublime text 163 | *.tmlanguage.cache 164 | *.tmPreferences.cache 165 | *.stTheme.cache 166 | 167 | # workspace files are user-specific 168 | *.sublime-workspace 169 | 170 | # project files should be checked into the repository, unless a significant 171 | # proportion of contributors will probably not be using SublimeText 172 | # *.sublime-project 173 | 174 | # sftp configuration file 175 | sftp-config.json 176 | 177 | 178 | ### Rails ### 179 | *.rbc 180 | capybara-*.html 181 | .rspec 182 | /log 183 | /tmp 184 | /db/*.sqlite3 185 | /db/*.sqlite3-journal 186 | /public/system 187 | /coverage/ 188 | /spec/tmp 189 | **.orig 190 | rerun.txt 191 | pickle-email-*.html 192 | 193 | # TODO Comment out these rules if you are OK with secrets being uploaded to the repo 194 | config/initializers/secret_token.rb 195 | 196 | ## Environment normalisation: 197 | /.bundle 198 | /vendor/bundle 199 | 200 | # these should all be checked in to normalise the environment: 201 | # Gemfile.lock, .ruby-version, .ruby-gemset 202 | 203 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 204 | .rvmrc 205 | 206 | # if using bower-rails ignore default bower_components path bower.json files 207 | /vendor/assets/bower_components 208 | *.bowerrc 209 | bower.json 210 | 211 | # Ignore pow environment settings 212 | .powenv 213 | 214 | 215 | ### Node ### 216 | # Logs 217 | logs 218 | *.log 219 | 220 | # Runtime data 221 | pids 222 | *.pid 223 | *.seed 224 | 225 | # Directory for instrumented libs generated by jscoverage/JSCover 226 | lib-cov 227 | 228 | # Coverage directory used by tools like istanbul 229 | coverage 230 | 231 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 232 | .grunt 233 | 234 | # node-waf configuration 235 | .lock-wscript 236 | 237 | # Compiled binary addons (http://nodejs.org/api/addons.html) 238 | build/Release 239 | 240 | # Dependency directory 241 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 242 | node_modules 243 | 244 | ### 245 | ### 246 | ### !!! CUSTOM IGNORE RULES GO BELOW HERE !!! 247 | ### 248 | ### 249 | 250 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.2 2 | 3 | # Create user and group 4 | RUN groupadd myapp --gid 6156 && \ 5 | useradd --home /home/myapp --create-home --shell /bin/false --uid 6157 --gid 6156 myapp 6 | 7 | # Docker Ruby image has some quirks that disallow it from running as a normal user 8 | ENV BUNDLE_APP_CONFIG . 9 | 10 | # Run stuff as myapp from now on 11 | USER myapp 12 | 13 | # Create and switch to the repo dir 14 | ENV REPO_DIR /home/myapp/repo 15 | RUN mkdir $REPO_DIR 16 | WORKDIR $REPO_DIR 17 | 18 | # First install gems 19 | COPY Gemfile $REPO_DIR/ 20 | COPY Gemfile.lock $REPO_DIR/ 21 | RUN bundle install --deployment --without development test 22 | 23 | # Then copy over rest of the app 24 | # .dockerignore will ensure that unnecessary files aren't copied 25 | COPY . $REPO_DIR 26 | 27 | # This will be the default command 28 | CMD bundle exec foreman start 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra', '~> 1.4.5' 4 | gem 'rufus-scheduler', '~> 3.0.9', require: false 5 | gem 'foreman', '~> 0.78.0' 6 | 7 | group :development do 8 | gem 'sinatra-contrib', '~> 1.4.2' 9 | gem 'rack-mini-profiler', '~> 0.9.3' 10 | end 11 | 12 | group :production do 13 | gem 'puma', '~> 2.11', require: false 14 | gem 'rails_12factor' 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | backports (3.6.4) 5 | foreman (0.78.0) 6 | thor (~> 0.19.1) 7 | multi_json (1.11.0) 8 | puma (2.11.1) 9 | rack (>= 1.1, < 2.0) 10 | rack (1.6.0) 11 | rack-mini-profiler (0.9.3) 12 | rack (>= 1.1.3) 13 | rack-protection (1.5.3) 14 | rack 15 | rack-test (0.6.3) 16 | rack (>= 1.0) 17 | rails_12factor (0.0.3) 18 | rails_serve_static_assets 19 | rails_stdout_logging 20 | rails_serve_static_assets (0.0.4) 21 | rails_stdout_logging (0.0.3) 22 | rufus-scheduler (3.0.9) 23 | tzinfo 24 | sinatra (1.4.5) 25 | rack (~> 1.4) 26 | rack-protection (~> 1.4) 27 | tilt (~> 1.3, >= 1.3.4) 28 | sinatra-contrib (1.4.2) 29 | backports (>= 2.0) 30 | multi_json 31 | rack-protection 32 | rack-test 33 | sinatra (~> 1.4.0) 34 | tilt (~> 1.3) 35 | thor (0.19.1) 36 | thread_safe (0.3.5) 37 | tilt (1.4.1) 38 | tzinfo (1.2.2) 39 | thread_safe (~> 0.1) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | foreman (~> 0.78.0) 46 | puma (~> 2.11) 47 | rack-mini-profiler (~> 0.9.3) 48 | rails_12factor 49 | rufus-scheduler (~> 3.0.9) 50 | sinatra (~> 1.4.5) 51 | sinatra-contrib (~> 1.4.2) 52 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma myapp.ru 2 | jobs: bundle exec ruby myjob.rb 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby (and Rails) Deployment Kickstart 2 | 3 | A template for deploying Ruby and Rails applications, with automation support for Ansible, Docker and Vagrant. Copy this over to your project, and modify to fit. 4 | 5 | Read the accompanying [blog post](https://medium.com/@rdsubhas/ruby-in-production-lessons-learned-36d7ab726d99) for more details. 6 | 7 | ## Structure 8 | 9 | **NOTE:** The files will look overwhelming at first, because they contain Vagrant, Ansible and Docker. You don't need everything. If you don't want Docker, get rid of Docker-related files. Similarly, if you don't want Ansible or Vagrant, get rid of those files. 10 | 11 | * Core application files are: `myapp.rb, myapp.ru, myjobs.rb` 12 | * Core supporting files are: `.env`, `Procfile`, `Gemfile*`, `provisioning/roles/myapp/files/.env` 13 | * The default `.env` file is suitable for development mode 14 | * In production, it is overwritten with `provisioning/roles/myapp/files/.env` 15 | * Docker: `.dockerignore, Dockerfile` 16 | * Vagrant: `Vagrantfile` 17 | * Ansible: `provisioning/*` 18 | 19 | ## Development 20 | 21 | * Install Vagrant and VirtualBox 22 | * Preferably, install `vagrant-cachier`, `vagrant-exec` and `vagrant-faster` plugins for a faster development experience 23 | * Run: `vagrant up` 24 | * Run: `vagrant ssh` > `cd /vagrant` > `foreman start` 25 | * Or if you have installed vagrant-exec, simply run `vagrant exec foreman start` 26 | * Go to `localhost:3000`, it will echo you a message with development configurations 27 | 28 | ## Production using Ansible 29 | 30 | *Supports only Debian and Ubuntu for now* 31 | 32 | #### Testing: 33 | 34 | * Stop everything else: `vagrant halt` 35 | * Start the Prod VM: `vagrant up prod` 36 | * Go to `localhost:3000`, it will echo you the same message with production configuration 37 | 38 | #### Actual Deployment: 39 | 40 | * Go to `provisioning` folder 41 | * Edit `prod_inventory` and update your production server name 42 | * Run: `ansible-playbook -i prod_inventory -u -vvv prod.yml` 43 | 44 | ## Production using Docker 45 | 46 | *Supports any OS that can run Docker, not limited to Debian/Ubuntu* 47 | 48 | #### Testing: 49 | 50 | * Stop everything else: `vagrant halt` 51 | * Start the Docker VM: `vagrant up docker` 52 | * Go to `localhost:3000`, it will echo you the same message with production configuration, except things are running using Docker now 53 | 54 | #### Actual Deployment: 55 | 56 | * Check the Docker section in `Vagrantfile` for the build and run commands 57 | * A full discussion of Docker is beyond the scope of this, but the gist is: 58 | * Build the Docker image (can be done locally or on the server) 59 | * Push it to a registry (not needed if you built on the server) 60 | * Run it in the server 61 | * For exact commands, check the Docker section in `Vagrantfile` 62 | 63 | ## In case of errors 64 | 65 | * Destroy the VMs: `vagrant destroy` 66 | * Start whatever you want up again 67 | 68 | ## Using it in your project: 69 | 70 | * Add Foreman and rails_12factor to your Gemfile 71 | * Copy necessary files (provisioning, .env, Procfile, Vagrantfile, Dockerfile) from here to your project 72 | * Modify them to fit, especially: 73 | * Procfile commands 74 | * roles/myapp/ 75 | * Encrypt production configuration (using git-crypt, ansible vault, or anything else) 76 | * `provisioning/roles/myapp/files` should be encrypted 77 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.network 'forwarded_port', guest: 3000, host: 3000 9 | 10 | # Development VM 11 | config.vm.define "dev", primary: true do |dev| 12 | dev.vm.box = "ubuntu/trusty64" 13 | dev.vm.provision "ansible" do |ansible| 14 | ansible.playbook = 'provisioning/dev.yml' 15 | ansible.verbose = 'vvv' 16 | end 17 | end 18 | 19 | # Production VM 20 | config.vm.define "prod", autostart: false do |prod| 21 | prod.vm.box = "ubuntu/trusty64" 22 | prod.vm.provision "ansible" do |ansible| 23 | ansible.playbook = 'provisioning/prod.yml' 24 | ansible.verbose = 'vvv' 25 | end 26 | end 27 | 28 | # Production VM with Docker Build 29 | config.vm.define "docker", autostart: false do |docker| 30 | docker.vm.box = "yungsang/boot2docker" 31 | docker.vm.synced_folder ".", "/vagrant" 32 | docker.vm.provision "shell", inline: <<-SCRIPT 33 | cd /vagrant 34 | 35 | echo "Building myapp docker image..." 36 | docker build -t myapp . 37 | 38 | echo "Killing old myapp container..." 39 | (docker kill myapp && docker rm myapp) || echo "Not running" 40 | 41 | echo "Running myapp docker image..." 42 | docker run \ 43 | --env-file=provisioning/roles/myapp/files/.env \ 44 | --publish=3000:3000 \ 45 | --name=myapp \ 46 | --detach=true \ 47 | myapp 48 | SCRIPT 49 | end 50 | 51 | # Use vagrant-cachier to cache apt-get, gems and other stuff across machines 52 | # Also consider using vagrant-exec, vagrant-faster and vagrant-omnibus 53 | if Vagrant.has_plugin?('vagrant-cachier') 54 | config.cache.scope = :box 55 | else 56 | puts "Run `vagrant plugin install vagrant-cachier` to reduce caffeine intake when provisioning" 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /myapp.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/reloader' if development? 3 | require 'rack-mini-profiler' if development? 4 | require 'json' 5 | 6 | class Myapp < Sinatra::Base 7 | 8 | configure :development do 9 | # Enable hot reloading in development mode 10 | register Sinatra::Reloader 11 | use Rack::MiniProfiler 12 | end 13 | 14 | # Enable request logging 15 | set :logging, true 16 | 17 | # If you're planning to proxy behind nginx, 18 | # Then you can move this bind inside `configure :development` 19 | set :bind, '0.0.0.0' 20 | 21 | # A simple endpoint 22 | get '/' do 23 | config = ENV.select{ |k,v| k =~ /^MYAPP_/ } 24 | return "Running with config: #{config.inspect}" 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /myapp.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require './myapp' 4 | Myapp.run! 5 | -------------------------------------------------------------------------------- /myjob.rb: -------------------------------------------------------------------------------- 1 | require 'rufus-scheduler' 2 | require 'logger' 3 | 4 | logger = Logger.new(STDOUT) 5 | $stdout.sync = true 6 | 7 | scheduler = Rufus::Scheduler.new 8 | 9 | scheduler.every '10s' do 10 | logger.info "My job is triggered!" 11 | end 12 | 13 | logger.info 'My jobs are running!' 14 | scheduler.join 15 | -------------------------------------------------------------------------------- /provisioning/dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | sudo: yes 4 | 5 | roles: 6 | - ruby 7 | 8 | # Development-specific tasks 9 | # If they get too large, make them a role as well 10 | tasks: 11 | - name: Run bundle install 12 | shell: bundle install chdir=/vagrant 13 | 14 | # For Rails apps, you can also initialize the Database here 15 | # shell: bundle exec rake db:create db:migrate 16 | -------------------------------------------------------------------------------- /provisioning/prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | sudo: yes 4 | 5 | roles: 6 | - ruby 7 | - myapp 8 | -------------------------------------------------------------------------------- /provisioning/prod_inventory: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /provisioning/roles/myapp/files/.env: -------------------------------------------------------------------------------- 1 | RACK_ENV=production 2 | PORT=3000 3 | MYAPP_CONFIG1=prod_config_1 4 | MYAPP_CONFIG2=prod_config_2 5 | -------------------------------------------------------------------------------- /provisioning/roles/myapp/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create group 3 | group: > 4 | name={{myapp_group}} gid={{myapp_gid}} state=present 5 | 6 | - name: Create user 7 | user: > 8 | name={{myapp_user}} uid={{myapp_uid}} group={{myapp_group}} 9 | home={{myapp_home}} createhome=yes shell=/bin/false state=present 10 | 11 | - name: Create folders 12 | file: > 13 | path={{item}} owner={{myapp_user}} group={{myapp_group}} 14 | mode=1700 state=directory 15 | with_items: 16 | - "{{myapp_home}}" 17 | - "{{myapp_home}}/gems" 18 | - "{{myapp_repo}}" 19 | 20 | ### 21 | ### Running stuff as {{myapp_user}} whenever permissions need to be preserved 22 | ### 23 | 24 | - name: Clone application repository 25 | git: > 26 | repo={{myapp_repo_url}} dest={{myapp_repo}} 27 | version=master accept_hostkey=yes force=yes 28 | sudo_user: "{{myapp_user}}" 29 | # For private apps, copy the key file and specify "key_file" in the git task 30 | # Use git-crypt, ansible-vault or anything that you want 31 | # But don't commit secret files unencrypted, even in private repositories 32 | 33 | - name: Copy dotEnv for this server 34 | copy: > 35 | src=.env dest={{myapp_repo}}/.env owner={{myapp_user}} group={{myapp_group}} mode=0600 36 | 37 | - name: Symlink persistent folders 38 | file: state=link force=yes src={{item.src}} dest={{item.dest}} 39 | with_items: 40 | - { src: "{{myapp_home}}/gems", dest: "{{myapp_repo}}/vendor/bundle" } 41 | 42 | - name: Bundle install in deployment mode 43 | shell: > 44 | bundle install --deployment --clean --without development test 45 | args: 46 | chdir: "{{myapp_repo}}" 47 | sudo_user: "{{myapp_user}}" 48 | 49 | ### At this point, you may want to run any custom tasks 50 | ### (like db:migrate, assets:precompile, etc) 51 | ### Just like the bundle install above 52 | 53 | - name: Create system service 54 | shell: > 55 | PATH=$(pwd):$PATH bundle exec foreman export -a myapp -u {{myapp_user}} upstart /etc/init/ 56 | args: 57 | chdir: "{{myapp_repo}}" 58 | 59 | - name: Restart service and enable at boot 60 | service: name=myapp state=restarted enabled=yes 61 | -------------------------------------------------------------------------------- /provisioning/roles/myapp/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | myapp_gid: 6156 3 | myapp_group: myapp 4 | myapp_uid: 6157 5 | myapp_user: myapp 6 | myapp_home: /home/myapp 7 | myapp_repo: /home/myapp/repo 8 | myapp_repo_url: https://github.com/rdsubhas/ruby-deploy-kickstart.git 9 | -------------------------------------------------------------------------------- /provisioning/roles/ruby/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Ruby repository 3 | apt_repository: repo='ppa:brightbox/ruby-ng' 4 | 5 | - name: Install packages 6 | apt: name={{ item }} state=present 7 | with_items: 8 | - build-essential 9 | - git 10 | - libxml2-dev 11 | - libxslt1-dev 12 | - libssl-dev 13 | - zlib1g-dev 14 | - nodejs 15 | - curl 16 | - wget 17 | - ruby2.2 18 | - ruby2.2-dev 19 | - ruby-switch 20 | 21 | - name: Dont install gem docs, they will slow down gem install 22 | lineinfile: "dest=/etc/gemrc line='gem: --no-ri --no-rdoc' create=yes" 23 | 24 | - name: Check bundler is installed or not 25 | shell: bundle -v 26 | register: bundler_present 27 | ignore_errors: True 28 | 29 | - name: Install bundler if not yet present 30 | shell: gem install bundler 31 | when: bundler_present|failed 32 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/ruby-deploy-kickstart/c62882f8a81cddafe6b6d3349ae364768d2af33d/vendor/.keep --------------------------------------------------------------------------------