Push-to-Deploy with Sail and GitHub Actions

Sail supports deploying WordPress out of the box, without the need of Git or any other source code management tools. This is great for solo-projects, or simple applications with very small teams.

With larger teams and more complex WordPress applications, you’ll want a more robust workflow, including pull requests, code reviews, etc.

GitHub Actions is one of the best CI/CD tools on the market, and personally my favorite. In this short tutorial, I’ll demonstrate how to use Sail with GitHub Actions to quickly deploy your WordPress applications to the DigitalOcean cloud.

Create a new GitHub repository

Let’s start by creating a new repository on GitHub and cloning it to our new project directory.

$ mkdir project && cd project
$ git clone git@github.com:username/repo.git .

Add a .gitignore file in the root of our project, to make sure things, such as uploads, backups or secrets don’t end up in the repository. Here’s the bare minimum I recommend:


Then add the .gitignore file to the repository and commit our changes:

$ git add .gitignore
$ git commit -m "Adding a .gitignore file"

Create a new Sail project

If you’re new to Sail, it’s a fresh CLI tool I wrote to provision and deploy WordPress to DigitalOcean. It takes a couple of minutes to install via Homebrew on Linux, MacOS and Windows.

In our project directory, start a new Sail application with sail init:

$ sail init

This will provision and deploy a WordPress instance to our DigitalOcean account, as well as pull all the application files from the new production server to our local working copy. We can commit these files to our Git repository:

$ git add .
$ git commit -m "Adding a fresh WordPress application"

Encrypt Sail secrets

Sail stores some secrets about your project in a hidden .sail directory, in the root of your project. You should never commit this directory unencrypted to your Git repository, as it contains SSH keys with root access to the production server, and other sensitive information.

However, we’ll need these secrets for GitHub Actions to deploy our Sail project, so we’ll generate a passphrase and encrypt the entire directory with GPG:

$ tar czf - .sail | gpg -c > .sail.tar.gz.gpg

You’ll be prompted to enter a new passphrase twice. Make sure it’s long and secure. We can then commit the encrypted file to the Git repository, and push all our changes to GitHub.

$ git add .sail.tar.gz.gpg
$ git commit -m "Add Sail secrets"
$ git push

We’ll also need to share the passphrase we used with GitHub, to make sure our Workflow can decrypt our file and read the contents. On the GitHub repository page open the Settings tab, select Secrets from the menu on the left, and then hit the New repository secret button in the right hand corner.

Use SAIL_PASSPHRASE as the secret name, and the passphrase you entered earlier as the value.

Create a GitHub Workflow

Create a new .github/workflows/deploy.yml file in your repository:

$ mkdir -p .github/workflows
$ touch .github/workflows/deploy.yml

Open the file in your favorite code editor, and paste the following contents:

name: deploy

    branches: [main]

    runs-on: ubuntu-latest
    - uses: actions/checkout@v2
    - uses: kovshenin/sail-deploy@v1
        passphrase: ${{ secrets.SAIL_PASSPHRASE }}
        encrypted_filename: '.sail.tar.gz.gpg'

The first line is just the name of the workflow, we’ll call it “deploy.”

The next few lines (the “on” section) determine the branches and actions, on which the workflow is going to run. In our case, we’re going to run this workflow whenever something is pushed to the branch called main.

Next is the jobs section, which describes exactly what to do. In our case, we’re going to run some steps, using the ubuntu-latest GitHub image. The steps themselves reference external reusable actions. The first one, actions/checkout clones the Git repository and does a checkout of the main branch.

The second is a custom action I wrote called sail-deploy, which essentially installs Sail, decrypts and extracts the Sail secrets archive using the provided passphrase, and then runs sail deploy.

Add and commit the file to your Git repository, and push the change to GitHub:

$ git add .github/workflows/deploy.yml
$ git commit -m "Adding the deploy workflow"
$ git push


To test your deployment, simply make a change to your repository and push it to your main branch. For example, create a new test.php file:

$ echo "<?php echo time();" > test.php
$ git add test.php
$ git commit -m "Testing deploys"
$ git push

Then go to your GitHub repository page, hit the Actions tab and watch the deploy run. When successful, you should be able to visit the test.php page of your site and get a timestamp.

Now, whenever you push changes to the main branch of your GitHub repository, a GitHub Action will use Sail to deploy those changes to your production environment, in an atomic way.

Of course you can continue to use Sail in your local working directory for ad-hoc deploys, quick rollbacks, domain management, backups and more.

If you have trouble setting this up for your project, or have questions or any other feedback, feel free to leave a comment below, or message me on Twitter and I’ll be happy to help you out. If you’re working on an existing project, don’t forget to check out these Sail migration tips on GitHub.

Happy sailing!

Sail: Deploy WordPress to DigitalOcean

