Deploying to Digital Ocean via GitHub Actions and SSH

GitHub Actions are a great tool for automating builds, tests and deployments in a composable and flexible way. The ServiceStack x tool provides mix templates to incorporate into your existing applications and repositories that can speed up different types of workflows.

We've created a mix template for building and deploying your ServiceStack app with GitHub Actions, GitHub Container Repository and Docker Compose all via SSH for a minimalist server hosting setup.

Specifically, we'll be using x mix build release-ghr-vanilla which has GitHub actions configured ready to deploy your ServiceStack application when a new GitHub release is created. This can be run at the root of your local repository folder, for example if you wanted to create an empty web application you would run:

git clone git@github.com:<your user or org>/DropletApp.git
cd DropletApp
x new web
x mix build release-ghr-vanilla
# 'y' to process with writing files from x mix.
git add -A
git commit -m "New ServiceStack project"
git push

Pushing your new application to GitHub, the build.yml will run a dotnet build and dotnet test within the CI environment. For deployments, we want to get a server setup for hosting the new application.

INFO

x mix release-* are designed to be used with ServiceStack applications that were created with most x new project templates that follow the ServiceStack recommended project structure. They are designed to be a starting point that you can edit once created to suit your needs.

Digital Ocean Droplets Host

In this tutorial, we'll be using a Digital Ocean Droplet as the target server and walk through the steps required to setup this automated deployment process for your ServiceStack application.

Droplet Setup

Since we are deploying a simple ServiceStack application, we are going to use the cheapest Basic Droplet at $5USD/month. This is plenty to host even multiple low traffic ServiceStack applications but your use case might have different hardware requirements.

Create your new droplet

The basic droplet we are going to create will have the following configuration.

  • Distribution - Ubuntu 20.04 LTS
  • Plan - Basic
  • $5/month
  • Datacenter region - Which ever suits your use case
  • VPC - default
  • Authentication - SSH keys, create a new one just for this server, follow the instructions on the right hand panel.

The rest of the options, leave as default.

Create your new SSH key

If you ended up using an existing SSH key, now would be the time to create one specifically for deploying applications to this server, and only that function.

The reason this is important because we will be using the private key within our GitHub Actions, which means the private key generated will be leaving your local computer and stored within GitHub Secrets. In the event that this key is compromised, we want to limit its use.

Digital Ocean has some excellent documentation for this process here.

Enable floating IP

Once your Droplet has started, you'll want to Enable Floating IP so that we have a static public IP address to route to for a domain/subdomain.

This can be done via

  • Manage
  • Droplets
  • Select your droplet
  • click Enable Floating IP at the top right.

Enable Floating IP option

Docker setup

Now that our Droplet is running and has a public IP address, we'll want to install Docker and docker compose.

SSH into your Droplet using the appropriate SSH key and your preferred SSH client (straight ssh, Putty for Windows, etc).

Eg, with a Linux ssh client, the command would be ssh root@<your_IP_or_domain> as root is the default user setup when creating an Ubuntu droplet.

INFO

the user may change depending on how your server is setup. See man ssh for more details/options.

Install docker and docker compose

Installing Docker for Ubuntu 20.04 can be done via the repository with some setup or via Docker provided convenience scripts. For a more detailed walk through, DigitalOcean have a good write up here. Scripted included below for ease of use.

Docker via convenience script

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

INFO

These scripts required sudo privileges, see Docker notes regarding security. Full repository based script available here. Docker is installed remoting under root in this example for simplification. Information of Docker security can be found in the Docker docs

Docker compose install

Run docker compose version to confirm.

INFO

Ensure you have v2+ of Docker Compose A compatibility script can be used for docker-compose via the following script. echo 'docker compose --compatibility "$@"' > /usr/local/bin/docker-compose sudo chmod +x /bin/docker-compose

Get nginx reverse proxy and letsencrypt companion running

Now we have Docker and Docker Compose installed on our new Droplet, we want to setup an nginx reverse proxy running in Docker. This will handle mapping requests to specific domain/subdomain requests to specific docker applications that have matching configuration as well as TLS registration via LetEncrypt. When a new docker container starts up and joins the bridge network, the nginx and letsencrypt companion detect the new application and look to see if routing and TLS certificate is needed.

In the x mix release-ghr-vanilla template, we include deploy/nginx-proxy-compose.yml file which can be copied to the droplet and run.

Here is the nginx Docker Compose file in full.

version: '2'

services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    network_mode: bridge

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion:2.0
    container_name: nginx-proxy-le
    restart: always
    environment:
      - DEFAULT_EMAIL=you@example.com
    volumes_from:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    network_mode: bridge

volumes:
  conf:
  vhost:
  html:
  dhparam:
  certs:
  acme:

You can use scp or shared clipboard to copy the short YML file over. For this example, we are going to copy it straight to the ~/ (home) directory.

scp -i <path to private ssh key> ./nginx-proxy-compose.yml root@<server_floating_ip>:~/nginx-proxy-compose.yml

Once copied, we can use docker compose to bring up the nginx reverse proxy.

docker compose -f ~/nginx-proxy-compose.yml up -d

To confirm these are running, you can run docker ps so have a look at what containers are running on your server.

Domain setup

Now our droplet server is all setup to host our docker applications, we want to make referring to our server easier by setting up a DNS record.

Specifically, we want to create an A record pointing to our Floating IP of our Droplet server.

INFO

You will need to use your DNS provider service to manage the DNS records of your domains.

GitHub Repository Setup

With the Droplet server all setup, first we'll need an application to deploy!

