AWS Compute Blog
Building a pocket platform-as-a-service with Amazon Lightsail
This post was written by Robert Zhu, a principal technical evangelist at AWS and a member of the GraphQL Working Group.
When you start a new web-based project, you need lightweight infrastructure that addresses your immediate needs. For my projects, I prioritize simplicity, flexibility, value, and on-demand capacity, and find myself quickly needing the following features:
- DNS configuration
- SSL support
- Subdomain to a service
- SSL reverse proxy to localhost (similar to ngrok and serveo)
- Automatic deployment after a commit to the source repo (nice to have)
Amazon Lightsail is perfect for building a simple “pocket platform” that provides all these features. It’s cheap and easy for beginners and provides a friendly interface for managing virtual machines and DNS. This post shows step-by-step how to assemble a pocket platform on Amazon Lightsail.
Walkthrough
The following steps describe the process. If you prefer to learn by watching videos instead, view the steps by watching the following: Part 1, Part 2, Part 3.
Prerequisites
You should be familiar with: Linux, SSH, SSL, Docker, Nginx, HTTP, and DNS.
Steps
Use the following steps to assemble a pocket platform.
Creating a domain name and static IP
First, you need a domain name for your project. You can register your domain with any domain name registration service, such as Amazon Route53.
- After your domain is registered, open the Lightsail console, choose the Networking tab, and choose Create static IP.
- On the Create static IP page, give the static IP a name you can remember and don’t worry about attaching it to an instance just yet. Choose Create DNS zone.
- On the Create a DNS zone page, enter your domain name and then choose Create DNS zone. For this post, I use the domain “raccoon.news.
- Choose Add Record and create two A records—“@.raccoon.news” and “raccoon.news”—both resolving to the static IP address you created earlier. Then, copy the values for the Lightsail name servers at the bottom of the page. Go back to your domain name provider, and edit the name servers to point to the Lightsail name servers. Since I registered my domain with Route53, here’s what it looks like:
Note: If you registered your domain with Route53, make sure you change the name server values under “domain registration,” not “hosted zones.” If you registered your domain with Route53, you need to delete the hosted zone that Route53 automatically creates for your domain.
Setting up your Lightsail instance
While you wait for your DNS changes to propagate, set up your Lightsail instance.
- In the Lightsail console, create a new instance and select Ubuntu 18.04.
For this post, you can use the cheapest instance. However, when you run anything in production, make sure you choose an instance with enough capacity for your workload.
- After the instance launches, select it, then click on the Networking tab and open two additional TCP ports: 443 and 2222. Then, attach the static IP allocated earlier.
- To connect to the Lightsail instance using SSH, download the SSH key, and save it to a friendly path, for example: ~/ls_ssh_key.pem.
- Restrict permissions for your SSH key:
chmod 400 ~/ls_ssh_key.pem
- Connect to the instance using SSH:
ssh -i ls_ssh_key.pem ubuntu@STATIC_IP
- After you connect to the instance, install Docker to help manage deployment and configuration:
sudo apt-get update && sudo apt-get install docker.io
sudo systemctl start docker
sudo systemctl enable docker
docker run hello-world
- After Docker is installed, set up a gateway called the nginx-proxy container. This container lets you route traffic to other containers by providing the “VIRTUAL_HOST” environment variable. Conveniently, nginx-proxy comes with an SSL companion, nginx-proxy-letsencrypt, which uses Let’s Encrypt.
# start the reverse proxy container
sudo docker run --detach \
--name nginx-proxy \
--publish 80:80 \
--publish 443:443 \
--volume /etc/nginx/certs \
--volume /etc/nginx/vhost.d \
--volume /usr/share/nginx/html \
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
jwilder/nginx-proxy
# start the letsencrypt companion
sudo docker run --detach \
--name nginx-proxy-letsencrypt \
--volumes-from nginx-proxy \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--env "DEFAULT_EMAIL=YOUREMAILHERE" \
jrcs/letsencrypt-nginx-proxy-companion
# start a demo web server under a subdomain
sudo docker run --detach \
--name nginx \
--env "VIRTUAL_HOST=test.EXAMPLE.COM" \
--env "LETSENCRYPT_HOST=test.EXAMPLE.COM" \
nginx
Pay special attention to setting a valid email for the DEFAULT_EMAIL environment variable on the proxy companion; otherwise, you’ll need to specify the email whenever you start a new container. If everything went well, you should be able to navigate to https://test.EXAMPLE.COM and see the nginx default content with a valid SSL certificate that has been auto-generated by Let’s Encrypt.
Troubleshooting:
- In the Lightsail console, make sure that Port 443 is open.
- Let’s Encrypt rate limiting (for reference if you encounter issues with SSL certificate issuance): https://letsencrypt.org/docs/rate-limits/.
Deploying a localhost proxy with SSL
Most developers prefer to code on a dev machine (laptop or desktop) because they can access the file system, use their favorite IDE, recompile, debug, and more. Unfortunately, developing on a dev machine can introduce bugs due to differences from the production environment. Also, certain services (for example, Alexa Skills or GitHub Webhooks) require SSL to work, which can be annoying to configure on your local machine.
For this post, you can use an SSL reverse proxy to make your local dev environment resemble production from the browser’s point of view. This technique also helps allow your test application to make API requests to production endpoints with Cross-Origin Resource Sharing restrictions. While it’s not a perfect solution, it takes you one step closer toward a frictionless dev/test feedback loop. You may have used services like ngrok and serveo for this purpose. By running a reverse proxy, you won’t need to spread your domain and SSL settings across multiple services.
To run a reverse proxy, create an SSH reverse tunnel. After the reverse tunnel SSH session is initiated, all network requests to the specified port on the host are proxied to your dev machine. However, since your Lightsail instance is already using port 22 for VPS management, you need a different SSH port (use 2222 from earlier). To keep everything organized, run the SSH server for port 2222 inside a special proxy container. The following diagram shows this solution.
Using Dockerize an SSH service as a starting point, I created a repository with a working Dockerfile and nginx config for reference. Here are the summary steps:
git clone https://github.com/robzhu/nginx-local-tunnel
cd nginx-local-tunnel
docker build -t {DOCKERUSER}/dev-proxy . --build-arg ROOTPW={PASSWORD}
# start the proxy container
# Note, 2222 is the port we opened on the instance earlier.
docker run --detach -p 2222:22 \
--name dev-proxy \
--env "VIRTUAL_HOST=dev.EXAMPLE.com" \
--env "LETSENCRYPT_HOST=dev.EXAMPLE.com" \
{DOCKERUSER}/dev-proxy
# Ports explained:
# 3000 refers to the port that your app is running on localhost.
# 2222 is the forwarded port on the host that we use to directly SSH into the container.
# 80 is the default HTTP port, forwarded from the host
ssh -R :80:localhost:3000 -p 2222 root@dev.EXAMPLE.com
# Start sample app on localhost
cd node-hello && npm i
nodemon main.js
# Point browser to https://dev.EXAMPLE.com
The reverse proxy subdomain works only as long as the reverse proxy SSH connection remains open. If there is no SSH connection, you should see an nginx gateway error:
While this solution is handy, be extremely careful, as it could expose your work-in-progress to the internet. Consider adding additional authorization logic and settings for allowing/denying specific IPs.
Setting up automatic deployment
Finally, build an automation workflow that watches for commits on a source repository, builds an updated container image, and re-deploys the container on your host. There are many ways to do this, but here’s the combination I’ve selected for simplicity:
- First, create a GitHub repository to host your application source code. For demo purposes, you can clone my express hello-world example. On the Docker hub page, create a new repository, click the GitHub icon, and select your repository from the dropdown list.
- Now Docker watches for commits to the repo and builds a new image with the “latest” tag in response. After the image is available, start the container as follows:
docker run --detach \
--name app \
--env "VIRTUAL_HOST=app.raccoon.news" \
--env "LETSENCRYPT_HOST=app.raccoon.news" \
robzhu/express-hello
- Finally, use Watchtower to poll dockerhub and update the “app” container whenever a new image is detected:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower \
--interval 10 \
APPCONTAINERNAME
Summary
Your Pocket PaaS is now complete! As long as you deploy new containers and add the VIRTUAL_HOST and LETSENCRYPT_HOST environment variables, you get automatic subdomain routing and SSL termination. With SSH reverse tunneling, you can develop on your local dev machine using your favorite IDE and test/share your app at https://dev.EXAMPLE.COM.
Because this is a public URL with SSL, you can test Alexa Skills, GitHub Webhooks, CORS settings, PWAs, and anything else that requires SSL. Once you’re happy with your changes, a git commit triggers an automated rebuild of your Docker image, which is automatically redeployed by Watchtower.
I hope this information was useful. Thoughts? Leave a comment or direct-message me on Twitter: @rbzhu.