Easily deploying a Sinatra app to DigitalOcean will require a number of upfront steps:
If you have docker installed natively on your machine, you may not see the need to install
docker-machine. However, we’re going to run our containers on a remote virtual machine. We will use
docker-machine to help us manage it.
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
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
I have my API token saved as an environment variable,
DO_TOKEN, so my command looks like this:
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.
From here, you should be able to ssh into the VM:
You should find yourself on a shell inside your VM. It will look something like:
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 in depth tutorial 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.
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 base images that we can build on. Since my personal site uses Ruby version 2.3.3, I picked the
phusion/passenger-ruby23 base image.
We can pull in a base image by including it in our Dockerfile.
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.
# 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/default
ADD app.conf /etc/nginx/sites-enabled/app.conf
Don’t worry if you aren’t sure where this
app.conf file came from. We haven’t created it yet. We’ll be doing that in the next section.
These next lines warrant some explaination:
COPY --chown=app:app . .
RUN bundle install
The first line is telling Docker to create the directory
/home/app/ and move to it. It’s the same as running:
RUN mkdir /home/app/
RUN cd /home/app/
Why house our app inside
/home/app? As per the passenger-docker README:
The [Passenger base] image has an
appuser with UID 9999 and home directory
/home/app. 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.
Your application should be placed inside /home/app.
Note: when copying your application, make sure to set the ownership of the application directory to
COPY --chown=app:app /local/path/of/your/app /home/app/webapp
The Passenger base image is configured to run under the
/home/app directory as the
app user. This is why we have the line:
COPY --chown=app:app . .
It copies our app files over to the Docker container while simultaneously giving the
app user ownership of our app directory. We then run
bundle install to install the gem dependencies from our
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
as that is the name of my app):
3. Configure Nginx with
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
app.conf doesn’t need to be complex.
Mine looks like this:
You don’t have to name this file
app.conf. I just do it for simplicity.
At the bottom of the
server block, we pass in some Passenger data. The
passenger_user field is telling Nginx the name of user that will be running the app on the VM.
We know from the last section that this user is
app. We also know that the location of the application directory is inside
/home/app. That’s why I pointed the
root value at
personalSite is the name of my app and
public is my static files folder.
passenger_ruby 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
Passenger comes with four main version of Ruby to choose from:
Choose the one best suited for your application.
app.conf file included, your project directory will look something like this:
$ tree .
| └── removed for brevity...
| └── removed for brevity...
| └── removed for brevity...
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
# This will build and run your docker container
docker build -t . && docker run -p 80:80
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
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
If you want to be able to do the same, add a simple
docker-compose.yml file to your project repo.
This file is mapping port
80 on your VM to port
80 of your Docker container. Hence the
80:80. What this means in layman terms is that anyone who visits your Droplet on port
80 will be directed to the app on your Docker Container.
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.
build 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
docker-compose.yml is sufficient for a simple Sinatra app. If you require a more complicated setup, such as attaching a database, I would suggest this tutorial from Jan David.
With this file in place, you should be able to run your app using
docker-compose up. If you want to run the app in the background so that it doesn’t take up your terminal, just add the
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 dot.tk for free.
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 Networking tab on the DigitalOcean dashboard and add the domain to your account.
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.
I would also recommend adding
@ 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