Server side SWIFT: Vapor, MySQL, and NGINX with SSL-cert running on Ubuntu instance from DigitalOcean

Todd Shifflett
14 min readApr 11, 2017

--

INTRODUCTION

This is an effort to bring together many of the useful resources that helped me get my Swift Vapor server up and running on a machine instance from DigitalOcean with https and MySQL using the NGINX webserver.

I’m going to set this up with 2 domains ( yourdomain.com and beta.yourdomain.com). One being the primary production App and the other serving a beta version so that you can test your development on a live site, setup exactly as your production site.

If you don’t want to set up both repositories you can ignore the stuff about beta but you’ll still have to conform to the production name structure for file paths in order for the provided scripts to work.

References

I rely heavily on Robert Bojor’s guide for setting up Perfect 2.0, Swift, Ubuntu and Digital Ocean. Other sources I attempt to link to where appropriate.

Also, before you invest the time required with the process I outline here you may want to check out Flock. It may be a much better and more well supported tool for your needs. Though I’m not sure if it deals with all the services I try to address here.

Versions of Packages used in this tutorial

  • Swift : 3.1
  • MySQL : 14.14
  • Vapor : 1.5
  • Nginx : 1.10.0
  • Ubuntu: 16.04.2 x64

Disclaimer

I believe I have fully tested the process after my edits, but it’s possible I missed something. There are also a couple areas I’d like to expand upon. I suspect people will have suggestions and possibly find bugs in my documentation. I will make every effort to incorporate public response.

Create Server instance on DigitalOcean

DigitalOcean is a great cloud computing platform for spinning up an instance of your internet accessible application. If you don’t already have an account you can signup here.

  1. Log in to your DigitalOcean account and, from the Droplets menu tab select Create Droplet
  2. Distribution: select Ubuntu 16.04.2 x64
  3. Size: Probably select the smallest one ( 512 MB/1 CPU — 20 GB SSD disk — 1000 GB transfer). You’ll be able to change this later after you’re up and running.
  4. We don’t need Block Storage for a basic setup.
  5. Region: Select an option close to you.
  6. No additional options.
  7. SSH Key: Select an SSH key you’ve previously set up on DigitalOcean. If you haven’t done that yet, go ahead and do that now. You can use their tutorial for reference. This may not be entirely required, but this tutorial assumes you have that setup.
  8. You’ll create 1 Droplet and you can use the default hostname it provides you.
  9. Click on Create

This should create something like what you see below…

Configure DNS

We’ll need to do this for the last step of getting SSL/HTTPS to work. Hopefully if you start the process now the DNS will refresh by the time you need to reference your domain name.

If you do not need or want to use SSL/HTTPS then you can skip this and always use the IP Address provided by DigitalOcean wherever I reference YOUR_DOMAIN.COM.

In order for your domain name to correctly point to this new IP Address. You first need to visit your domain name registrar and change the Name Servers to reference:

  • NS1.DIGITALOCEAN.COM
  • NS2.DIGITALOCEAN.COM
  • NS3.DIGITALOCEAN.COM

Follow DigitalOcean’s instructions on adding your domain name(s) to the Droplet. This is done under the Networking menu item in your DigitalOcean control panel.

Remember to add an A record for both @ and beta which are directed to your ubuntu droplet you just setup.

Once this is done you may need to request your domain registrar to refresh DNS. It may take some time for the changes to take effect.

Configure the remote server

If your DNS has refreshed and you can ping yourdomain.com then you should be able to ssh into your DigitalOcean remote server. I have stored my DigitalOcean RSA key in the file: ~/.ssh/id_rsa_digitalOcean you may have it saved as something different.

ssh -i ~/.ssh/id_rsa_digitalOcean root@YOUR_DOMAIN.COM -v

If the DNS has not yet refreshed you can still proceed by using the IP Address provided by DigitalOcean for your Droplet. Change YOUR_DROPLET_IP to use the one for your own instance.

ssh -i ~/.ssh/id_rsa_digitalOcean root@YOUR_DROPLET_IP -v

The first time you login you will get a prompt questioning the authenticity of the host. This is okay, answer yes when it asks you if you are sure you want to continue connecting.

