Preview deployments with ember-cli-deploy and Github actions

| Simon Ihmig

We recently set up a way to create deployments for our Ember.js app's pull requests, so you can not only review the code diffs, but also preview how the changes look and behave in the browser. But first let me explain the context, as there is obviously no one-size-fits-all approach.

So for a lot of apps we build for startups and medium-sized companies, which are not very large in terms of user volume or traffic (e.g. B2B apps), we mostly choose to deploy them to classic dedicated or shared/virtual LAMP-stack servers, rather than the Cloud-based world like AWS (which we also do when appropriate). This is for diverse reasons, mostly though based on backend and maintenance concerns. So deployment in that context basically boils down to copying files to the server via SSH/SCP/SFTP.

Then on our development side we transitioned over the years from hosting our code on a self-hosted Bitbucket server, accompanied by a Bamboo server for CI/CD, to Bitbucket Cloud and Bitbucket Pipelines and then finally to Github and Github Actions.

So the task at hand here is to set up a system that will automatically create a preview deployment for any pull request in Github using a Github Actions workflow that pushes the build onto our server using an SSH-based Ember CLI Deploy setup. This is how we did it...

Basic setup

We use ember-cli-deploy-simply-ssh as the SSH-based deployment plugin. Its basic setup is quite simple:

// config/deploy.js
module.exports = function (deployTarget) {
  const ENV = {
    build: {},
    'simply-ssh': {
      connection: {
        // parameter hash accepted by SSH2, see for details
        host: process.env.DEPLOY_HOST,
        username: process.env.DEPLOY_USER,
        privateKey: process.env.DEPLOY_SSH_KEY,
      dir: process.env.DEPLOY_PATH,
   // ...
  return ENV;

To trigger the deployment in Github Actions, we set up our workflow to deploy to our staging environment on every push to the develop branch (we follow GitFlow), and to deploy to production for every release tag (vX.Y.Z).

To be honest this is quite limiting, and the biggest complain I have about Github Actions is, that it does not have out-of-the-box deployment features that we were used to coming from Bitbucket, things like manual deployments ("push the button!"), approvals, rollbacks, history of deployments (per environment), a nice UI for all of that, environment-specific settings/secrets etc. I am currently trying to figure out a better way to do this, but this is out of scope for now, and maybe a topic for one of the next blog posts.

A basic deployment workflow based on this could look like this. Note how we don't have hard-coded settings in the deploy.js but pass them via environment variables:

# .github/workflows/deploy.yml
name: CD

      - develop
      - 'v*'

    name: Deploy to staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup node.js
        uses: actions/setup-node@v1
          node-version: 12

      - name: Install dependencies
        uses: bahmutov/npm-install@v1

      - name: Deploy to staging
        run: yarn deploy staging
          DEPLOY_HOST: ""
          DEPLOY_USER: "user"
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY_STAGING }}
          DEPLOY_PATH: "/path/to/vhost/root"
          EMBER_API_HOST: ""

    name: Deploy to production
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
      # except for env vars same as staging...

Deploying pull requests

Now to the actual task of setting up deployments for pull requests. What do we want to achieve here?

Fortunately all of this was possible, and not too hard to achieve actually. But step by step...

Introducing revisions

First how can we deploy something to the same staging environment, without overriding another deployment. As this is what currently is happening: the SSH Ember CLI Deploy plugin will override any existing index.html. Luckily the primitive for this is readily available: introducing "revisions".

Revisions are a concept of Ember CLI Deploy of separately deployed builds, where one of them is "activated". ember-cli-deploy-simply-ssh already supports this mode out of the box, we just need to install the additional plugin ember-cli-deploy-revision-data. Now our deployment will not override our index.html on every deployment, but upload each deployed release into its separate directory under ./releases. Only the active one will then have a current symlink, that points to the active revision directory.

A pleasant side effect of this is that the frontend deployment using an atomic symlink switch is now very similar to what we do for our PHP / Symfony based deployments using Deployer.

Trigger workflow from PR

We now have to add another job to our deploy.yml workflow, that is mostly the same as our staging job. We just have to restrict it to only run for a pull request: if: startsWith(github.ref, 'refs/pull/'). Also for the staging and production jobs we have to add the --activate flag when invoking ember deploy, so the deployed revision gets automatically activated (symlinked), while this is not the case for the pull request deployment.

Domains, DNS and Rewrite rules

We now have the deployment part hooked up nicely. But we still have no means to access the deployed (but not activated!) revisions from a browser. This is the next part...

First we need to set up DNS, so that our preview subdomain (e.g. gets routed to the same server also handling For this we create a wildcard CNAME record, so something like this:

*.staging IN CNAME staging

This will direct any HTTP requests for * to our server, but the server still does not know what to do with this. We need to configure the domains (aka virtual host) root folder correctly. So for this needs to be the current symlink, so something like /var/www/staging/current. But now we also have to configure our server to handle any request for * using the base directory as the vhost root (e.g. /var/www/staging).

How to do this configuration depends highly on your server setup, if you have some admin frontend from your provider, or you deal with raw config files, or something else. So not going into details here.

Our server will now receive traffic for preview-<something>, and try to serve content from the /var/www/staging directory. But as you can imagine there is noting in there, no index.html, as the real content is in /var/www/staging/releases/<something>. The missing piece of the puzzle is now to find a way to dynamically serve the content from the right releases/* subdirectory, based on the used subdomain.

For our good ole Apache server I created the following mod_rewrite rule by placing a .htaccess file into the base directory:

RewriteEngine on

# serve revision if it exists
RewriteCond %{HTTP_HOST} ^preview-(\w+)\.staging\.example\.com$ [NC]
RewriteCond /var/www/releases/%1 -d
RewriteRule ^(.*)$ /releases/%1/$1 [L,NC]

# otherwise return a 404 response
RewriteRule ^ - [R=404,L,NS]

So for a request to this will now tell Apache to serve the content from the releases/abcdef directory, so the directory matching the uploaded Ember CLI Deploy revision.

This example is assuming you are running Apache, but a similar configuration should also be possible for other servers like nginx.

And that's it: we have now deployed our pull request commit to our staging server, and configured it so that it can serve that build without affecting the actual staging environment! 🎉

What's missing?

There is just one thing left we haven't solved yet from our initial requirements:

it must be obvious where to find the new deployment URL

For this we will make use of an excellent feature of Github Actions: that it can not only be used for traditional test and deployment tasks, but also to create experiences interacting with the diverse Github APIs itself. And through shared Github Actions, this can be done really easily, without reinventing the wheel.

So for our case I used an existing action to post a PR comment as soon as the PR has been successfully deployed:

PR is deployed and ready for preview!

For the deployment URL to be more deterministic I switched the ember-cli-deploy-revision-data hash type to git-commit, i.e. identifying a revision by the corresponding git commit SHA instead of a MD5 hash of index.html, so we can easily construct the matching URL in the Github workflow. The following two simple steps (the first one was needed to cut the SHA to the seven characters used by the deployment plugin) were all what was needed to post the comment:

  - uses: benjlevesque/short-sha@v1.0
    id: short-sha
      length: 7
  - name: Post PR comment
    uses: thollander/actions-comment-pull-request@1.0.0
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      message: "PR is deployed and ready for [preview](http://preview-${{ steps.short-sha.outputs.sha }}! 👀"

Mission accomplished! 🚀

I hope this was a useful read! Should you want to try this out by yourself, instead of listing all the final config files here, I'll just refer you to the public demo app repo which I used to experiment with this:

Should you have any questions, then feel free to get in touch!

Simon Ihmig
Simon Ihmig

Here we blog about topics we find interesting, while we work on custom made, ambitious web apps and product configurators.

show imprinthide imprint


kaliber5 GmbH

Sorthmannweg 20
22529 Hamburg
Tel: +49 (40) 226325 - 0


HRB: 99796 Hamburg
USt-ID-Nr.: DE 232385951
Tax number: 43 ⁄ 203 ⁄ 03244


Managing Directors:
Christian Olgemöller,
Simon Ihmig,
Ulf Schmidt-Dumont


The reproduction of information or data, in particular the use of texts, excerpts, pictures or video material requires prior consent.


Despite careful checks, we assume no liability for the content of external links. The operators bear sole responsibility for the content of linked pages.

show privacy and data protectionhide privacy and data protection

Privacy and Data Protection

kaliber5 takes the confidentiality of personal data very seriously and therefore uses up-to-date techniques to secure all data and to engage in dialogue with the user.

Use of personal data

Hereby kaliber5 informs about the processing of personal data when using the website and the rights under the data protection law.

Responsible for data processing

Für die Datenverarbeitung verantwortlich ist die
kaliber5 GmbH
Sorthmannweg 20
22529 Hamburg

Data Protection Officer

The responsible data protection officer of kaliber5 can be reached at the following address:
kaliber5 GmbH
Sorthmannweg 20
22529 Hamburg

Data categories and origin of the data

The website of kaliber5 GmbH can also be used anonymously. Personal data (such as IP addresses) of users of the website are stored in temporary log files at most at short notice. When visiting the website, data (date, time, time spent, viewed pages, navigation, hardware and software used) of kaliber5 are anonymized by an external service provider for the anonymous analysis of user behavior. The anonymization takes place before the data is stored at the service provider.

Purposes of data processing

Should the user in certain cases provide kaliber5 with their personal data, they will be treated confidentially in accordance with the data protection regulations in force at the Company's registered office. If he sends an e-mail to kaliber5 or uses an online form on the kaliber5 website, the personal data provided there (eg name or e-mail address) will be used exclusively for correspondence with the user for the purpose of sending the requested information or for the other purposes mentioned in the individual form. If personal data is processed for a purpose not mentioned here, the user will be informed in advance.

Legal basis for the processing of personal data

All personal data is processed by kaliber5 in compliance with the provisions of the European General Data Protection Regulation (DSGVO), the Federal Data Protection Act (BDSG) and all other applicable laws on the processing of personal data. The respective legal basis for the processing of personal data depends on the context and the purpose of the data processing. The legal basis for the processing of personal data are "legitimate interests of the person responsible for handling the communication" as well as other "predominant legitimate interests".

Data access

Within the responsible person, access to personal data is only given to persons and entities responsible for the data concerned and for the respective transaction. For this purpose, clear business distributions and authorization concepts have been set up.

All personal data are also transmitted to service providers for the above purposes. This integration of service providers is also necessary in the context of the maintenance and administration of IT systems.

In addition, personal information may be transmitted to regulatory authorities or other recipients if required by law or by contract.

Data transmission to a third country

All personal data is processed only within the European Economic Area (EEA).

Measures for data security

kaliber5 uses state-of-the-art organizational and technical measures to protect the security of the data against destruction, willful or accidental manipulation, loss or access by unauthorized persons.

All dialog forms used on the website use SSL (Secure Socket Layer) encryption. This SSL connection protects all personal data in transit from unauthorized third parties.

All users are requested to use the dialog forms for data transmission, since unsecured e-mails with unencrypted data may possibly be acknowledged or changed by unauthorized persons.

Privacy rights of those affected

At the address listed above, any person concerned may request the information, correction, deletion and publication (in a standard, structured and machine-readable format) of the personal data stored about him. He can also claim his right to restrict the processing of the data.

Right to object

If kaliber5 processes users' data to protect legitimate interests, users may object to processing for reasons that arise from their particular situation. kaliber5 will then no longer process such personal data unless kaliber5 can establish compelling legitimate grounds for processing that outweigh the interests of the data subject, rights and freedoms, or the processing serves to assert, exercise or defend legal claims. If kaliber5 processes the data of a victim on the basis of his consent, this consent can be withdrawn at any time with effect for the future.

Possibilities for complaint

Affected parties have the option of complaining to the above-mentioned data protection officer or to a data protection supervisory authority. The data protection supervisory authority responsible for kaliber5 is:

Der Hamburgische Beauftragte für Datenschutz und Informationsfreiheit
Klosterwall 6 (Block C), 20095 Hamburg
Tel.: (040) 4 28 54 - 40 40
E-Fax: (040) 4 279 - 11811

Duration of data storage

kaliber5 deletes all personal data as soon as it is no longer necessary for the above purposes.

In particular, the legal duties of proof and retention, which are governed inter alia by the Commercial Code, tax laws and the tax code, are taken into account. This results in storage periods of up to ten years. If claims can be asserted against kaliber5 (legal limitation period of three or up to thirty years), personal data can also be kept for this time.

Obligations of the user to provide data

No user is required to provide personal information when using the Website. However, there are services (such as "consultation request") that require personal data. Without this data, the desired performance can not be provided. Only the actual required data is collected.

Automated individual decisions or profiling

kaliber5 does not perform any automated processing to make a decision or profiling. If this happens in each case by kaliber5, the person concerned will be informed in the respective application.

Change this statement

From time to time, changes in technology and developments of the Website to adapt to this Privacy Policy. The user is therefore urged to observe the current version of this privacy policy each time the kaliber5 website is visited.

Use of cookies

While using the kaliber5 website, so-called cookies are stored on the respective computer. A cookie is a small text file that can be used, among other things, to control the presentation and operation of the website. Closing the dialog deletes the cookie. kaliber5 uses cookies to provide users with the best possible results and to ensure the correct use of the functions provided on the website. Statistical evaluations of the website are done anonymously in order to improve the usability of the website. In some cases, additional data can be requested in addition to the earmarked, mandatory data that help kaliber5 to get to know and advise the user better or to improve the website and for advertising purposes.

The cookies do not contain any personal information. The settings for cookies vary from browser to browser.

Use of Google Analytics

This website uses the Google Analytics web analytics service provided by Google Inc. (1600 Amphitheater Parkway, Mountain View, CA 94043, USA; "Google"). kaliber5 uses Google Analytics to analyze data for statistical purposes from AdWords. Google Analytics uses cookies that allow an analysis of the use of this webiste. As a rule, the information generated by the cookie about the use of this website is transmitted to a Google server in the USA and stored there. If the user of this website does not wish this, he can deactivate it under the following link kaliber5 has activated IP anonymization on this website. For example, Google will shorten the IP address of all visitors to this website beforehand within member states of the European Union or other parties to the Agreement on the European Economic Area.

n exceptional cases, the full IP address can also be transmitted to a Google server in the USA and shortened there. Google will use this information on behalf of kaliber5 to evaluate the use of the website, to create reports on website activity and to provide other services related to the website to kaliber5. There is no merger between the data processed by Google Analytics and other data from Google. Through appropriate setting in the browser, the user can prevent the storage of cookies. However, it is explicitly stated that some functions of this website may require the use of cookies. Under the following link, the user can install plugins that capture the data generated by the cookie and related to their use of the website (including your IP address) to prevent Google and the processing of this data by Google:

Using Google AdWords Conversion Tracking

kaliber5 is a Google AdWords customer and uses Google Conversion Tracking, an analytics service provided by Google Inc. (1600 Amphitheater Parkway, Mountain View, CA 94043, USA; "Google"). Google AdWords sets a cookie ("conversion cookie") on the user's computer if the user has reached the kaliber5 website via a Google ad.

These cookies are not for personal identification and lose their validity after 30 days. When the user visits certain pages while the cookie has not expired, kaliber5 and Google can detect that a user clicked on the ad and was redirected to the kaliber5 website.

All advertisers receive different cookies. Thus, cookies can not be tracked through the websites of advertisers. Conversion cookie information can be used to generate conversion statistics for advertisers. An AdWords customer learns how many people clicked on an ad and then took an action tagged with a conversion tracking tag. However, an advertiser does not receive information that personally identifies a user.

Any user who does not wish to participate in tracking may object to this use. To do this, he must prevent or disable the installation of the cookies by means of a corresponding setting in the browser. Thus, he is not included in the conversion tracking statistics.

The privacy policy and further information from Google can be found at:

Information encryption and transmission

When using the contact form and all other forms, the transmission of the information uses SSL (Secure Socket Layer) encryption with a key length of at least 128 bits.

Handling emails

kaliber5 uses the e-mail address provided by the user to respond to the requested information by e-mail. As soon as kaliber5 sends an e-mail, the transmission is first tried encrypted (with the help of TLS encryption). If the recipient does not support encryption, we will send the email unencrypted. In doing so, kaliber5 uses the stored e-mail addresses exclusively for correspondence with the respective users and does not forward any e-mail addresses to third parties.

kaliber5 keeps emails related to a contract and does not send unsolicited emails. All unsolicited e-mails claiming to be from kaliber5 are fake and should be deleted.

All unencrypted e-mails are not protected against unauthorized access, falsification, etc. on the Internet. Therefore, the use of the contact form is recommended.