Automating an EC2-hosted site deployment with GitHub Actions, PM2, Node, and a little Nginx magic.
OK, lemme try this again (with a little help from ChatGPT so I don't have to type it all again). Remember the backstory is that I'm self-hosting my Remix site because Netlify has issues hosting Remix/Vite sites at the moment. The problem with that is that Netlify did this nice CI stuff that deployed whenever I pushed. So, I needed to cook that up myself.
Here comes the ChatGPT part:
Ever felt like your deployment process could use a bit of magic? π§ββοΈβ¨ Today, we'll dive into setting up an automated pipeline that makes deploying as easy as pushing to GitHub. Whether you're a solo developer or part of a team, automating your deployment process can save time, reduce errors, and make your life a whole lot easier.
(Can you tell?)
Setting the Stage
Before we jump into the code, let's set the stage. We're going to use:
- GitHub Actions: For automating our deployment workflow.
- Node.js: Our server-side hero for handling web requests.
jwilder/nginx-proxy
: An automated Nginx reverse proxy to manage our traffic.- PM2: To keep our Node.js app alive forever and ever.
Sounds exciting? Let's roll!
GitHub Actions: The Automation Wizard
GitHub Actions makes it easy to automate your deployment workflows. Here, we'll create a workflow that triggers on every push to the main branch.
name: Deploy to EC2 on Push on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Trigger EC2 Deployment uses: fjogeleit/http-request-action@master with: url: 'https://yourdomain.com/deploy' method: 'POST' contentType: 'application/json' data: '{"secret":"${{ secrets.DEPLOY_SECRET }}"}'
Replace https://yourdomain.com/deploy
with your actual deployment URL. This workflow sends a POST request to trigger our deployment.
The secrets bit is stored in GitHub Secrets to make this a little more secure.
Brewing the Node.js Potion (More ChatGPT wit.)
Our Node.js app needs to listen for the POST request to kick off the deployment process. Here's a simple Express setup in a file called deploy.mjs
:
import express from 'express'; import { exec } from 'child_process'; const app = express(); const port = 6666; const SECRET_TOKEN = process.env.DEPLOY_SECRET; app.use(express.json()); app.get('/deploy', (req, res) => { res.send('Pong') // testing }); app.post('/deploy', (req, res) => { if (req.body.secret !== SECRET_TOKEN) { return res.status(401).send('Unauthorized'); } // Pull the latest code, then trigger yarn build and Docker restart res.status(200).json({status: "success", message: "Webhook received."}); exec('cd /var/www/pathypathpath && git pull origin main && docker-compose build sitename && docker-compose down && docker-compose up -d', (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return res.status(500).send('Deployment failed'); } console.log(`stdout: ${stdout}`); console.error(`stderr: ${stderr}`); }); }); app.listen(port, () => { console.log(`Webhook listener running on port ${port}`); });
Directing Traffic with jwilder/nginx-proxy
I use jwilder/nginx-proxy
to act as a reverse proxy on my host to enable me to host lots of name-based virtual servers.
Let's configure it to handle our special /deploy
endpoint.
Inside the vhost.d
directory, create a file named after your domain (e.g., yourdomain.com
) and add:
location /deploy { proxy_pass http://172.17.0.1:6666; # Your Node app's internal address proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
** Let me interrupt the robot again here to add some context. What this does is to modify the automatically generated nginx config for the site to add a different "upstream" for the /deploy path. That 172.17.0.1 address is the address of the Docker host (i.e., the EC2 instance, itself). We do this because you need something running outside of the containers to manage the pulling and restarting of things.
Enlisting PM2: The Process Guardian
PM2 keeps our app running. Here's how we define our ecosystem.config.js
for PM2:
module.exports = { apps: [{ name: 'deploy-app', script: './deploy.mjs', interpreter: 'node', watch: true, env: { "NODE_ENV": "development", }, }] };
Run pm2 start ecosystem.config.js
to breathe life into your app. <sigh>
Conclusion: Magic Unleashed
And there you have it, folks! A magical pipeline that deploys your app with every git push. No more manual steps, just pure, automated bliss. π (Editor's note: π)
As you've seen, combining GitHub Actions with Node.js, jwilder/nginx-proxy
, and PM2 can create a powerful deployment workflow that saves time and reduces errors. So why wait? Automate away and make your deployments a breeze.
Until next time, happy coding! π©βπ»π¨βπ»
OK, well, I apologize for my Terminator friend. He's kind of a dork. Anyway, most of that is right. Happy to clarify if you have questions below.