Static website hosting with nginx on EC2
This article walks through how to set up a simple web server to host a static website with nginx on EC2.
All the examples are drawn from my experience building this very website, from creating the static files and setting up the server to registering a domain and enabling HTTPS.
Table of content:
- Creating the static files
- Setting up the server
- Registering a domain and routing traffic to the website
- Setting up HTTPS
1. Creating the static files
For the purpose of this example, we will be generating and managing static files using Jekyll.
Jekyll is a static website generator. It helps you separate layout and content in html and Markdown files respectively, and compiles everything down to a ready to deploy website.
The project’s README
states the philosophy:
Jekyll does what you tell it to do — no more, no less.
It doesn’t try to outsmart users by making bold assumptions, nor does it burden them with needless complexity and configuration.
Put simply, Jekyll gets out of your way and allows you to concentrate on what truly matters: your content.
A popular alternative is Hugo. I decided to go for Jekyll because I am familiar with the template system, and I happen to like the default theme. Plenty of free and open source themes are available for both Jekyll and Hugo, so feel free to pick a different one.
Installing Jekyll & setting up the project
First, let’s make sure we have a recent version of ruby installed:
Ruby >= 2.2.5 is required for Jekyll.
To update, install rvm, the ruby package manager:
We can now install Jekyll, as well as bundler to help build and run the development server:
Set up the directory structure:
Browse to http://localhost:4000
The website layout is set up. We will soon add content, but first let see how to import the theme layout into the project. This will enable easy editing of the layout files to fit our purpose.
By default, the theme is “hidden” inside a ruby gem.
Let’s find out where the actual theme files are hidden:
copy over the files:
Finally, we need to adjust the project settings, as described in the official documentation:
- Open
Gemfile
and removegem "minima", "~> 2.0"
- Open
_config.yml
and removetheme: minima
You may want to tweak the project metadata further by editing _config.yml
, e.g website title, author, description, etc.
Creating a landing page
For example,
Open index.md
and set the landing page metadata:
The important bit is the layout. It describes the name of the html file used to render the page.
_layouts/home.html
looks something like:
The content
variable is fed from the text below the header in index.md
, e.g:
Run the server again. Going forward, the server will update itself whenever a file changes.
The task of modifying the layout is left as an exercise for the reader. Take your time and tweak the templates to match your expectations.
Live demo: romainstrock.com
Next, let’s create the blog.
Setting up a blog
For the purpose of this example, a blog is:
- an index page introducing the blog and listing existing posts,
- a collection of posts.
With Jekyll, there are two main ways to create posts. One way is to add posts to the _posts
directory.
This is the common way to define posts,
however a main drawback is that post names need to follow the following strict convention:
YEAR-MONTH-DAY-title.md
At build time, posts are expanded into _site/posts/<YEAR>/<MONTH>/<DAY>/<title>.html
. This is unfortunate because
it forces us to include the date in the url.
When using a Jekyll server, like our development server, it is possible to overcome this limitation by setting
permalink: <title>.html
in the post metadata. However, in this example we are relying on the output html structure
because our web server will only serve static files - without running Jekyll.
A better way to structure a blog is to use collections.
Add a new collection to your project in _config.yaml
:
Then create a folder _blog
which will hold the blog index and the posts.
The blog index is the page that will be rendered when the “naked” /blog/
url is requested.
Create the file _blog/index.md
:
The key now is to create a new layout file _layouts/blog.html
, making use of the content and listing
all the existing posts.
Live demo: /blog/
Let’s now create a post.
Create a new file named _blog/<post-url>.md
, e.g _blog/static-website-nginx-ec2.md
The default layout at _layouts/page.html
is quite good, but feel free to modify it.
Live demo: current post
Now that the website is ready, let see how to deploy it to an EC2 instance.
2. Setting up the server
In this section, we will demonstrate how to set up an nginx server, on ubuntu, running on an EC2 instance.
Configuring an EC2 instance
This section assumes that you already have an AWS account, and the rights necessary to start new instances.
I won’t go through the details of starting an EC2 instance, but here are the key points:
- Latest Ubuntu AMI
- Instance type: t2.nano, the cheapest machine available on EC2 (512 Mib ram, 1 virtual CPU), under $5 a month.
- Create a security group. More details below
- Optionally, you can attach an IAM role to this machine to give it some rights, e.g access to S3 without the need to specify credentials.
- Important: create a new SSH key pair, or use an existing one if you are sure you have access to this key.
The security group is important. It is basically a way to white list entries on the firewall of the instance, i.e open ports to the outside world.
Here is the security group config I recommend:
The idea is to open the port 80 and 443 to allow incoming HTTP and HTTPS traffic (respectively), and the port 22 for SSH access. Note that port 22 is open to a single IP only, so that only you can access it. The drawback is that you will have to revisit this setting often if you use a dynamic IP.
I am not quite sure why the interface shows 2 entries for ports 80 and 443. Only one entry per port is needed, with value 0.0.0.0/0
.
If you messed up one of these steps, don’t worry, all you have to do is terminate the instance and start over.
Start the instance and wait for it to finish initializing.
Next, we need to assign a persistent IP to this machine. To do so, we will be using what Amazon calls elastic IPs.
They are easy to allocate from the EC2 dashboard, so go ahead and do it. Then assign it to your newly created instance.
The advantage of using an elastic IP as opposed to using the instance original public IP is that the former is static and can easily be re-allocated to another instance if this one were to be terminated (either by you or if Amazon decides to kill it as it may do sometime). This way, we can safely attach a DNS A record to this IP to route traffic to the instance without fear of having to change it later if the instance dies. We will discuss it in greater details in the 3rd section.
Now that our instance is up and running, let’s ssh in and start configuring it.
You will need the instance ssh private key and the instance IP handy to connect:
When asked if you want to continue, say yes.
On the instance, we need to install nginx as well as a few more packages to deal with S3, which we will use to store the website code:
The instance is now configured. All that is left is to deploy the website’s code to the server and to configure nginx to serve the files.
Deploying code to the server
The easiest way to deploy the code is to copy the files over ssh to the server, e.g:
This is an OK solution for a website that does not need to be updated very often. However I decided to use a variation using S3 because it is a bit cleaner and it also makes it easy to automate the delivery process later on.
So first, let’s upload the code to Amazon S3. As for the instance, we need to make sure python and pip are installed. On Ubuntu, the step above will work. On Mac, you may want to install it with brew or a similar method.
Ubuntu:
Mac:
Then let’s go ahead and create deploy.sh
:
Note: You may need to configure aws cli with your AWS credentials by running aws configure
.
Change the script permissions and run deploy.sh
:
All the files should now be in the bucket.
Next, let’s SSH again into the instance and retrieve the code from the bucket:
Note: if you did not set an IAM role for this instance, you will need to configure the aws cli by running
aws configure
.
Now, every time you want to update the code, all you have to do is re-run deploy.sh
, then
SSH into the instance and sync the files.
One latent advantage of this approach is that it is easy to modify the deployment process to automatically sync the files when the content changes on S3. For example, after building the code, we can create a checksum file and upload it to S3 with the rest of the content.
deploy.sh
:
Then on the instance, create a minutely cron job that checks if the checksum file has changed. If so, run the sync command again. The implementation of the cron job is once again left as an exercise for the reader.
Now that we know how to deploy code to the server, let’s find out how to configure nginx to serve the files.
Configuring nginx
First, let’s list the requirements:
- On the server, the directory
<website-name>/
is the root of the website. All the links are resolved from it. We will need to let nginx know about it. - We want to be able to accept incoming HTTP traffic from port 80 (We will see in the last section how to extend the configuration to support HTTPS on port 443).
- We want
http://<domain>/
to serve index.html - We want
http://<domain>/blog/
to serve the blog index page - We want
http://<domain>/blog/<post-title>.html
to serve the correct page. - We want to be able to serve assets with the correct content-type.
- Everything else returns an HTTP error 404 Not Found
nginx.conf
:
All that is left is to upload this file to the server, e.g via scp
:
Then SSH to the server, copy the config file to the right place and restart nginx.
If something went wrong with the config, you can debug it by running:
If this command shows that the service is running, we are now ready to test it.
Browse to http://<IP>/
You should see your website in all its glory.
3. Registering a domain and routing traffic to the website
Now that we are serving the website, we want to purchase a domain name and direct the traffic to the server. This is a short section, I will mainly point to various useful resources.
I personally like to use Gandi. But there are dozen of other registrars available.
After purchasing a domain, we need to let DNS servers know where to find our server. This is done by adding
a A record
DNS entry.
How to set a DNS record will vary depending on your registrar. The documentation for Gandi is here.
You’ll need to input the following:
- Type:
A
- Name:
*
for naked domains (http://romainstrock.com
) or input the subdomain, e.gwww
- Value: IP of the server
- TTL (time to live): 3 hours or so (i.e no need to re-validate the value more than every 3 hours)
Wait a couple of minutes and you should be able to access the server via the domain name. If the domain used to have a A record pointing somewhere else then it may take longer (up to 48h).
We’ve successfully set up an HTTP server accessible via a domain name, and serving static files.
Last but not least, we need to set up HTTPS to allow secure connections to our website.
4. Setting up HTTPS
Setting up HTTPS for a website used to be a pain, partially because certificates used to be commercial only, and also because setting it up properly is not always straightforward.
Nowadays, free certificates are available thanks to Let’s Encrypt, which brought down the cost of operating a certificate authority by automating all parts of the process. So let’s go ahead and create a certificate with Let’s Encrypt.
The documentation is here. In this guide, they recommend using certbot to easily generate a certificate from the command line.
Let’s first install certbot
. On the server:
Then run the following command and follow the instructions:
When asked about domain names, specify <domain>
and www.<domain>
.
At the end of the process, you should receive 2 files: <domain>.key
and <domain>.pem
.
These files are important and a copy should be kept somewhere safe.
Finally, let see how to modify the nginx config to accept HTTPS connections using these keys.
nginx.conf
:
Restart nginx:
Your website should now expose a very satisfying 🔒 in front of the url bar.
Wrapping up
In this article, we’ve learned:
- How to create a simple static website & blog with Jekyll
- How to deploy this website to an EC2 instance
- How to set up nginx to serve the website files
- How to register a domain and route traffic to the website
- How to set up HTTPS
If you’ve made it this far, well done!
We’ve only scratched the surface of most of the tools we’ve used, but it has been enough to build a simple website. Most of the concepts can be extended and improved, as we’ve seen for example with the automatic update of files by the server in section 2.
The source code of this website is available on Github.
Feel free to drop me an email if you have questions or comments regarding this article.