Sail is a free and open source CLI tool to provision and deploy WordPress applications to the DigitalOcean cloud. Here’s a quick video demo of how it works:

I’m a DIY guy when it comes to WordPress hosting, so I like to get my hands dirty with servers, code, configuration and everything else. I’ve been using virtual servers at DigitalOcean for small WordPress projects for a very long time, and it’s great, and also very affordable.

However, it’s a bit annoying to do routine maintenance on existing servers, or provision and configure new servers for newer projects so, like most developers, I wrote a bunch of scripts, and used them for many many years.

Over the last couple months I’ve decided to clean up (rewrite) all those scripts and package them into one easy to use CLI tool, which I called sail. It’s open source on GitHub, and available for Linux, MacOS and Windows through Homebrew and PyPI.

The Competition

Sure, there are plenty of existing products and services for managing WordPress on DigitalOcean and other cloud providers, and trust me, I tried them all. Every single one of them beasts. Here are some of the problems I had:

  • A lot of them don’t provide vanilla WordPress, they bundle stuff from their partners which I don’t want
  • Most of them lack any sort of deployment tools, so I have to set things up on my own
  • Many of them won’t give me root access to the server I’m paying for, and some will not even let me use my own DigitalOcean account to run the VMs
  • Most of them are web GUIs, while I always prefer the command line for such things
  • Some of them charge me double the droplet price for features and services which I’ll never use

Sail is free and open source, and it allows you to:

  • Quickly provision a clean WordPress site to your DigitalOcean account
  • Deploy code changes and rollback in quick atomic operations
  • Add domains and free SSL certificates through Let’s Encrypt
  • Create and restore complete file and database backups
  • Quickly access server logs, production SSH, WP-CLI and MySQL shells
  • With full root access, and all from the command line


While my focus right now is to complete and polish all the core Sail features, I do have some more exciting things on the roadmap for the next few releases. This is not a promise, but rather a taste of what’s coming next:

  • Blueprints, which will allow you to specify additional plugins, themes, settings and server software to launch with your Sail project
  • Staging, and all the pushing/syncing to and from and between
  • Profiling, because every millisecond counts


The easiest way to get Sail is from Homebrew or PyPI. It run on Linux, MacOS and Windows (via WSL). Give it a spin, I think you’ll love it. And if you don’t, let me know why in the comments below.

Rsync’s link-dest: Not Great for Deployments

TIL: rsync’s --link-dest is pretty bad for deploying code to production servers, unless you can get some fancy copy-on-write going on.

Rsync is probably the best utility to transfer large numbers of files from one location to another, quickly and effectively. The --link-dest argument allows you to hard-link files from a different destination if they haven’t changed, saving both time on transfer, as well as disk space.

It’s perfect for backups, and seemed to me like it would be a good idea for code deployments as well. But I was wrong.

Deploying to production means you have a particular copy lying around, that is not unlikely to change especially in single-server setups, where user actions, such as a WordPress core or plugin update, can lead to changes on the filesystem.

