Credit: DigitalOcean Being a web developer for the first couple years of my career, I didn’t need to learn much about server maintenance. When it came to deploying my own personal applications, Heroku was my best friends. A couple CLI commands and I had a live site. No hassle. However, when I joined DigitalOcean, I quickly learned that that would need to change. I would be involved in maintaining infrastructure and build pipelines. It was intimidating at first but I was determined to learn. While working on production servers gives me hands-on experience, I figured there were methods of practice that wouldn’t put millions of dollars at stake. That’s when I came up with the idea to move my from Heroku to a DigitalOcean . What better practice? It’s a small, static site that uses Sinatra for some lightweight routing. I figured it wouldn’t be too difficult to deploy. personal site Droplet Note: I’m undergoing some site maintenance so my website may not be operational when you’re reading this. There was one caveat. As a lazy developer, I love the ease of use of Heroku. For this migration to work, I needed to figure out a way to spin up my website in just one CLI command. That way, whenever I made changes, I could update the site in a matter of minutes. Luckily, that’s possible with . docker-compose In this article, I’ll be discussing how I was able to deploy my website to a DigitalOcean Droplet using Docker with the help of a app server and to serve my static files. Passenger Nginx By the end, you will have the tools you need to deploy and update your own Sinatra app with a single command, . docker-compose up Note: In case you’re unfamiliar with Docker, it’s a service that makes it incredibly easy to spin up apps inside of containers. How Docker works and the difference between virtual machines and containers are beyond the scope of this article. Read about it here if you’re curious. Additionally, I compiled a lot of this information from other tutorials . While I do add additional information and simplify many of the steps, if you get stuck, refer to them for additional information. Prerequisites Easily deploying a Sinatra app to DigitalOcean will require a number of upfront steps: Create a DigitalOcean Account and API Key Build a Sinatra app with a config.ru file Add the gem to your Gemfile passenger Install , , and Docker Docker-Machine Docker-Compose Why Docker-Machine? If you have docker installed natively on your machine, you may not see the need to install . However, we’re going to run our containers on a remote virtual machine. We will use to help us manage it. docker-machine docker-machine Once you have all of the necessary prereqs, we can begin. 1. Spin up a Droplet The first step in this process is to spin up the virtual machine (VM)where we’ll be hosting our app. DigitalOcean makes this pretty simple using their cloud dashboard. However, there’s an even easier way with . docker-machine One of the prerequisite steps was to create a DigitalOcean API key. With that in hand, you can create a DigitalOcean Droplet with a single command: docker-machine create --driver digitalocean --digitalocean-access-token <your API Key> <droplet name> I have my API token saved as an environment variable, , so my command looks like this: DO_TOKEN docker-machine create --driver digitalocean --digitalocean-access-token $DO_TOKEN personal-site As you can see from the output on your terminal, this is creating a Droplet and assigning it an ssh key on your behalf. You can even see it in your DigitalOcean dashboard. The default configuration is a 1 GB Ubuntu Droplet in the New York datacenter. if you have different configuration needs. Use additional flags From here, you should be able to ssh into the VM: docker-machine ssh <droplet name> You should find yourself on a shell inside your VM. It will look something like: root@<droplet name>:~# The default Droplet that is created has 1 GB of memory and the Ubuntu 16.04.4 operating system. For a small app, this should be fine. If your app requires something different, . use additional flags 2. Create a Dockerfile Passenger has an on how to manually configure Nginx and Passenger inside of a VM. It’s long and thorough but it will work if you prefer to do it manually. in depth tutorial We can avoid all of those steps by using Docker. In order to user Docker in our app, we need to include a Dockerfile. Passenger provides a variety of that we can build on. Since my personal site uses Ruby version 2.3.3, I picked the base image. base images phusion/passenger-ruby23 We can pull in a base image by including it in our Dockerfile. FROM phusion/passenger-ruby23:0.9.33 Note: As of the time of this article, version _0.9.33_ is the latest stable version of the _phusion/passenger_ base file. It’s good to include a specific version instead of _latest_ so that your app is not subject to breaking updates. Next we’ll want to set the correct environment for Docker, run Passenger’s init script, and enable Nginx: # Set correct environment variables.ENV HOME /root# Use baseimage-docker's init process.CMD ["/sbin/my_init"] # Enable Nginx (it is disabled by default)RUN rm -f /etc/service/nginx/down Next we’ll want to remove the default Nginx configuration and add our own. RUN rm /etc/nginx/sites-enabled/defaultADD app.conf /etc/nginx/sites-enabled/app.conf Don’t worry if you aren’t sure where this file came from. We haven’t created it yet. We’ll be doing that in the next section. app.conf These next lines warrant some explaination: WORKDIR /home/app/<app name>COPY --chown=app:app . .RUN bundle install The first line is telling Docker to create the directory and move to it. It’s the same as running: /home/app/<your app name> RUN mkdir /home/app/<app name>RUN cd /home/app/<app name> Why house our app inside ? As per the passenger-docker README: /home/app The [Passenger base] image has an user with UID 9999 and home directory . Your application is supposed to run as this user. Even though Docker itself provides some isolation from the host OS, running applications without root privileges is good security practice. app /home/app Your application should be placed inside /home/app. Note: when copying your application, make sure to set the ownership of the application directory to by calling app COPY --chown=app:app /local/path/of/your/app /home/app/webapp The Passenger base image is configured to run under the directory as the user. This is why we have the line: /home/app app COPY --chown=app:app . . It copies our app files over to the Docker container while simultaneously giving the user ownership of our app directory. We then run to install the gem dependencies from our app bundle install Gemfile. Finally, all that’s left is to clean up our image of any superfluous files to keep it’s size small and easy to deploy. RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* In the end, your Dockerfile should look something like this (I’ve swapped with as that is the name of my app): personalSite <app name> 3. Configure Nginx with app.conf Since we’re using Nginx in our application, let’s set it up. If you’ve used Unicorn or Puma before, you’re probably accustomed to long configuration files. Passenger takes care of all of the proxy boilerplate for us, so our doesn’t need to be complex. app.conf Mine looks like this: You don’t have to name this file . I just do it for simplicity. app.conf At the bottom of the block, we pass in some Passenger data. The field is telling Nginx the name of user that will be running the app on the VM. server passenger_user We know from the last section that this user is . We also know that the location of the application directory is inside . That’s why I pointed the value at . is the name of my app and is my static files folder. app /home/app root /home/app/personalSite/public personalSite public Finally, the field is telling Passenger which version of Ruby with which it should run the application. Since my site uses Ruby 2.3.3, I’m using binary. passenger_ruby /usr/bin/ruby2.3 Passenger comes with four main version of Ruby to choose from: /usr/bin/ruby2.1 /usr/bin/ruby2.2 /usr/bin/ruby2.3 /usr/bin/ruby2.4 Choose the one best suited for your application. With the file included, your project directory will look something like this: app.conf $ tree ..├── Dockerfile├── Gemfile├── Gemfile.lock├── Rakefile├── config.ru├── lib| └── removed for brevity...├── public| └── removed for brevity...├──app.rb├──views| └── removed for brevity...└── app.conf At this point, we have all the files we need to run our app on Docker. Run these commands in your terminal to see for yourself: # This points our local Docker client at your remote machine eval $(docker-machine env <droplet name>) # This will build and run your docker container docker build -t <image name> . && docker run -p 80:80 <image name> You can see your app by typing the IP address where your Droplet is located into a browser. You can find the IP address by either checking your DigitalOcean dashboard or asking : docker-machine docker-machine ip <machine name> 4. (Optional) Setup docker-compose.yml Since our apps can already run on Docker, this is an optional step. However, Docker Compose can simplify the deployment process even more. Docker Compose is meant to help orchestrate complicated deployments with multiple containers. However, even though my website is small, it will allow me to spin up my app with a simple . docker-compose up If you want to be able to do the same, add a simple file to your project repo. docker-compose.yml This file is mapping port on your VM to port of your Docker container. Hence the . What this means in layman terms is that anyone who visits your Droplet on port will be directed to the app on your Docker Container. 80 80 80:80 80 This is important. Without mapping these two ports together, your app would be isolated from the Internet and no one would be able to visit it. The property gives a relative path to the directory that contains our Dockerfile. Since the Dockerfile is inside the main directory, the relative path is . Finally, the name of your app goes in the section. build . <app name> This example is sufficient for a simple Sinatra app. If you require a more complicated setup, such as attaching a database, I would suggest . docker-compose.yml this tutorial from Jan David With this file in place, you should be able to run your app using . If you want to run the app in the background so that it doesn’t take up your terminal, just add the flag. docker-compose up -d docker-compose up -d 5. Get a domain and configure the nameservers With a running app in place, it’s time to point your domain at DigitalOcean’s nameservers. If you don’t have a domain yet, there are plenty of venders to choose from. If you’re not picky, you can get one from for free. dot.tk For paid domains, I usually go with . GoDaddy Having a domain isn’t enough. It’s not pointing to anything. This is where nameservers come in. Once you have your domain, go the tab on the DigitalOcean dashboard and add the domain to your account. Networking You should now be able to see the three nameservers that DigitalOcean provides. Go back to where you bought your domain from (dot.tk, Godaddy, etc) and add them to your DNS configuration. dot.tk Godaddy I would also recommend adding and A records to your domain in the DigitalOcean dashboard and point them at the droplet that is running your app. In the end, the DNS records for you domain should look similar to this www @ DNS is a tricky subject. If you have any trouble setting up nameservers or records, check out these . helpful tutorials There you have it. You should have a small Sinatra up and running in no time following these steps. The best part is that if you make changes to your site and want to update it with as little downtime as possible, just run: docker-compose up -d --build If in the future you need a refresher but don’t want to read this entire article again, I’ve listed all of the example files from this tutorial in . You can also see how I use this files in a production setting on . As I’m not a Docker expert, please let me know if you find any bugs! this gist my personal site’s repo So try it yourself and let me know how it goes in the comments below! Never stop pushing yourself to learn new things. Happy coding!