We’re going to start by creating a new user on your remote server and setting that user up with sudo privileges. This will allow you to run the NGINX server from this user’s home directory and bind the service to the correct port mappings.

Adjust USER_NAME to the name you want to use for administering your remote server.

useradd -d /home/USER_NAME -m -s /bin/bash USER_NAME  
echo "USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

Next, we’ll set this User up to use the same ssh keys that were set up when the droplet was created.

mkdir /home/USER_NAME/.ssh  
cp /root/.ssh/authorized_keys /home/USER_NAME/.ssh
chown -R USER_NAME.USER_NAME /home/USER_NAME/.ssh

Now logout from your remote server and try to log back in as the new user you just created. Adjusting LOCAL_KEY_PATH USERNAME and YOUR_DROPLET_IP accordingly.

ssh -i LOCAL_KEY_PATH USER_NAME@YOUR_DROPLET_IP -v

I’m going to suggest adding this to the User’s ~/.bash_profile because I find it helpful. It’s not necessary though.

Edit ~/.bash_profile (I use vi but feel free to use whichever editor you’re comfortable with.

Now source that file so the changes take effect.

source ~/.bash_profile

Update packages installed by DigitalOcean

export LC_ALL="en_US.UTF-8"  
export LC_CTYPE="en_US.UTF-8"
sudo dpkg-reconfigure locales

That will bring up a configuration page. You should be able to hit return through these next two pages…

Now update and upgrade…

sudo apt-get update  
sudo apt-get upgrade -y

Next, install some of the packages required by Swift in order to compile, along with some other helpful utilities.

sudo apt-get install make clang libicu-dev pkg-config libssl-dev libsasl2-dev libcurl4-openssl-dev uuid-dev git curl wget unzip -y

Next we need to install Swift. First check https://swift.org/download to make sure you know which is the latest version.

cd /usr/src
sudo wget https://swift.org/builds/swift-3.1-release/ubuntu1604/swift-3.1-RELEASE/swift-3.1-RELEASE-ubuntu16.04.tar.gz
sudo gunzip < swift-3.1-RELEASE-ubuntu16.04.tar.gz | sudo tar -C / -xv --strip-components 1

Let’s make sure that worked…

swift --version

That should return something like: Swift version 3.1 (swift-3.1-RELEASE)

If Swift has successfully installed then you can remove the tar.gz file:

sudo rm -f swift-3.1-RELEASE-ubuntu16.04.tar.gz

And before we get too far let’s just change back to our $HOME folder.

cd ~

ADD SWAP

I’ve had problems compiling release builds on DigtalOcean and adding a swap partition seems to have solved that issue.

DigitalOcean provides reference for this.

Allocate 4 Gigabytes for Swap

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile

Should return something like:

Setting up swapspace version 1, size = 4194300 KiB
no label, UUID=e2f1e9cf-c0a9-4ed4-b8ab-714b8a7d6944

Enable the Swap partition

sudo swapon /swapfile

Check to make sure this worked:

sudo swapon -s

You should get something like:

Filename                Type        Size    Used    Priority
/swapfile file 4194300 0 -1

Now Make the Swap partition permanent

sudo vi /etc/fstab

At the bottom of the file, you need to add a line that will tell the operating system to automatically use the file you created:

/swapfile   none    swap    sw    0   0

SETUP MySQL

This How-To will set up MySQL to host the database on the same server as our app. If you are going to use a database provider as a service you can omit mysql-server from the following line.

sudo apt-get install mysql-client libmysqlclient-dev mysql-server -y

That should bring up a prompt to create a password for the root MySQL user. Create and remember this password.

Next, edit mysqlclient.pc located in /usr/lib/x86_64-linux-gnu/pkgconfig

sudo vi /usr/lib/x86_64-linux-gnu/pkgconfig/mysqlclient.pc

You’ll remove the flags: -fabi-version=2 and -fno-omit-frame-pointer so that the final file looks like:

Now let’s secure the MySQL server installation…

sudo mysql_secure_installation

This will take you through a few prompts…

  1. enter root password. [Enter the password you recently created from the previous install step]
  2. Validate Password Plugin? This is probably a good idea. but not needed for this tutorial. [y]
  3. Change Root Password? We just created the root password so we probably don’t need to change it. [n]
  4. Remove Anonymous Users? [y]
  5. Disallow root login remotely? [y]
  6. Remove test database and access to it? [y]
  7. Reload privilege tables now? [y]

Now you should be able to check your setup.

mysql --version

That should give you something like: mysql Ver 14.14 Distrib 5.7.17, for Linux (x86_64) using EditLine wrapper

Next, lets add a new User to control MySQL so you don’t have to login as the root user. You can reference DigitalOcean’s tutorial on this if you like.

mysql -u root -p

Enter the password when prompted. That should bring up the MySQL control shell with a mysql> prompt. Make sure to change NEW_USER and USER_PASSWORD to suit your needs.

CREATE USER 'NEW_USER'@'localhost' IDENTIFIED BY 'USER_PASSWORD';

Now we need to grant that user with permissions to work with the databases. I’m going to grant that user with total access. Ultimately it may be best to change these for security reasons. I’m not even remotely an expert with security.

GRANT ALL PRIVILEGES ON * . * TO 'NEW_USER'@'localhost';

Now reload the privileges.

FLUSH PRIVILEGES;

Use ctrl-d to exit the MySQL shell.

CONFIGURE FOR VAPOR

Now we’re going to get the tools needed to insure your ubuntu server is up and running with Vapor. You can reference the Vapor Docs.

You should be able to verify that everything Vapor needs up to this point is configured correctly with:

curl -sL check.vapor.sh | bash

That should return: Compatible

Now we can install the Vapor Toolbox. I don’t think this is a required step for the server side, but I’m doing it just in case.

eval "$(curl -sL https://apt.vapor.sh)"
sudo apt-get install vapor

You can verify it worked with:

vapor --help 

It should return: Usage: vapor <new|build|run|fetch|clean|test|xcode|version|self|heroku>

PREPARE USER’s HOME DIRECTORIES and GIT REPOSITORIES

You may want to customize some of these steps and locations to suit your own workflow. My process may not be considered “best practice”, and if you have suggestions on a better workflow I would very much like to incorporate it into this document.

I am going to set up directories which will be used to push our project’s git repositories, and some which will be used as our web-root for the NGINX server.

First the location for the git repositories; One for the beta repository and one for the production repository.

Insure that you are in your User’s home directory. ( cd ~ or just cd )

The APP_NAME should be the name you’d like to use as your web or api app. In my example I’ll just call mine “myWebApp” but this can be whatever you want.

We create an empty Public repository now because eventually that’s what Vapor will look for as the public webroot, but we also need it for the step of creating the SSL certificates.

mkdir -p src/APP_NAME/beta/.git src/APP_NAME/production/.git appServer/APP_NAME/production/bin appServer/APP_NAME/production/Public appServer/APP_NAME/beta/bin appServer/APP_NAME/beta/Public

git init ~/src/APP_NAME/beta/.git/. --bare
git init ~/src/APP_NAME/production/.git/. --bare

We’re going to create a post-receive hook for the git repositories. We don’t need the .sample hooks, so I’ll remove them but feel free to keep them if you want.

rm ~/src/APP_NAME/*/.git/hooks/*.sample

First we’ll set up the beta hook.

cd ~/src/APP_NAME/beta/.git/hooks

Using vi or your prefered editor create a post-receive file which contains everything to build and move it over to the path NGINX will look for when serving the app. You should not need to change the values in this file unless you have changed the paths from what I’ve mentioned above.

Make this post-receive hook executable.

chmod +x post-receive

Next let’s copy that post-receive hook over to the production repository.

cp ~/src/APP_NAME/beta/.git/hooks/post-receive ~/src/APP_NAME/production/.git/hooks/post-receive

CONFIGURE SUPERVISOR

supervisor is a process that will run on the remote server and insure that your Swift App stays up and running.

sudo apt-get install supervisor -y  
service supervisor restart

Now we need to setup the init scripts for your beta and production apps. Since we’re setting up for both a beta and production environment we’ll have a script for each of these.

cd /etc/supervisor/conf.d

Create a new file (you’ll need to use sudo) called APP_NAME.conf and put the following lines in…

Now lets create another init script for the beta app. We’ll call this APP_NAME-beta.conf

Next, make supervisor aware of these new init scripts

sudo supervisorctl reread

CONFIGURE NGINX

On the remote DigtalOcean server instance… If everything has been set up correctly, you should be able to login by typing…

ssh REMOTE_USER_NAME@YOUR_DOMAIN.COM

Now…

sudo apt-get install nginx -y

After it has installed we need to set up the virtual hosts for the beta and production sites. We’ll also remove the default nginx site.

cd /etc/nginx/sites-available
sudo rm -rf default ../sites-enabled/default

Now, using your editor of choice (and sudo) create APP_NAME.vhost which should contain…

Next, copy this file to APP_NAME-beta.vhost and edit that; inside change the word production to beta and, on ONLY lines 5 and 6 (the logs), where it says APP_NAME change that to APP_NAME-beta. I would have thought that we would need to change line 26 for the beta vhost to point at http://127.0.1:8181 because that’s the port the beta app is bound to, but it seems not to make a difference.

Let nginx know that these virtual hosts should be enabled…

sudo ln -s /etc/nginx/sites-available/APP_NAME.vhost /etc/nginx/sites-enabled/APP_NAME.vhostsudo ln -s /etc/nginx/sites-available/APP_NAME-beta.vhost /etc/nginx/sites-enabled/APP_NAME-beta.vhost

Next, edit the nginx.conf file located in /etc/nginx

cd /etc/nginx
sudo mv nginx.conf nginx.conf.backup

Now (using sudo) create a new nginx.conf file:

Next, restart the nginx server…

sudo /etc/init.d/nginx restart

ENABLE SSL/HTTPS

The https protocol is an extra layer of security for internet traffic. I won’t try to describe it any farther than that. It does become extra important if your webApp is serving as an API for iOS applications. It’s my understanding Apple has begun to enforce that calls made from your app will need to connect via https and that regular old http will no longer pass App Store requirements.

I’ll be referencing instructions for Securing Nginx with Let’s Encrypt from DigitalOcean.

Let’s go back to the User’s home directory to start: cd

This will Clone the Let’s Encrypt repository into /opt

sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

We’re going to request a certificate for both the beta and production sites. We’ll start with the production. YOUR_DOMAIN.COM should be whatever you called it way back in the beginning when you were registering the A records with DigitalOcean.

cd /opt/letsencrypt
./letsencrypt-auto certonly -a webroot --webroot-path=/home/USER_NAME/appServer/APP_NAME/production/Public -d YOUR_DOMAIN.COM

Note: The Let’s Encrypt software requires superuser privileges (sudo).

Once letsencrypt initializes you will be prompted for some information.

When prompted, enter an email address to be used for notices and lost key recovery:

You must agree to the Subscriber Agreement…

If everything has worked then you should get a message:

Output:
IMPORTANT NOTES:
- If you lose your account credentials, you can recover through
e-mails sent to USER_NAME@MAIL_DOMAIN.com
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/BETA.YOUR_DOMAIN.COM/fullchain.pem. Your
cert will expire on 2016-03-15. To obtain a new version of the
certificate in the future, simply run Let's Encrypt again.
- Your account credentials have been saved in your Let's Encrypt
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Let's
Encrypt so making regular backups of this folder is ideal.
- If like Let's Encrypt, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

Make note of the certificate path: /etc/letsencrypt/live/BETA.YOUR_DOMAIN.COM/fullchain.pem and the expiration date.

Now repeat the process for the beta site…

./letsencrypt-auto certonly -a webroot --webroot-path=/home/USER_NAME/appServer/APP_NAME/beta/Public -d BETA.YOUR_DOMAIN.COM

Insure that the certificates exist; you can check by listing their location:

sudo ls -aF /etc/letsencrypt/live

That should return something like:

./    ../    BETA.YOUR_DOMAIN.COM/    YOUR_DOMAIN.COM/ 

We now need to re-edit the nginx virtual host config and uncomment the lines about the certificate files.

cd /etc/nginx/sites-available
sudo vi YOUR_DOMAIN.vhost

It should now look like this…

Do the same for BETA.YOUR_DOMAIN.vhost

Now restart nignx:

sudo /etc/init.d/nginx restart

TK setup cron to regenerate the SSL cert?

LOCAL DEVELOPMENT

Okay, now let’s jump back to your local machine for a bit to setup the app for development if you don’t already have one.

These instructions assume you are working on a mac and already have Xcode, Swift, as well as Vapor up and working. If you need to install the Vapor toolbox please refer to the Vapor Docs.

In the terminal app navigate to where you keep your project files. I’ll just use the desktop for this demonstration. Be sure to adjust the APP_NAME

cd ~/Desktop
vapor new APP_NAME
cd APP_NAME

Next, add the mysql-provider to your Package dependencies. Your Package.swift file should at least contain…

I use Xcode, so I’ll have vapor set up the .xcodeproj file…

vapor xcode --mysql

Now the APP_NAME folder should contain…

APP_NAME/
.build/
.git/
.gitignore
.travis.yml
Config/
Localization/
Package.pins
Package.swift
Procfile
Public/
README.md
Resources/
Sources/
app.json
APP_NAME.xcodeproj/
license

My .gitignore file looks like this:

.DS_Store
.build
Packages
*.xcodeproj
xcuserdata
secrets
mysql.json

If you are using a secrets folder to store Config files for things like MySQL you will have to copy/create those files on the remote server before your app will function properly.

You’ll likely have a Config folder which looks like:

Config/
app.json
clients.json
crypto.json
droplet.json
servers.json
beta/
mysql.json
production/
mysql.json
secrets/
mysql.json

You should make sure to use a different database name between the production and beta environments. For Example the Config/production/mysql.json should look like:

{
"host": "127.0.0.1",
"user": "MYSQL_USER_NAME",
"password": "MYSQL_PASSWORD",
"database": "APP_DATABASE"
}

and the Config/beta/mysql.json:

{
"host": "127.0.0.1",
"user": "MYSQL_USER_NAME",
"password": "MYSQL_PASSWORD",
"database": "APP_DATABASE_beta"
}

In order to have the beta and production environments running at the same time you need to make sure each app is bound to a different port. To accomplish this we add a configuration to override the servers.json Config, and change the port for the beta. Add a custom Config/beta/servers.json file.

{
"default": {
"port": "$PORT:8181",
"host": "0.0.0.0",
"securityLayer": "none"
}
}

TK also need to somehow configure the app to protect against public access if we’re running the beta

Adding the remote repositories. By now, hopefully, your DNS has refreshed and you are able to ping YOUR_DOMAIN.COM

Let’s make sure you’ll be able to push to the remote server. We can do that by letting ssh know that when you access that address you want to use a specific key. You do this by editing ~/.ssh/config

Add these lines (Words in ALL_CAPS need to be adjusted for your setup)

Host YOUR_DOMAIN.COM
Hostname YOUR_DOMAIN.COM
user REMOTE_USER_NAME
IdentityFile=~/.ssh/YOUR_DIGITAL_OCEAN_ID_RSA_FILE

Next, insure you are still in your APP_NAME folder and add the remote git repositories…

git remote add beta ssh://REMOTE_USER_NAME@YOUR_DOMAIN.COM/home/REMOTE_USER_NAME/src/APP_NAME/beta/.gitgit remote add production ssh://REMOTE_USER_NAME@YOUR_DOMAIN.COM/home/REMOTE_USER_NAME/src/APP_NAME/production/.git

Now you have a beta and production repository. You can push out to the beta when you want to test your app in the real world. And push out to production when you want it live.

Let’s push out the beta to make sure everything is working.

git add .
git commit -m 'First commit to test pushing to beta server`
git push beta master

At this point the files should be transferred to the remote beta repository and the post-receive hook should run. The project will compile and you’ll see the output from post-receive ending with remote: Restarting APP_NAME-beta

You should now be able to go to https://YOUR_DOMAIN.COM and connect with your app.

--

--