Make your own Heroku! Auto-deploy a Docker app on your own server with GitHub Actions

toficofi

Josh Lee

Posted on September 20, 2021

Make your own Heroku! Auto-deploy a Docker app on your own server with GitHub Actions

If you've switched from using PaaS services like Heroku to running off a VPS (or your own machine!) then you might miss features like auto-deploy.

Luckily, with the power of ⚑ GitHub Actions and Docker, it's simple to replicate auto-deploy yourself.

You can find the final example repository below:


🎯 The Goal

We'll be able to git push origin live some changes, and find that your server has automatically pulled the changes, built a new image, and restarted the containers, without lifting a finger.

🧠 What do I need?

This guide assumes a basic understanding of Docker, Git, and bash, and that you have a VPS or another Linux machine already provisioned. The stack for your app doesn't matter here!


πŸš€ Let's get started!

Our app

In this example, we'll be using the default ASP.Net Core webapp template. It doesn't matter what stack you use, as long as it's Dockerized.

$ mkdir auto-deploy-docker
$ cd auto-deploy-docker
$ dotnet new webapp
$ dotnet run dev
Enter fullscreen mode Exit fullscreen mode

(local shell)

That'll get us started with a simple webapp, found at localhost:5000.

Untitled-1


🐳 Docker time

Drop a Dockerfile at the root of the project. We're going to steal the config from Dockerize an ASP.NET Core application as it fits our needs. I've annotated the file with comments.

# syntax=docker/dockerfile:1
# First stage: build the app
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env

# Make and switch to the /app directory
WORKDIR /app

# Copy the C# project file into /app
COPY *.csproj ./

# Restore dependencies
RUN dotnet restore

# Now we can copy the rest of the source into /app
COPY ./ ./

# Build the project in Release mode to the "out" directory
RUN dotnet publish -c Release -o out

# Second stage: run the app
FROM mcr.microsoft.com/dotnet/aspnet:5.0

# Switch back to /app
WORKDIR /app 

# Copy the binaries from the build stage into our new stage
COPY --from=build-env /app/out .

# Set the entrypoint for when the image is started
ENTRYPOINT ["dotnet", "auto-deploy-docker.dll"]
Enter fullscreen mode Exit fullscreen mode

To reduce the size of the image, drop a .dockerignore so we don't copy in the binaries from the context. We don't need these, because we're building the project inside the container.

bin/
obj/
Enter fullscreen mode Exit fullscreen mode

Note: For deployment pipelines, it's generally preferred to build the project outside the container, and use the compiled binaries in the image. We won't worry about that for this guide.

Let's build the image and verify it works locally. Make sure you exit the dotnet run dev you started earlier, so it doesn't hog up port 5000!

# build the image
$ docker build . -t auto-deploy-docker

# run the image, binding the container's 80 to our 5000
$ docker -p 5000:80 run auto-deploy-docker 
Enter fullscreen mode Exit fullscreen mode

(local shell)

Navigate again to localhost:5000 and you should see the same app.

Great! The last thing we'll want to do here is make use of the powerful Docker Compose. If you've not used Compose yet, it's basically a way to define all the containers we need to use for our app with a docker-compose.yml configuration file, which means we can put them all up and tear them all down in one go without faffing around with other commands.

In this example, we're only running a single webapp in a container, but if you need to add other services - like a background worker, or a database container, Compose helps you orchestrate them. On top of that, it lets you configure containers to automatically restart when they go down or the host machine is restarted.

Make a docker-compose.yml and fill it out as so:

version: "3.8"
services:
  auto_deploy_docker: # name of the service
    image: auto-deploy-docker # the image to use
    container_name: auto-deploy-docker # what to label the container for docker ps
    ports:
      - 80:80 # note, we're mapping to port 80 instead of 5000 because we'll use 80 on the VPS
    restart: unless-stopped # restart if failed, until we stop it ourselves
Enter fullscreen mode Exit fullscreen mode

Give it a shot (again, make sure you stop the previous docker run)

# bring everything up (will run in background)
$ docker-compose up -d

# tear everything down
$ docker-compose down
Enter fullscreen mode Exit fullscreen mode

(local shell)

Nice. We've got everything up and running locally. Commit your app to GitHub, and make a live branch that reflects what will be live on our VPS. This is where we'll push changes to have them deployed on the server.


πŸ’» Dropping our app on the VPS

It's time to take things remote. SSH into your VPS (I'm using PuTTY), and make sure you have Git and Docker installed. I'm using a Debian with Docker image from OVH. At the moment, they're doing a nice $3.50 a month VPS.

Start by enabling credential caching. This will save our GitHub username and Personal Access Token. It will, however, save it in plaintext, so make sure you have tight control over your VPS.