Create a new repository on GitHub. This setup will work with public or private repositories, select your options and clone it to your local machine.

git clone <GitHub URL>
cd <project name>

We are going to use x new web as a command to create a blank ServiceStack application. Run this in your newly cloned repository folder, the project name will be derived from the repository directory name.

INFO

If you create the project in a new directory beforehand and want to name it, use x new web <project name>.

The x new command gives us a working ServiceStack project from a template, x mix allows us to add additional templated files that work with templated ServiceStack projects.

Now our project is created, you can mix in our GitHub Action templates in the local repository folder by running:

x mix build release-ghr-vanilla

The build mix template provides a GitHub Action that builds and tests our dotnet project. The release-ghr-vanilla provides a GitHub Action that uses Docker to package the application, pushes the Docker image to GitHub Container Registry (ghcr.io) and deploys the application via SSH + docker compose to our new Droplet.

Just like other x mix templates ServiceStack provides, these are a starting point to help get things running quickly with known patterns. Unlike external dependencies, they just copy the templated code that is editable and not tied to any code generation service that will update these files.

Files provided by the release-ghr-vanilla are:

  • .github/workflows/release.yml - Release GitHub Action Workflow
  • deploy/docker-compose-template.yml - Templated docker compose file used by the application
  • deploy/nginx-proxy-compose.yml - File provided to get nginx reserve proxy setup as used by steps above.
  • Dockerfile - Self contained Docker that builds, publishes and hosts your application.

Make sure GitHub Enable improved container support is turned on

The account or organization of your repository at the time of writing needs to "Enable improved container support".

INFO

This step may no longer be required once Improved Container Support is generally available.

Goto:

  • Account
  • Settings
  • Packages
  • Improved container support
  • select Enable improved container support
  • Save.

INFO

See GitHub Docs for details.

Once these steps are done, our GitHub Actions will be able to push Docker images to GitHub Container Registry.

Full Steps

Create new repository in GitHub first.

git clone git@github.com:<your user or org>/WebApp.git # Where "WebApp" is the name of your repository
cd WebApp
x new web
x mix build release-ghr-vanilla
git add -A
git commit -m "Add new ServiceStack project with GitHub Action files"
git push
Steps overview
  • Create a new GitHub repository
  • Clone new repository locally
  • Change directory to new repository
  • Locally create ServiceStack project using x new.
  • Mix in GitHub Actions using x mix
  • Commit and push changes to GitHub

Create secrets

The x mix templates needs the following information to perform the deployment, this information is added to the GitHub repository as the following secrets.

  • DEPLOY_HOST - hostname used to SSH to, this should be a domain or subdomain with A record pointing to the server's IP adddress.
  • DEPLOY_USERNAME - the username being logged into via SSH. Eg, ubuntu, ec2-user, root etc.
  • DEPLOY_KEY - SSH private key used to remotely access deploy server/app host.
  • LETSENCRYPT_EMAIL - Email address for your TLS certificate generation

These secrets can use the GitHub CLI for ease of creation. Eg, using the GitHub CLI the following can be set.

gh secret set DEPLOY_HOST -b"<DEPLOY_HOST, domain or subdomain for your application and server host.>"
gh secret set DEPLOY_USERNAME -b"<DEPLOY_USERNAME, the username being logged into via SSH. Eg, `ubuntu`, `ec2-user`, `root` etc.>"
gh secret set DEPLOY_KEY < [path to ssh key]
gh secret set LETSENCRYPT_EMAIL -b"<LETSENCRYPT_EMAIL, Email address for your TLS certificate generation, eg me@example.com>"

Repository secrets can be created under Settings->Actions->Secrets.

Tag release

To kick off any new deployment, we use GitHub Releases.

Provide a version number and name, the version will be used to tag the Docker image in GHCR.io. If you are using the GitHub CLI, you can also do this via the command line. For example,

gh release create v1 -t "v1" --notes ""

Go to the Actions tab in your repository to see the progress of your deployment.

INFO

The initial deployment might take upto a minute for LetsEncrypt to generate and use the certificate with your domain. Make sure your DNS is all setup before publishing the Release, otherwise further delays related to DNS TTL will likely occur. If you are having problems with your app hosting, be sure to configure the logs in the nginx and your app docker containers for any startup issues. You can also run in attached mode to watch the output of these containers via docker compose -f ~/nginx-proxy-compose.yml up.

GitHub Container Registry Pricing

If you're already have a Pro or Team plan, you get free allowances to using the GitHub Container Registry. It has the same pricing as the GitHub Packages product which is summarized as the following for Pro or Team.

  • 2GB storage free
  • 10GB data transfer free
  • Additional storage $0.25 per GB
  • Additional transfer out $0.50 per GB (GitHub Actions are free)

With Docker images though, they can get large pretty quickly. While GitHub Container Registry is still in beta, it is free to use but additional storage and transfer costs are something to keep in mind. Hopefully use of retention policies and other features can help manage to keep these prices down.

Wrapping up

Having a CI process from the very start of a project/prototype is something that pays off quickly, even as a solo developer. The release-ghr-vanilla template is designed to help get that process started by providing a "no fuss" pattern for prototyping ideas and keeping costs down while giving a dockerized path forward as your hosting requirements change. GitHub Actions provide a great way to build and maintain your CI process right where your code lives, and even though GitHub Container Repository is in the early stage, we think it provides a simplified workflow that works well for the indie/solo developer as well as teams. We intend to put together more of these templates and patterns for different use cases, feel free to give us feedback and let us know what you'd like to see!