Recently, I have looked how to develop multiple small applications and how to deploy them to the cloud. See the following blog posts
I still could not find a way to actually deploy these small applications onto a single cloud instance. Different deployment services tried to create a separate instance to run each application; that is a huge overhead and costs a lot.
Finally, I found a solution that works for me, and is suitable for small to medium sized applications. If you have used Heroku, the concept would be very familiar to you, and in practice the steps are similar.
I am using Dokku - which is Docker-powered Heroku-like application. You can create a single DigitalOcean "droplet" with Dokku 0.4 with 1-click setup and have your own "Heroku" platform. The Dokku can then create and control multiple applications, each application running inside its own Docker container. If you need additional features, there are plugins for load balancing, common databases, etc.
Here are my steps to start and configure separate apps inside a single Dokku instance hosted on DigitalOcean. Hope these steps are useful, especially if you try to configure virtual hosting for application to avoid addressing apps by their transient port numbers.
Step zero
- Read the official Dokku installation guide - the steps might have changed since I have described them.
- Important make sure you have your public SSH key uploaded to DigitalOcean before creating any new droplets for simple and secure login.
Create droplet
Read the Running Dokku on DigitalOcean instructions
- Pick one-click apps
- Select "Dokku 0.4.14 on Ubuntu"
- Pick at least 1GB size
- Add your SSH key for simplicity and security, instead of using the root password
- Choose internal host name, for example "dokku" instead of the default
- Click "Create" button and wait a few seconds
Check Dokku running in the droplet
- Copy droplet's IP4 address and run
ssh root@<ip4>
to login - Check dokku's installation
1 | root@dokku:~# dokku version |
If the version is old(er), you can upgrade dokku, see instructions
1 | sudo apt-get update |
Configure custom domain name (optional)
If you setup a custom domain, you will be able to use <application name.domain.com>
for
reach your application. Otherwise you will have to use <ip4>:<port>
to reach the individual
applications. Even worse, every time you redeploy
an application X, you will get a new port number: <ip4:port1>
, <ip4:port2>
, <ip4:port3>
which makes connecting to an application difficult.
With a custom domain name and virtual host names, the application stays at the same
name <name-of-the-app.domain.com>
and the port is remapped internally.
WARNING maybe it is possible, but I failed to switch existing an Dokku droplet from using port numbers to the custom domain name; had to rebuild the droplet and create the applications again after configuring the domain. Thus it is recommended that you perform domain / virtual host name configuration first, before creating any apps.
Buy a domain and point DNS record at the droplet
- First, you need a custom domain name for the droplet.
Purchase a domain name
<domain.com>
, for example from domains.google.com - Go into the settings (where you bought the domain) and point DNS servers at the
DigitalOcean's name servers:
ns1.digitalocean.com
,ns2.digitalocean.com
andns3.digitalocean.com
- Verify that the new domain servers are listed in
$ whois <domain.com> | grep "Name Server"
response - From the DO droplet's actions pick "Add Domain" and enter the new domain
<domain.com>
- There should be nothing to do, there is already a default
@ <ip4>
A record created - Add wild card mapping for everything else to the same domain - A record
* <ip4>
Note just enter a wildcard "*" in the first column, and<ip4>
address in the second. - You should see the following text in the record on the config page
1 | domain.com. 1800 IN A <ip4> |
- Test the mapping by browsing to
<domain.com>
- you should see the same screen as browsing to<ip4>
directly - Create CNAME record if you need aliases, I don't think you need them with Dokku
- Test the domain resolution from the command line by using
ping <domain.com>
- Test the SSH works using domain name in addition to IP4
1 | ssh root@<domain.com> |
Set virtualhost
- Open the Dokku application in the browser using the new name
<domain.com>
and enter the new domain name in the input box instead of the<ip4>
address. - Click "Finish setup"
Maybe
In order to properly route each application foo
to foo.domain.com
you might have to set the hostname on the droplet. SSH into the droplet and
check the current domain name, and set it to domain.com
1 | sudo hostname -f |
Install LetsEncrypt
In order to get free automatic SSL certificates for our applications and the new domain, need to install Dokku LetsEncrypt plugin.
1 | sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git |
Create an application
- ssh into the box
- Create new
hello
application usingdokku apps:create hello
- Exit SSH from the droplet and switch back to the local computer
- Since Dokku operates under its own user "dokku" we want to associate it with the user
root
to use our SSH key when push to the remote repo. From the local computer execute the following remote command
1 | cat ~/.ssh/id_rsa.pub | ssh root@<ip4> "sudo sshcommand acl-add dokku root" |
- From the local Git repo (for example I am using bahmutov/hello-world) add Dokku as another remote git server
1 | git remote add dokku dokku@<ip4>:hello |
- push the code to the remote (you should see lots of build messages)
1 | git push dokku master |
You should see build process log messages, similar to the build log generated when pushing an app to Heroku.
Hint: you can push a different local branch to Dokku server. Just map
it to Dokku's "master" like this git push dokku local-branch:master
.
If the build step during the push fails, it might be due to the droplet's small size. Try resizing the drop let to have larger memory limit and push again. For example, I could run small HTTP server on a droplet with 512MB, but ExpressJS required at least 1 GB droplet.
Checking the application's state
- ssh into the box
- see the list of running applications
1 | root@dokku:~# dokku apps |
- check the app's logs
1 | root@dokku:~# dokku logs hello |
- Make direct request to the application running inside the Docker container.
To find the internal IP and port, see the
nginx.conf
file created with the application.
1 | dokku:~# cat /home/dokku/hello-world/nginx.conf |
Use the above "server" IP to check the web application
1 | dokku:~# curl 172.17.0.4:5000 |
If you have configured custom domain and virtual host name resolution, skip the IP4 port step below.
- Find the actual port on the droplet mapped to the application (running inside Docker).
1 | root@dokku:~# dokku urls hello |
- Check the application's response by running
curl
or whatever is your preferred HTTP client
1 | $ http <ip4>:32769 // or http://<hello.domain.com> |
Our Hello World is running at that full address
<ip4>:32769
(or at hello.domain.com
).
Setting up a database container (service)
If you need a database, for example MongoDB, you can easily create it to run in a separate container on Dokku.
- SSH into Dokku box and install Mongo plugin
dokku plugin:install https://github.com/dokku/dokku-mongo.git mongo
- Then create a new mongo service, for example name it "my-db"
dokku mongo:create my-db
- the command will show full Mongo URI
- You can always list created mongo service with
dokku mongo:list
and get details about a particular one withdokku mongo:info my-db
Let us say we want to use this MongoDB from a Parse server
Setting up Parse server
From your local machine:
- Clone the parse example to local folder
- Install dependencies
npm i -r http://registry.npmjs.org/
just to test the server locally - SSH into Dokku box
- Create
parse
application on Dokku hostdokku apps:create parse
- Link the mongo service to the new app
dokku mongo:link my-db parse
- Note: you can see other environment variables in the index.js file
- Add remote Dokku as another git server
git remote add dokku dokku@<custom domain>:parse
and push the codegit push dokku master
- SSH into the box and set app id and master key environment variables
1 | dokku config:set parse APP_ID=<app id> |
Hint: you can set multiple variables at once using
1 | dokku config:set <app> key1=value1 key2=value2 |
Try making a GET request
1 | curl -X GET -H "X-Parse-Application-Id: <app>" -H "X-Parse-Master-Key: <key>" -G \ |
Setting up SSL for an application
We have already installed LetsEncrypt plugin. Now we can use it to enable
SSL for our hello.domain.com
application.
1 | dokku config:set --no-restart hello DOKKU_LETSENCRYPT_EMAIL=<[email protected]> |
While we are at the Dokku box, print the two urls for "hello" app
1 | dokku urls hello |
Let us try regular request, and should get a redirect to the secure end point.
1 | $ http http://hello.domain.com |
The secure endpoint works
1 | $ http https://hello.domain.com |
Any modern browser is also very happy loading this secure page.
Note the free certificates expire after N months. Use the plugin's commands to setup a cron job to auto-renew them.
Note if you try to get a second certificate for a different application, it might fail. Check if the domain name is displayed correctly, sometimes it thinks the domain is still the first one*
For example, this will work
1 | dokku letsencrypt hello |
But when you try to get a certificate for second application "bye"
1 | ``` |
In this case try to cleanup "letsencrypt" for every application.
1 | ; do for each application |
Making SSL bulletproof
You can test your new SSL setup using SSL Labs
Just open the browser at https://www.ssllabs.com/ssltest/analyze.html?d=<app name.domain.com>
and wait a minute. Most likely your default setup will get a "C" due to
SSLv3 enabled, which has Poodle vulnerability
Note the commands below could be executed using a single script I placed into bahmutov/secure-dokku.
Disable SSLv3
Let us disable the SSLv3 in the nginx configuration for all applications.
SSH into the Dokku box, the nginx configuration should be in the file
/etc/nginx/conf.d/dokku.conf
. Add the following line:
1 | echo "ssl_protocols TLSv1 TLSv1.1 TLSv1.2;" >> /etc/nginx/conf.d/dokku.conf |
You can confirm that the SSLv3 was disabled from the command line - the following command should return an error
1 | openssl s_client -connect <app.domain.com>:443 -ssl3 |
Boom! SSL Labs should bump the grade from "C" to "A-". If you want to go all the way to "A" you should configure forward secrecy.
Enable Forward Secrecy
To enable this feature and disable weak keys, I tweaked the configuration file following the advice in excellent Strong SSL Security on nginx; here is the result
1 | include /home/dokku/*/nginx.conf; |
The file /etc/nginx/conf.d/dhparams.pem
was generated by me
1 | openssl dhparam -out /etc/nginx/conf.d/dhparams.pem 2048 |
After editing the /etc/nginx/conf.d/dokku.conf
file, restart the service
again
1 | service nginx reload |
If there is a problem, inspect the log file, usually in
/var/log/nginx/error.log
. Most likely there is a spelling error.
After everything said and done, your website should get solid "A"!
Simple persistent storage
If you deploy a new version of the application, all its data inside the Docker container is lost. In order to save the data, and have it available after new deploy, you need to mount an external folder.
See docs for more information.
Let us mount external folder /var/lib/dokku/data/storage/hello
as /tmp/data
inside the application "hello"
1 | mkdir -p /var/lib/dokku/data/storage/hello |
And ... the application "hello" cannot actually write into that folder, see issue 2215. Seems every Dokku container runs a random non-privileged user, thus that user cannot actually write into the folder.
As a temporary workaround until Dokku v7, you can give everyone write access to the folder.
1 | chmod a+w /var/lib/dokku/data/storage/hello |
Since every deploy changes the user, if the new user creates new files
or recreates the existing ones, we need to run this command again.
I scheduled a quick cron job to do this very often; run crontab -e
then enter the following to run the command every two minutes
1 | # m h dom mon dow command |
Hopefully, this would be fixed in a better way soon.
Setting up continuous deployment
We do not want to perform manual deploys, instead I prefer to deploy my applications automatically. For publishing NPM modules I prefer semantic release, similarly for deploys we can use a CI server to push the new code as soon as unit tests pass.
Simple case - Shippable
Let us follow the instructions from
Shippable
and setup the deployment pipeline for hello-world
application.
First, we need to give Shippable access to Dokku so it can deploy applications. Browse to Shippable "CI / Settings" tab and copy the public "Deployment Key". Then from the local terminal add this key to Dokku (consult User Management / Adding Deploy Users). If the SSH key is in the clipboard, we can simply run
1 | $ pbpaste | ssh [email protected] "sudo sshcommand acl-add dokku shippable" |
The last word "shippable" is the name of the new user we have just created for Shippable connections.
Second, enable the build for the project, in this case hello-world
.
A simple shippable.yml
file pushes to the Dokku remote server after a
successful build
1 | language: node_js |
Note the full git repo for the application "hello" is still
[email protected]:hello
- even if we have created user "shippable", it
is only to login via a specific SSH key.
Via Shippable user interface you can restrict the builds to run only for tagged commits, to make sure you only deploy a finished features for example. You can find the full configuration docs here.
Slightly more complex - CircleCI
Similarly, you can configure other CI services like CircleCI, Codeship to push code to Dokku after a successful build. First, we need to generate a new SSH key pair, upload the private key to CircleCI and public to Dokku.
1 | ssh-keygen -q -t rsa -b 2048 -f "/tmp/ci_rsa" -N "" |
This has generated two files in /tmp
folder. Grab the private key and
place it in the clipboard
1 | cat /tmp/ci_rsa | pbcopy |
Hint: even better is to store the generated key pair in your folder for future use
1 | folder=$(pwd) |
Browse to the CircleCI page https://circleci.com/gh/<username>/<project>/edit#ssh
and paste the text into the text box (leave the hostname
blank to allow
reusing this key for any dokku domain name).
Now grab the public key file and pipe it directly to the Dokku ssh command
1 | cat /tmp/ci_rsa.pub | ssh [email protected] "sudo sshcommand acl-add dokku codeship" |
That is it, we should be able to push new code directly from the build
using the command git remote add dokku [email protected]:hello
first.
1 | deployment: |
Beefing up server security
This is a list of advanced steps to make the Dokku box even more secure.
Disable SSH login using passwords
We should only login using SSH key, not passwords.
Edit file /etc/ssh/sshd_config
and insert line
1 | PasswordAuthentication no |
If you are only logging in from specific IP(s) you can restrict the Dokku host to only allow logins from these addresses, see this guide Note this will probably break continuous deployment!
Restart the SSH daemon
1 | service ssh restart |
Login with non-root user
It is a good idea to make a new non-root user just to be able to SSH into the box, and disable SSH login for the root user. See how.
Enable 2FA for SSH login
This is an advanced step to make sure only you can login. Read the Configuring SSH with 2FA and Key based authentication
Conclusion
I love Dokku - it seems mature, feature complete, simple and allows me to spend very little money to run a plenty of small applications. While not as simple as Zeit now, Dokku has a huge advantage - every tool that runs in a Docker container can be deployed in seconds.