{"id":3694,"date":"2015-09-08T09:25:27","date_gmt":"2015-09-08T07:25:27","guid":{"rendered":"https:\/\/usersnap.com\/?p=3694"},"modified":"2025-08-05T18:13:50","modified_gmt":"2025-08-05T16:13:50","slug":"deploying-static-websites-flightplan","status":"publish","type":"post","link":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/","title":{"rendered":"A beginner&#8217;s guide to deploying static sites with versioning and rollbacks using Flightplan"},"content":{"rendered":"\n<p>With the rise of cheap VPS (Virtual Private Server) services and the increase of complexity in the architecture of new web applications, deployment processes are becoming a very important topic and a skill to master to some extent.<\/p>\n\n\n\n<p>Long gone the days when we just needed a cheap hosting service and an FTP access to be able to setup and update our static websites.<\/p>\n\n\n\n<p>Furthermore, it is worth considering that software development has become a lot more collaborative thanks to tools like Git and services like GitHub and therefore people are getting used to the benefits of versioning. This brought in the idea of being able to keep our deploys versioned as well and to be able to roll back to a previous version easily in case a new deploy ends up to break something.<\/p>\n\n\n\n<p>In this article, we will learn how to set up a VPS (or a test virtual machine) to serve a static website with Nginx and how to create a <strong>simple yet effective deployment process<\/strong> to keep our website updated. Of course we will take care of integrating versioning and rollbacks in the process.<\/p>\n\n\n\n<p>I am assuming you already have a basic knowledge of Bash, Git, SSH, and Ubuntu but I will try to make things as clear as possible so that, even if you are a newbie, you should be able to understand and follow the tutorial.<\/p>\n\n\n\n<p>Also, you will need to have <a href=\"https:\/\/git-scm.com\" target=\"_blank\" rel=\"nofollow noopener\">Git<\/a> and <a href=\"https:\/\/nodejs.org\" target=\"_blank\" rel=\"nofollow noopener\">NodeJs<\/a> installed on your local machine.<br><\/p>\n\n\n\n<!--more-->\n\n\n<div class=\"acf-cta\" style=\"background-image: url(https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2025\/02\/Group-1000004194.svg); width: 100%;\"><h2>Try Usersnap for Product Deployment <\/h2><a href=\"https:\/\/usersnap.com\/signup\" class=\"cta-button\">Try Usersnap Now<\/a><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Setup of the machine<\/h2>\n\n\n\n<p>The first thing to do is to have a server machine that we will need to be configured to serve our website. My advice is to get a basic VPS on <a href=\"https:\/\/www.digitalocean.com\" target=\"_blank\" rel=\"nofollow noopener\">DigitalOcean<\/a> or <a href=\"https:\/\/www.linode.com\" target=\"_blank\" rel=\"nofollow noopener\">Linode<\/a>. It will cost you very little money ($5-10\/month) and you can shut down the machine once you are done with the tutorial and save the money for when you need to serve your next real website.<\/p>\n\n\n\n<p>As an alternative, you can also use a local virtual machine to simulate a remote server. In this case, my advice is to go with <a href=\"https:\/\/www.vagrantup.com\/\" target=\"_blank\" rel=\"nofollow noopener\">Vagrant<\/a>.<\/p>\n\n\n\n<p>In case you chose the first option, here are some tutorials that will explain you how to run a new machine and how to establish a connection through SSH:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.digitalocean.com\/help\/getting-started\/setting-up-your-server\/\" target=\"_blank\" rel=\"nofollow noopener\">Digital Ocean getting started guide<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.linode.com\/docs\/getting-started\" target=\"_blank\" rel=\"nofollow noopener\">Linode getting started guide<\/a><\/li>\n<\/ul>\n\n\n\n<p>In case you want to go with the local virtual machine option here&#8217;s another guide for you to <a href=\"https:\/\/docs.vagrantup.com\/v2\/getting-started\/\" target=\"_blank\" rel=\"nofollow noopener\">get started with Vagrant<\/a>.<\/p>\n\n\n\n<p>Whatever path you choose I expect you have the following requirements before moving on to the rest of the tutorial:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>You have a remote machine up and running with Ubuntu installed<\/li>\n\n\n\n<li>You know the IP address (or the domain name) of the machine<\/li>\n\n\n\n<li>You have an SSH key pair for your local machine<\/li>\n\n\n\n<li>You have authorized your key pair to connect to your remote machine with SSH<\/li>\n<\/ol>\n\n\n\n<p>So, just to make things clear, we will work on two different machines. The <strong>local<\/strong> machine (our computer) for development and the <strong>remote<\/strong> machine (our server) as production environment to serve our website to the World. All the next paragraphs will be labeled with &#8220;(local)&#8221; or &#8220;(remote)&#8221; to make clear where we will perform our actions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Install Nginx (remote)<\/h2>\n\n\n\n<p>I am assuming your server is running Ubuntu (at the time of writing the last LTS version is 14.04). The first thing we need to do is to login with SSH to our server and install Nginx. In case you never heard of it, Nginx is one of the most famous web servers. It is really easy to install and configure and it also offers very good performances. You can use Apache as an alternative but we will stick with Nginx in this tutorial. To install Nginx on Ubuntu you just need to be sure you are connected to your remote server and to run the following command in your bash prompt:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nsudo apt-get install -y nginx\n<\/pre><\/div>\n\n\n<p>If everything went fine we now have Nginx installed. If you open your browser and visit http:\/\/yourserver\/ (where <em>yourserver<\/em> is the real IP or the domain name of your server) you should be able to see the default Nginx welcome page. We will change that later with our real website.<\/p>\n\n\n\n<p>We will also need to have git installed on our server, so let&#8217;s add it:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nsudo apt-get install -y git\n<\/pre><\/div>\n\n\n<p>For now, our server is ok, we will see in a moment how to configure it for our needs, but let&#8217;s create a new website on our development machine first!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Create a sample website (local)<\/h2>\n\n\n\n<p>In this paragraph, we will create a very simple website and start to version it using git.<\/p>\n\n\n\n<p>Let&#8217;s just create a new folder in our local machine, open a command line shell <strong>in that folder<\/strong> and run:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit init\n<\/pre><\/div>\n\n\n<p>For the sake of simplicity, on our website, we are going to have two files and a HTML home page and a CSS file.<\/p>\n\n\n\n<p>Our HTML file is called <code>index.html<\/code> and it should look like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=\"utf-8\"&gt;\n &lt;title&gt;My wonderful website&lt;\/title&gt;\n &lt;link rel=\"stylesheet\" href=\"style.css\"&gt;\n &lt;\/head&gt;\n &lt;body&gt;\n &lt;h1&gt;Welcome to my wonderful website&lt;\/h1&gt;\n &lt;p&gt;You will never find another place as cool as this&lt;\/p&gt;\n &lt;\/body&gt;\n&lt;\/html&gt;<\/pre>\n\n\n\n<p>Pretty simple! Now let&#8217;s move to our <code>style.css<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&nbsp;\n\ncss\n html {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n }\n\nbody {\n margin: 0;\n }\narticle,\n aside,\n details,\n figcaption,\n figure,\n footer,\n header,\n hgroup,\n main,\n menu,\n nav,\n section,\n summary {\n display: block;\n }\naudio,\n canvas,\n progress,\n video {\n display: inline-block;\n vertical-align: baseline;\n }\naudio:not([controls]) {\n display: none;\n height: 0;\n }\n[hidden],\n template {\n display: none;\n }\na {\n background-color: transparent;\n }\na:active,\n a:hover {\n outline: 0;\n }\nabbr[title] {\n border-bottom: 1px dotted;\n }\nb,\n strong {\n font-weight: bold;\n }\ndfn {\n font-style: italic;\n }\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n }\nmark {\n background: #ff0;\n color: #000;\n }\nsmall {\n font-size: 80%;\n }\nsub,\n sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\nsup {\n top: -0.5em;\n }\nsub {\n bottom: -0.25em;\n }\nimg {\n border: 0;\n }\nsvg:not(:root) {\n overflow: hidden;\n }\nfigure {\n margin: 1em 40px;\n }\nhr {\n box-sizing: content-box;\n height: 0;\n }\npre {\n overflow: auto;\n }\ncode,\n kbd,\n pre,\n samp {\n font-family: monospace, monospace;\n font-size: 1em;\n }\nbutton,\n input,\n optgroup,\n select,\n textarea {\n color: inherit;\n font: inherit;\n margin: 0;\n }\nbutton {\n overflow: visible;\n }\nbutton,\n select {\n text-transform: none;\n }\nbutton,\n html input[type=\"button\"],\n input[type=\"reset\"],\n input[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n }\nbutton[disabled],\n html input[disabled] {\n cursor: default;\n }\nbutton::-moz-focus-inner,\n input::-moz-focus-inner {\n border: 0;\n padding: 0;\n }\ninput {\n line-height: normal;\n }\ninput[type=\"checkbox\"],\n input[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n }\ninput[type=\"number\"]::-webkit-inner-spin-button,\n input[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n }\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n }\ninput[type=\"search\"]::-webkit-search-cancel-button,\n input[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n }\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n }\nlegend {\n border: 0;\n padding: 0;\n }\ntextarea {\n overflow: auto;\n }\noptgroup {\n font-weight: bold;\n }\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n }\ndata-language=\"css\"&gt;td,\n th {\n padding: 0;\n }\n\/** ------ END NORMALIZE.CSS ------ **\/\nbody h1,\n body p {\n text-align: center;\n }<\/pre>\n\n\n\n<p>As a starting point, our css file includes <a href=\"https:\/\/necolas.github.io\/normalize.css\/\" target=\"_blank\" rel=\"nofollow noopener\">Normalize.css<\/a>, an open source stylesheet crafted to minimize the differences in the way different browsers handles some properties. It helps web designer to obtain the same graphic layout through all the major browser.<\/p>\n\n\n\n<p>So if we want to see how our new amazing web site looks like we just need to open our <code>index.html<\/code>.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-1.png\"><img decoding=\"async\" width=\"920\" height=\"463\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-1.png\" alt=\"website preview version\" class=\"wp-image-3701\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-1.png 920w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-1-300x151.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-1-140x70.png 140w\" sizes=\"(max-width: 920px) 100vw, 920px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n<div class=\"acf-cta\" style=\"background-image: url(https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2025\/02\/Group-1000004194.svg); width: 100%;\"><h2>Try Usersnap for Product Deployment <\/h2><a href=\"https:\/\/usersnap.com\/signup\" class=\"cta-button\">Try Usersnap Now<\/a><\/div>\n\n\n\n<p><\/p>\n\n\n\n<p>Isn&#8217;t a beautiful piece of art? Ok, maybe it&#8217;s not, but it&#8217;s a decent starting point for you next great thing \ud83d\ude42<\/p>\n\n\n\n<p>Now let&#8217;s add the new files to our local git repository. Go to the command line and launch the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit add index.html style.css\n<\/pre><\/div>\n\n\n<p>Then we can make our first commit:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit commit -am &quot;first version of the website&quot;\n<\/pre><\/div>\n\n\n<p>At this stage our git repository lives only on our local machine, so nobody will be able to read it from the outside, neither our server will be able to download the files when we want to release a new version of the project.<\/p>\n\n\n\n<p>The next thing to do is to add a remote repository. To understand what remote repositories are let&#8217;s quote the <a href=\"https:\/\/git-scm.com\/book\/en\/v2\/Git-Basics-Working-with-Remotes\" target=\"_blank\" rel=\"nofollow noopener\">official git documentation<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>To be able to collaborate on any Git project, you need to know how to manage your remote repositories. Remote repositories are versions of your project that are hosted on the Internet or network somewhere. You can have several of them, each of which generally is either read-only or read\/write for you. Collaborating with others involves managing these remote repositories and pushing and pulling data to and from them when you need to share work.<\/p>\n<\/blockquote>\n\n\n\n<p>So now we have one problem: where do we have to put our remote repository? The easiest solution is to get an account on <a href=\"https:\/\/github.com\" target=\"_blank\" rel=\"nofollow noopener\">GitHub<\/a> and create a new public repository there. GitHub is free to use for public repositories, but you can purchase a paid account to have private repositories in case your repository contains sensible information that you don&#8217;t want to share.<\/p>\n\n\n\n<p>Once you have your account on GitHub, you can <a href=\"https:\/\/github.com\/new\" target=\"_blank\" rel=\"nofollow noopener\">create a new repository<\/a> using their web interface:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/create-a-new-repository-on-github-flightplan.png\"><img decoding=\"async\" width=\"700\" height=\"531\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/create-a-new-repository-on-github-flightplan.png\" alt=\"create a new repository\" class=\"wp-image-3703\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/create-a-new-repository-on-github-flightplan.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/create-a-new-repository-on-github-flightplan-300x228.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/create-a-new-repository-on-github-flightplan-140x106.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Let&#8217;s add a name and a description, be sure to select <strong>public<\/strong> and click the &#8220;Create repository&#8221; button.<\/p>\n\n\n\n<p>Once done you should be able to see something like this:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/your-public-repository-on-github.png\"><img decoding=\"async\" width=\"700\" height=\"531\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/your-public-repository-on-github.png\" alt=\"your public github repo for static websites\" class=\"wp-image-3720\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/your-public-repository-on-github.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/your-public-repository-on-github-300x228.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/your-public-repository-on-github-140x106.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>As you can tell, our project file are not there yet, let&#8217;s <em>push<\/em> them!<\/p>\n\n\n\n<p>To push our files we need to associate our local repository to the new remote one, so copy your repository address in the right column (you can choose <code>https<\/code> or <code>ssh<\/code> URLs, I recommend to use the former) and get back to your command line console with the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit remote add origin\n<\/pre><\/div>\n\n\n<p>Be sure to replace with the URL you copied from your GitHub repository page.<\/p>\n\n\n\n<p>Now we have associated our local repository with the new GitHub remote one. GitHub creates some files when you create a new repository, so it&#8217;s a great thing to download them into your local repository:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit pull origin master\n<\/pre><\/div>\n\n\n<p>This will make you ready to push our file to the remote repository<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit push --set-upstream origin master\n<\/pre><\/div>\n\n\n<p>If you refresh your GitHub project page, you will now see all our project files are there.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prepare the deployment scripts (local)<\/h2>\n\n\n\n<p>We now have our website and our server and at this stage we can move to the core of our article: how to organize a process to simplify the deployment of our website in the new server.<\/p>\n\n\n\n<p>There are hundreds of tools and SaaS platforms to manage complex deployment processes on one or more servers, but one of my favourite is <a href=\"https:\/\/github.com\/pstadler\/flightplan\" target=\"_blank\" rel=\"nofollow noopener\">Flightplan<\/a>. Flightplan is a NodeJS library that can be easily installed and configured to create a sequence of shell commands in our local machine and in our remote server to deploy and update our website or app.<\/p>\n\n\n\n<p>The first thing to do is to install Flightplan in your local machine:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nnpm install -g flightplan\nnpm install flightplan --save-dev\n<\/pre><\/div>\n\n\n<p>The first command installs Flightplan as a global command (the <code>fly<\/code> command) in your system, the second one allows us to use the Flightplan library inside your project.<\/p>\n\n\n\n<p>Now we need to create a configuration file inside the main folder of our project called <code>flightplan.js<\/code>.<\/p>\n\n\n\n<p>Before writing the code in our Flightplan configuration file let&#8217;s understand what exactly we want to achieve and how we want to structure our files and folders to do so.<\/p>\n\n\n\n<p>As we said, our objective is to be able to deploy a new version of our website with a single command and to be able to rollback to the previous version just by launching another command. So we expect to have at least two different commands: <code>deploy<\/code> and <code>rollback<\/code>.<\/p>\n\n\n\n<p>To achieve this goal we can structure the folders in our remote server as follows:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>repo<\/code>: a folder used to download the data (clone) from our git repository. It will allow us to download only the data that changed since the last time we deployed, so it will be really efficient.<\/li>\n\n\n\n<li><code>versions<\/code>: in this folder we will have different subfolders, one for every deploy.<\/li>\n\n\n\n<li><code>current<\/code>: a symlink to the last deployed version in the <code>versions<\/code> folder. It&#8217;s the folder that will be used to configure Nginx and the only one that will be shown online.<\/li>\n<\/ul>\n\n\n\n<p>The flow we want to create for every deploy is the following:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>the <code>repo<\/code> folder is updated to the last version using <code>git pull<\/code><\/li>\n\n\n\n<li>the repo folder is copied as subfolder inside <code>versions<\/code> with a convention name that is given by the current timestamp on the server (this allows us to keep versions easily ordered)<\/li>\n\n\n\n<li>the newly copied folder is &#8220;symlinked&#8221; to the <code>current<\/code> folder<\/li>\n<\/ol>\n\n\n\n<p>Furthermore we want to add a nice feature to save disk space: we want to have a fixed numbers of deployed versions on the server (e.g. no more than 10), so we might need to delete the older ones at the end of every deploy.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ flightplan.js\nvar plan = require('flightplan');\n\n\/**\n * Remote configuration for \"production\"\n *\/\nplan.target('production', {\n  host: 'example.com',\n  username: 'someuser',\n  password: 'somepassword',\n  agent: process.env.SSH_AUTH_SOCK,\n\n  webRoot: '\/var\/www\/mywebsite',\n  ownerUser: 'www-data',\n  repository: 'https:\/\/github.com\/someuser\/example-com.git',\n  branchName: 'master',\n  maxDeploys: 10\n});\n\n\/**\n * Creates all the necessary folders in the remote and clones the source git repository\n * \n * Usage:\n * &gt; fly setup[:remote]\n *\/\nplan.remote('setup', function(remote) {\n\tremote.hostname();\n\n\tremote.sudo('mkdir -p ' + remote.runtime.webRoot);\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tremote.sudo('mkdir versions');\n\t\tremote.sudo('git clone -b ' + remote.runtime.branchName + ' ' + remote.runtime.repository + ' repo');\n\t});\n});\n\n\/**\n * Deploys a new version of the code pulling it from the git repository\n *\n * Usage:\n * &gt; fly deploy[:remote]\n *\/\nplan.remote('deploy', function(remote) {\n\tremote.hostname();\n\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tremote.sudo('cd repo &amp;&amp; git pull');\n\t\tvar command = remote.exec('date +%s.%N');\n\t\tvar versionId = command.stdout.trim();\n\t\tvar versionFolder = 'versions\/' + versionId\n\t\t\n\t\tremote.sudo('cp -R repo ' + versionFolder);\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' ' + versionFolder);\n\t\tremote.sudo('ln -fsn ' + versionFolder + ' current');\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' current');\n\n\t\tif (remote.runtime.maxDeploys &gt; 0) {\n\t\t\tremote.log('Cleaning up old deploys...');\n\t\t\tremote.sudo('rm -rf `ls -1dt versions\/* | tail -n +' + (remote.runtime.maxDeploys+1) + '`');\n\t\t}\n\n\t\tremote.log('Successfully deployied in ' + versionFolder);\n\t\tremote.log('To rollback to the previous version run \"fly rollback:production\"');\n\t});\n});\n\n\/**\n * Rollbacks to the previous deployed version (if any)\n *\n * Usage\n * &gt; fly rollback[:remote]\n *\/\nplan.remote('rollback', function(remote) {\n\tremote.hostname();\n\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tvar command = remote.exec('ls -1dt versions\/* | head -n 2');\n\t\tvar versions = command.stdout.trim().split('\\n');\n\n\t\tif (versions.length &lt; 2) {\n\t\t\treturn remote.log('No version to rollback to');\n\t\t}\n\n\t\tvar lastVersion = versions[0];\n\t\tvar previousVersion = versions[1];\n\n\t\tremote.log('Rolling back from ' + lastVersion + ' to ' + previousVersion);\n\n\t\tremote.sudo('ln -fsn ' + previousVersion + ' current');\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' current');\n\n\t\tremote.sudo('rm -rf ' + lastVersion);\n\t});\n});\n<\/pre>\n\n\n\n<p>It&#8217;s a lot of code, let&#8217;s analyze it piece by piece.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The remote configuration<\/h3>\n\n\n\n<p>The remote configuration defines our production environment on our server and tells Flightplan how to connect to it and what the specific options for the server are. In more advanced setups you can also specify an array of servers to which the deploy will be carried simultaneously.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">plan.target('production', {\n  host: 'example.com',\n  username: 'someuser',\n  password: 'somepassword',\n  agent: process.env.SSH_AUTH_SOCK,\n\n  webRoot: '\/var\/www\/mywebsite',\n  ownerUser: 'www-data',\n  repository: 'https:\/\/github.com\/someuser\/example-com.git',\n  branchName: 'master',\n  maxDeploys: 10\n});\n<\/pre>\n\n\n\n<p>As you can see there are some configurations that you will need to change, like:&nbsp;<code>host<\/code>, <code>username<\/code>, <code>password<\/code> (probably not required), <code>webRoot<\/code> and <code>repository<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The setup command<\/h3>\n\n\n\n<p>The setup command is a helper command that you can launch once to configure the remote machine folders.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">plan.remote('setup', function(remote) {\n\tremote.hostname();\n\n\tremote.sudo('mkdir -p ' + remote.runtime.webRoot);\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tremote.sudo('mkdir versions');\n\t\tremote.sudo('git clone -b ' + remote.runtime.branchName + ' ' + remote.runtime.repository + ' repo');\n\t});\n});\n<\/pre>\n\n\n\n<p>The command <code>remote.hostname()<\/code> is a debug function that prints the hostname of the server the script is currently in. I recommend to use it, because if you need to switch to a multi-server environment it will be very useful to follow the output of all the servers.<\/p>\n\n\n\n<p>In the next lines we first create our web root folder, then inside that folder we create the <code>versions<\/code> folder and we clone our repository into the folder <code>repo<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The deploy command<\/h3>\n\n\n\n<p>The deploy command is, of course, the most important one, let&#8217;s review its code:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">plan.remote('deploy', function(remote) {\n\tremote.hostname();\n\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tremote.sudo('cd repo &amp;&amp; git pull');\n\t\tvar command = remote.exec('date +%s.%N');\n\t\tvar versionId = command.stdout.trim();\n\t\tvar versionFolder = 'versions\/' + versionId\n\t\t\n\t\tremote.sudo('cp -R repo ' + versionFolder);\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' ' + versionFolder);\n\t\tremote.sudo('ln -fsn ' + versionFolder + ' current');\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' current');\n\n\t\tif (remote.runtime.maxDeploys &gt; 0) {\n\t\t\tremote.log('Cleaning up old deploys...');\n\t\t\tremote.sudo('rm -rf `ls -1dt versions\/* | tail -n +' + (remote.runtime.maxDeploys+1) + '`');\n\t\t}\n\n\t\tremote.log('Successfully deployied in ' + versionFolder);\n\t\tremote.log('To rollback to the previous version run \"fly rollback:production\"');\n\t});\n});\n<\/pre>\n\n\n\n<p>The first thing we do is to update the <code>repo<\/code> folder pulling all the new stuff from our repository. Then we execute the <code>date +%s.%N<\/code> bash command that prints out the current timestamp in seconds and milliseconds (e.g. <code>1441459969.874386446<\/code>). This will be the id of our new deploy. At this stage, we simply copy the <code>repo<\/code> folder inside the <code>versions<\/code> folder using the new version id as folder name (e.g. <code>versions\/1441459969.874386446<\/code>). To finish the process we symlink this new folder to the <code>current<\/code> folder. We also take care to assign these files to the user <code>www-data<\/code> so that nginx will be able to read these files.<\/p>\n\n\n\n<p>Then we clean up the old deploys using a handy bash short-liner command. The command lists and removes all the folders in the <code>versions<\/code> directory skipping the first n, where n is the number of deploys that we want to keep.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The rollback command<\/h3>\n\n\n\n<p>The rollback command allows us to restore the previous version and to delete the newest one, it&#8217;s like a step back in time.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">plan.remote('rollback', function(remote) {\n\tremote.hostname();\n\n\tremote.with('cd ' + remote.runtime.webRoot, function() {\n\t\tvar command = remote.exec('ls -1dt versions\/* | head -n 2');\n\t\tvar versions = command.stdout.trim().split('\\n');\n\n\t\tif (versions.length &lt; 2) {\n\t\t\treturn remote.log('No version to rollback to');\n\t\t}\n\n\t\tvar lastVersion = versions[0];\n\t\tvar previousVersion = versions[1];\n\n\t\tremote.log('Rolling back from ' + lastVersion + ' to ' + previousVersion);\n\n\t\tremote.sudo('ln -fsn ' + previousVersion + ' current');\n\t\tremote.sudo('chown -R ' + remote.runtime.ownerUser + ':' + remote.runtime.ownerUser + ' current');\n\n\t\tremote.sudo('rm -rf ' + lastVersion);\n\t});\n});\n<\/pre>\n\n\n\n<p>What we do here is to use another bash short-liner to get the last 2 deployed versions inside the <code>versions<\/code> folder. The first one is the newest one, the second is the one we want to rollback to. Then we simply symlink the second to the <code>current<\/code> folder and delete the first one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Make the first deploy (local)<\/h2>\n\n\n\n<p>Ok, now that we have all our Flightplan commands ready we can launch off the first deploy.<\/p>\n\n\n\n<p>For the first time we need to run:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nfly setup:production\n<\/pre><\/div>\n\n\n<p>to set up all the folders on our server, then we can launch our first deploy with<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nfly deploy:production\n<\/pre><\/div>\n\n\n<p>If we have done everything correctly our deploy should be successful and show a green dot! But wait&#8230; why our server still shows the default nginx page? Oh well, we still need to configure nginx to use our <code>current<\/code> folder!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Configure the web server (remote)<\/h2>\n\n\n\n<p>Get back to you ssh console and run:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nsudo nano \/etc\/nginx\/sites-available\/default\n<\/pre><\/div>\n\n\n<p>This will open the nginx configuration file with the nano editor. Move the cursor down until you see a <code>server<\/code> block definition and look for the <code>root<\/code> property. There should be the default nginx page path in there, just replace it with the path of your <code>current<\/code> folder (e.g. <code>root \/var\/www\/mywebsite\/current;<\/code>). Then you have to save the file with <code>ctrl + o<\/code> and exit with <code>ctrl + x<\/code>.<\/p>\n\n\n\n<p>The last step to do is to force nginx to reload the new configuration:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nsudo service nginx reload\n<\/pre><\/div>\n\n\n<p>Now get back to our website and refresh! Tad\u00e1! Our web server is now serving our new website! Cheer up!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Updating the website (local)<\/h2>\n\n\n\n<p>Ok now maybe it&#8217;s time to realize that our new website it&#8217;s not so amazing, maybe it would be nice to add an image and change the background color. You can grab a very nice image from <a href=\"https:\/\/unsplash.it\/800\/500?image=744\" target=\"_blank\" rel=\"nofollow noopener\">here<\/a>. Let&#8217;s update our HTML file:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;!doctype html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=\"utf-8\"&gt;\n &lt;title&gt;My wonderful website&lt;\/title&gt;\n &lt;link rel=\"stylesheet\" href=\"css\/style.css\"&gt;\n &lt;\/head&gt;\n &lt;body&gt;\n &lt;h1&gt;Welcome to my wonderful website&lt;\/h1&gt;\n &lt;p&gt;You will never find another place as cool as this&lt;\/p&gt;\n &lt;p&gt;&lt;img src=\"img\/bridge.jpg\" alt=\"A nice picture of The Bridge\"\/&gt;&lt;\/p&gt;\n &lt;\/body&gt;\n&lt;\/html&gt;<\/pre>\n\n\n\n<p>We basically just added the paragraph with the image, but notice we also moved our stylesheet into its own <code>css<\/code> folder! Ok now just add the following block of CSS at the end of our stylesheet to change the background color:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ncss\n body {\n background: #97d2fc;\n }\n<\/pre><\/div>\n\n\n<p>And here we have it!<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-2.png\"><img decoding=\"async\" width=\"700\" height=\"611\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-2.png\" alt=\"website preview 2\" class=\"wp-image-3704\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-2.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-2-300x262.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/our-website-preview-version-2-140x122.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>I know, I know&#8230; it looks like one of that websites from the early &#8217;90s, but let&#8217;s pretend it&#8217;s wonderful and we want to deploy this new version for now.<\/p>\n\n\n\n<p>So first of all we need to commit our modifications. It&#8217;s always a good idea to launch a <code>git status<\/code> before committing to have a clear idea of what has changed. That&#8217;s the output we should get:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status.png\"><img decoding=\"async\" width=\"700\" height=\"449\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status.png\" alt=\"git-status\" class=\"wp-image-3707\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-300x192.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-140x90.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>There are some things to review here. First of all we moved our css file to the folder <code>css<\/code> so git thinks we deleted it. We also changed our file <code>index.html<\/code>, then there are some new files and folders that we need to add to the repository.<\/p>\n\n\n\n<p>But it&#8217;s a good idea to not add the <code>node_modules<\/code> folder because we don&#8217;t want it on our website, it&#8217;s just there because it contains all the needed dependencies to run Flightplan on our local machine. Thus, the best approach is to put it onto the git ignore list. To do so we need to create a new file called <code>.gitignore<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># .gitignore\nnode_modules\/\n<\/pre>\n\n\n\n<p>If you run <code>git status<\/code> again you can see that the output changed a bit:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-2.png\"><img decoding=\"async\" width=\"700\" height=\"449\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-2.png\" alt=\"git-status-2\" class=\"wp-image-3705\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-2.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-2-300x192.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-2-140x90.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The <code>node_modules<\/code> folder disappeared and the new <code>.gitignore<\/code> file is waiting to be added to the repository.<\/p>\n\n\n\n<p>To add all these new files and folders to our repository we need to launch the following git command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit add --all\n<\/pre><\/div>\n\n\n<p>Let&#8217;s launch <code>git status<\/code> again to see what&#8217;s happened:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><a href=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-3.png\"><img decoding=\"async\" width=\"700\" height=\"449\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-3.png\" alt=\"git-status-3\" class=\"wp-image-3706\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-3.png 700w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-3-300x192.png 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/git-status-3-140x90.png 140w\" sizes=\"(max-width: 700px) 100vw, 700px\" \/><\/a><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Now everything is green and git also recognized that we moved the css file to the new <code>css<\/code> folder. Great, it seems that we have added all the files that we need and we are now ready to <strong>commit<\/strong> the changes to generate a new version in the local repository:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit commit -am &quot;Added picture and background color&quot;\n<\/pre><\/div>\n\n\n<p>And then we can send this new version to the remote repository with:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\ngit push\n<\/pre><\/div>\n\n\n<p>Now it&#8217;s time to run our deploy script to update our remote server:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nfly deploy:production\n<\/pre><\/div>\n\n\n<p>We should see a bunch of lines of output and then a green dot that confirms that our deploy went fine! Great, let&#8217;s now refresh our website page in the browser and see that our new version is online!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rolling back (local)<\/h2>\n\n\n\n<p>We are just cheering up for the update but after a couple of minutes one of our friends call us to say:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>&#8220;Man, what&#8217;s happened to your website? It looks uglier than ever, I really loved the previous version!&#8221;<\/p>\n<\/blockquote>\n\n\n\n<p>So yes, maybe we realize as well that the previous version had that kind of fascinating minimalism and that it&#8217;s better to rollback. Our Flightplan configuration it&#8217;s here to support us and make things simple, just run:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; light: false; title: ; toolbar: true; notranslate\" title=\"\">\nfly rollback:production\n<\/pre><\/div>\n\n\n<p>Let&#8217;s refresh your browser and tell our friend he was right!<\/p>\n\n\n\n<p>Maybe it&#8217;s time to work on a better new version, but now you know how to do it on your own \ud83d\ude09<\/p>\n\n\n\n<p><\/p>\n\n\n<div class=\"acf-cta\" style=\"background-image: url(https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2025\/02\/Group-1000004194.svg); width: 100%;\"><h2>Try Usersnap for Product Deployment <\/h2><a href=\"https:\/\/usersnap.com\/signup\" class=\"cta-button\">Try Usersnap Now<\/a><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping it up!<\/h2>\n\n\n\n<p>In this article we learned a lot of useful things about how to keep our website versioned and how to deploy it easily. We used a bunch of interesting tools like git and Flightplan.<\/p>\n\n\n\n<p>The article was focused on explaining the basics of these tools. You might have a dynamic website written in Php or NodeJs rather than a static site&nbsp;so you might need to add more steps to configure your remote server and your deploy process. Anyways, it shouldn&#8217;t be too hard now that you have a boilerplate to start from.<\/p>\n\n\n\n<p>I hope the article was useful, I really hope to hear your opinions in the comments below!<\/p>\n\n\n\n<p><em><b>About the Author:<\/b><\/em><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"alignright\"><img decoding=\"async\" width=\"300\" height=\"300\" src=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/08\/luciano-Mammino-300x300.jpg\" alt=\"Luciano Mammino\" class=\"wp-image-3577\" srcset=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/08\/luciano-Mammino-300x300.jpg 300w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/08\/luciano-Mammino-150x150.jpg 150w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/08\/luciano-Mammino-140x140.jpg 140w, https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/08\/luciano-Mammino.jpg 800w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/figure>\n<\/div>\n\n\n<p>Luciano is a Software Engineer born in 1987, the same year that the Nintendo released \u201c<em>Super Mario Bros<\/em>\u201d in Europe, which, by chance is his favorite video game!<\/p>\n\n\n\n<p>He is passionate about code, the web, smart apps and everything that&#8217;s creative like music, art and design. As web developer, his experience has been mostly with PHP and Symfony2, even if he recently fell in love with Javascript, NodeJS and, Docker. In his (scarce) free time he writes on his personal blog at <a href=\"http:\/\/loige.co\/\" target=\"_blank\" rel=\"noopener\">loige.co<\/a>.<\/p>\n\n\n\n<p><em><strong>This article is brought to you by <a href=\"https:\/\/usersnap.com\">Usersnap<\/a>. It\u2019s your central place to organize user feedback and collect bug reports. Report bugs in your browser, and see the bigger picture. <a href=\"https:\/\/usersnap.com\/signup\">Get your 15-day free trial now.<\/a><\/strong><\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>With the rise of cheap VPS (Virtual Private Server) services and the increase of complexity in the architecture of new web applications, deployment processes are becoming a very important topic and a skill to master to some extent. Long gone the days when we just needed a cheap hosting service and an FTP access to [&hellip;]<\/p>\n","protected":false},"author":19,"featured_media":3699,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":true,"inline_featured_image":false,"ub_ctt_via":"","footnotes":""},"categories":[8],"tags":[],"class_list":["post-3694","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development-blog"],"acf":[],"featured_image_src":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","author_info":{"display_name":"Luciano Mammino","author_link":"https:\/\/usersnap.com\/blog\/author\/luciano\/"},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.6 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>A beginner&#039;s guide to deploying static sites using Flightplan!<\/title>\n<meta name=\"description\" content=\"In this article you&#039;ll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A beginner&#039;s guide to deploying static sites using Flightplan!\" \/>\n<meta property=\"og:description\" content=\"In this article you&#039;ll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\" \/>\n<meta property=\"og:site_name\" content=\"Usersnap Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/usersnap\" \/>\n<meta property=\"article:published_time\" content=\"2015-09-08T07:25:27+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-08-05T16:13:50+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"700\" \/>\n\t<meta property=\"og:image:height\" content=\"467\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Luciano Mammino\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@usersnap\" \/>\n<meta name=\"twitter:site\" content=\"@usersnap\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Luciano Mammino\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\"},\"author\":{\"name\":\"Luciano Mammino\",\"@id\":\"https:\/\/usersnap.com\/blog\/#\/schema\/person\/4cf1eb7a2024a26cec6234d2210bbef4\"},\"headline\":\"A beginner&#8217;s guide to deploying static sites with versioning and rollbacks using Flightplan\",\"datePublished\":\"2015-09-08T07:25:27+00:00\",\"dateModified\":\"2025-08-05T16:13:50+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\"},\"wordCount\":3156,\"publisher\":{\"@id\":\"https:\/\/usersnap.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg\",\"articleSection\":[\"Web Development\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\",\"url\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\",\"name\":\"A beginner's guide to deploying static sites using Flightplan!\",\"isPartOf\":{\"@id\":\"https:\/\/usersnap.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg\",\"datePublished\":\"2015-09-08T07:25:27+00:00\",\"dateModified\":\"2025-08-05T16:13:50+00:00\",\"description\":\"In this article you'll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage\",\"url\":\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg\",\"contentUrl\":\"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg\",\"width\":700,\"height\":467,\"caption\":\"flightplan nodejs\"},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/usersnap.com\/blog\/#website\",\"url\":\"https:\/\/usersnap.com\/blog\/\",\"name\":\"Usersnap Blog\",\"description\":\"Learn more about how to collect user feedback and build better products with the magic power of feedback.\",\"publisher\":{\"@id\":\"https:\/\/usersnap.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/usersnap.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/usersnap.com\/blog\/#organization\",\"name\":\"Usersnap\",\"url\":\"https:\/\/usersnap.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/usersnap.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/usersnap.com\/wp-content\/uploads\/2020\/08\/Usersnap-Updated-Logo.png\",\"contentUrl\":\"https:\/\/usersnap.com\/wp-content\/uploads\/2020\/08\/Usersnap-Updated-Logo.png\",\"width\":136,\"height\":26,\"caption\":\"Usersnap\"},\"image\":{\"@id\":\"https:\/\/usersnap.com\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/usersnap\",\"https:\/\/x.com\/usersnap\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/usersnap.com\/blog\/#\/schema\/person\/4cf1eb7a2024a26cec6234d2210bbef4\",\"name\":\"Luciano Mammino\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/usersnap.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/104d57b5a075cf3c194390f48461e793ce9a433c2f4518b52cfc698e3e22b4c2?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/104d57b5a075cf3c194390f48461e793ce9a433c2f4518b52cfc698e3e22b4c2?s=96&d=mm&r=g\",\"caption\":\"Luciano Mammino\"},\"description\":\"Luciano, Usersnap's author, invites you to explore the world through their eyes on our blog. Gain knowledge and inspiration through our blog's exclusive content.\",\"url\":\"https:\/\/usersnap.com\/blog\/author\/luciano\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A beginner's guide to deploying static sites using Flightplan!","description":"In this article you'll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/","og_locale":"en_US","og_type":"article","og_title":"A beginner's guide to deploying static sites using Flightplan!","og_description":"In this article you'll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.","og_url":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/","og_site_name":"Usersnap Blog","article_publisher":"https:\/\/www.facebook.com\/usersnap","article_published_time":"2015-09-08T07:25:27+00:00","article_modified_time":"2025-08-05T16:13:50+00:00","og_image":[{"width":700,"height":467,"url":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","type":"image\/jpeg"}],"author":"Luciano Mammino","twitter_card":"summary_large_image","twitter_creator":"@usersnap","twitter_site":"@usersnap","twitter_misc":{"Written by":"Luciano Mammino","Est. reading time":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#article","isPartOf":{"@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/"},"author":{"name":"Luciano Mammino","@id":"https:\/\/usersnap.com\/blog\/#\/schema\/person\/4cf1eb7a2024a26cec6234d2210bbef4"},"headline":"A beginner&#8217;s guide to deploying static sites with versioning and rollbacks using Flightplan","datePublished":"2015-09-08T07:25:27+00:00","dateModified":"2025-08-05T16:13:50+00:00","mainEntityOfPage":{"@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/"},"wordCount":3156,"publisher":{"@id":"https:\/\/usersnap.com\/blog\/#organization"},"image":{"@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage"},"thumbnailUrl":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","articleSection":["Web Development"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/","url":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/","name":"A beginner's guide to deploying static sites using Flightplan!","isPartOf":{"@id":"https:\/\/usersnap.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage"},"image":{"@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage"},"thumbnailUrl":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","datePublished":"2015-09-08T07:25:27+00:00","dateModified":"2025-08-05T16:13:50+00:00","description":"In this article you'll learn how to deploy a static website with Nginx and how to create a deployment process to keep our website updated using Flightplan.","inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/usersnap.com\/blog\/deploying-static-websites-flightplan\/#primaryimage","url":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","contentUrl":"https:\/\/usersnap.com\/blog\/wp-content\/uploads\/2015\/09\/flightplan-nodejs-web-development-deployment.jpg","width":700,"height":467,"caption":"flightplan nodejs"},{"@type":"WebSite","@id":"https:\/\/usersnap.com\/blog\/#website","url":"https:\/\/usersnap.com\/blog\/","name":"Usersnap Blog","description":"Learn more about how to collect user feedback and build better products with the magic power of feedback.","publisher":{"@id":"https:\/\/usersnap.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/usersnap.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/usersnap.com\/blog\/#organization","name":"Usersnap","url":"https:\/\/usersnap.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/usersnap.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/usersnap.com\/wp-content\/uploads\/2020\/08\/Usersnap-Updated-Logo.png","contentUrl":"https:\/\/usersnap.com\/wp-content\/uploads\/2020\/08\/Usersnap-Updated-Logo.png","width":136,"height":26,"caption":"Usersnap"},"image":{"@id":"https:\/\/usersnap.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/usersnap","https:\/\/x.com\/usersnap"]},{"@type":"Person","@id":"https:\/\/usersnap.com\/blog\/#\/schema\/person\/4cf1eb7a2024a26cec6234d2210bbef4","name":"Luciano Mammino","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/usersnap.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/104d57b5a075cf3c194390f48461e793ce9a433c2f4518b52cfc698e3e22b4c2?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/104d57b5a075cf3c194390f48461e793ce9a433c2f4518b52cfc698e3e22b4c2?s=96&d=mm&r=g","caption":"Luciano Mammino"},"description":"Luciano, Usersnap's author, invites you to explore the world through their eyes on our blog. Gain knowledge and inspiration through our blog's exclusive content.","url":"https:\/\/usersnap.com\/blog\/author\/luciano\/"}]}},"_links":{"self":[{"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/posts\/3694","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/users\/19"}],"replies":[{"embeddable":true,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/comments?post=3694"}],"version-history":[{"count":0,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/posts\/3694\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/media\/3699"}],"wp:attachment":[{"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/media?parent=3694"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/categories?post=3694"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/usersnap.com\/blog\/wp-json\/wp\/v2\/tags?post=3694"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}