So if you change a file that happened to be hard-linked to other releases lying around for a potential emergency rollback, then you’ve effectively just borked them all :(

$ mkdir -p releases/1
$ echo good > releases/1/wp-config.php

$ rsync -rt --link-dest=../1/ releases/1/ releases/2/
$ rsync -rt --link-dest=../2/ releases/2/ releases/3/
$ rsync -rt --link-dest=../3/ releases/3/ releases/4/

$ ln -sfn releases/4 latest
$ echo bad > latest/wp-config.php

# I screwed up, I'm going to roll back to release 1.
# Which I know was good... Right?
$ cat releases/1/wp-config.php

Next best option is --copy-dest just to speed up transfer, but not preserve disk space.

LIVE: Creating a Caching Plugin for WordPress from Scratch

Join me live as I write a page caching plugin for WordPress from scratch. For educational purposes of course, as part of our advanced WordPress training program over at Koddr.io. I’ll be tearing apart some existing caching plugins to find out how they work, then build my own using similar concepts.

I’ll be doing a deep dive into WordPress’ advanced-cache.php drop-in and covering everything you’ll ever need to know about it.

Bring your favorite beverage and join me on YouTube or Twitch.tv.

Upcoming Stream: Creating a Page Caching Plugin for WordPress from Scratch

Have you always wanted to write your own page caching plugin for WordPress? Probably not. In any case, I’ll be doing exactly that, tomorrow at around 9 UTC during a live broadcast on Twitch and YouTube. For educational purposes of course, as part of our WordPress training program at Koddr.io.

I’ll be starting from scratch, with a stock WordPress site. I’ll briefly look at how some of the most popular page caching plugins work to get some ideas. I’ll then write out a list of features for the new plugin, and start coding right away.

It will probably take a few hours to get it into decent shape. I’m setting aside around six hours for the entire stream, but you never know. The end result will be published to a public repository on GitHub under the GPL, for everyone to be able to explore, adapt, extend and transform into the caching plugin of your dreams.

The Twitch and YouTube broadcast links will be published on my Twitter and here on this blog as soon as I go live. See you tommorrow!

Goodbye Automattic

You might have heard that I left Automattic. It’s true. It’s the best company I’ve ever worked at. And by far the longest. Almost 9 years! I’ll miss my friends an coworkers.

I’m not leaving WordPress. In fact, my next impact will be around hosting for WordPress specifically, maybe WooCommerce, I’m not sure yet. It’s going to be a fun challenge. I’ll post when I have news.

Don’t be a stranger.


As you may have heard, Automattic recently secured the rights to operate the sale and registration of .blog — a new top-level domain, which is currently in the Sunrise period, where trademark owners can apply.


The Landrush period, where anyone can apply for their desired .blog domains, is scheduled for November 2nd, and public launch is expected on November 21st. However, a few select bloggers were granted the possibility to get .blog domains sooner as part of the Founders Program, and I was very lucky to be one of them.

Welcome to konstantin.blog — a new home for my archive of almost eight years worth of writing on many topics, including SEO (yeah…), AWS, Twitter, robotics, Linux, PHP, WordCamps and WordPress.

I admit I have neglected this place for a while, haven’t posted as much as I should have, and I can probably come up with plenty of excuses. But this new domain comes with a little string attached — I have to write more frequently, which I intend to do, so watch out for fresh thoughts, ideas, tips and hacks, and a lot of WordPress of course.

If you’re looking for your own .blog domain, head over to get.blog for more information and updates.

WordCamp Moscow 2016 Recap

WordCamp Moscow 2016

WordCamp Moscow 2016 was held this weekend in the amazing Digital October Center. Fourteen speakers from Russia, Ukraine and Lithuania, two tracks with great content on design, programming, blogging, business and of course SEO. Huge props to Dmitry Mayorov for taking on the lead organizer role and making the best out of it.

We had a little over 200 attendees this year, and the event was quite a success. 92% of the survey respondents said that the event was “great” or “good,” and only 8% said it was “okay” or “could be better.” Nobody said it was awful, so that’s a win.

A fair amount of new speakers applied this year, in fact, five of them never spoke at a WordCamp before. The overall survey results (speakers and their talks) were good. Not amazing, but good. We decided to divide up the two tracks by “anticipated popularity” this year, rather than by content, which I think worked out really well, although some attendees complained in the survey.

A small number of attendees didn’t like some of the talks because they were “too basic” or “too vague.” Well yeah, that happens.

The breaks were long, as usual, pizza for lunch, better-than-last-year coffee, fruits and snacks. We even had a lovely press wall this year, with the conference and sponsor logos, which attendees (and photographers) really enjoyed. The sponsors area was much more active this year, with all four of our platinum sponsors having their own table or booth.

The after-party was in a cafe/restaurant in the same building, where luckily this year we were able to negotiate a cheaper selection of beers, juices, waters and wines for our pre-order to fit our budget, so we didn’t run out as quickly as we did last year.

The Talk

Besides being on the WordCamp Moscow organizing team, I was also a speaker. My talk was about memory management in WordPress and why increasing the PHP memory limit is a bad idea. It was targeted at advanced users and developers, though beginner users were also happy to hear they shouldn’t get a more expensive server if their memory consumption averages around 90%.

The slides are available on SlideShare, the video will be up on WordPress.tv around September.

Again, thanks to Dmitry Mayorov, the WordCamp Moscow 2016 organizing team, all the speakers and volunteers, for making such a great event. I really hope that attendees from other cities and countries were inspired enough to create their own WordPress meetup group, and start working towards a WordCamp in their area.

What the Queries

I’ve never been a fan of IDEs, complex debugging tools with breakpoints, variable watch lists and all that fancy stuff. var_dump() and print_r() have always been my best friends.

Recently I was playing around with the caching arguments in WP_Query, trying to combine that with update_meta_cache() while sticking wp_suspend_cache_addition() somewhere there in the middle, and it quickly became a mess, so I wanted to know what queries am I actually running under the hood.

I came up with this little piece, which I think I’ll use more often from now on:

// Assuming SAVEQUERIES in set to true.
$GLOBALS['wpdb']->queries = array();

// All the magic goes here

var_dump( $GLOBALS['wpdb']->queries );

This gives you a nice list of SQL queries that were triggered only by that magic code in between. Works great when you need a quick sanity check on all those caching arguments, priming meta or term caches, splitting queries and whatnot.

Obviously it empties the initial set of queries, so anything in Debug Bar, Query Monitor, etc. will no longer be accurate.

What’s your favorite way to keep track of queries?