$ git config --global credential.helper store
Enter fullscreen mode Exit fullscreen mode

Next, clone your repo.

$ git clone https://github.com/toficofi/auto-deploy-docker.git
$ cd auto-deploy-docker
Enter fullscreen mode Exit fullscreen mode

Git will prompt you to log into GitHub if you haven't already. You can't use your GitHub password here, you'll need to generate a Personal Access Token and use that in place of a password. Limit the scopes for the PAT. We only need repo access on the VPS.

Great! Let's give it a test run.

# build the image on the server
$ docker build . -t auto-deploy-docker
$ docker-compose up
$ git checkout live # switch to our live branch
Enter fullscreen mode Exit fullscreen mode

(remote shell)
Navigate to your server's IP address, and you should see the page up and running. Fantastic!

Next up, we'll write a bash script for automatically pulling changes and deploying them.


πŸ“ The deploy script

Let's recap where we are.

  • We have an ASP.Net Core app, and we've Dockerized it.
  • We've got it on GitHub, and the VPS has the repo pulled.
  • We can run docker-compose up to start our container.

In order to respond to changes to the code, these are the steps we need to take:

  1. Pull down the updated live branch
  2. Build the image (which will also build the project)
  3. Tear down the existing containers
  4. Start up the existing container

We'll wrap these steps up in a little deploy.sh script that will handle this all with a single execution.

Make the deploy.sh script:

echo "Deploying changes..."
# Pull changes from the live branch
git pull

# Build the image with the new changes
docker build . -t auto-deploy-docker

# Shut down the existing containers
docker-compose down

# Start the new containers
docker-compose up -d
echo "Deployed!"
Enter fullscreen mode Exit fullscreen mode

Note: If you're developing on a Windows platform, make sure you change your line endings to use \r (Unix-style) instead of \r\n, or bash on the server won't understand the script.

Make the script executable, and give it a shot!

$ chmod +x deploy.sh
$ deploy.sh
Enter fullscreen mode Exit fullscreen mode

(remote shell)

It might take a couple of minutes, but you'll be able to watch it pull changes, build your image, and start the containers up.

Connect to your VPS and you should see the app!


⚑ Setting up the GitHub Action

We've got our deploy.sh script ready to go. The final piece of the puzzle is to set up a GitHub Action that will automatically invoke this script whenever the live branch has been updated, completing the loop.

Jump onto your repo on GitHub, navigate to the Actions page and hit skip this and set up a workflow yourself.

aa

You'll be presented with a templated Workflow script. It looks a little overwhelming, but it's really just a list of "actions". Each Action is triggered by something, like a push to a branch. It will then execute a sequence of steps in a GitHub-provided container. You can read more about Actions here.

We're just going to use the action to SSH and invoke our deploy.sh script whenever the live branch is pushed to.

Drop this workflow script in:

name: Deploy

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the live branch
  push:
    branches: [ live ]
  pull_request:
    branches: [ live ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  deploy:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      - name: log into VPS and trigger deploy script
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }} # uses secrets stored in the Secrets tab
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          script: sh deploy.sh
Enter fullscreen mode Exit fullscreen mode

This should be pretty self explanatory. Commit the workflow script to the live branch.

We'll need to allow the Action to access our SSH host, username, password and port. Go to Settings > Secrets.

Add secrets for HOST (IP/domain for your server), USERNAME (ssh username), PASSWORD (ssh password) PORT (probably 22).

secrets

NOTE: These secrets are accessible to anyone with Contributor access to the repository, which means they'll be able to log into your VPS. Be careful.

With the secrets added, that should be us ready to go. Let's give it a whirl!

On your local machine, make a change to your app. For example, we'll change Index.cshtml to show "Hello, Auto Deploy!"

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Hello, Auto Deploy!</a>.</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Push your changes to the live branch, and jump onto GitHub to watch the action. You'll be able to see GitHub executing each step for you. Once it's done, navigate to your server IP/domain and some ✨magic✨ has happened!

done


🎁 Wrapping up

Now that you've completed this guide, you'll be able to deploy your changes on your VPS with a simple git push. This will save you a ton of time, and makes a foundation for a CI/CD pipelines.

Here are some next steps you might take:

  • Run some tests as part of your deployment pipeline
  • Build the app using GitHub Actions, and send over the binaries via SCP instead of maintaining a repo on the VPS
  • Send yourself a notification when the deployment has completed with PushBullet CLI

πŸŽ‰ Thanks for reading! You can find me on Twitter here. and on LinkedIn here.

πŸ’– πŸ’ͺ πŸ™… 🚩
toficofi
Josh Lee

Posted on September 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related