Launching a Containerized Application on AWS EC2 with Github Actions for Simple Deployment
This post walks through the basics of setting up an EC2 instance to run a Docker project for a Web server. This is a barebones setup for small projects. I recently went through setting up an EC2 Instance for my Marketing API project. The project is written in Python (with FastAPI) on an M1 Mac and I used Docker to build the image on the server so that it defaulted to the correct hardware. The project also includes a simple Github Action that will automatically deploy any time new code is pushed to main. In a future post I may attempt a simple testing suite as a set in this deployment action.
Configure the EC2 Instance
Create an account and walk through the basic EC2 instance configuration. All code snippets are based on using Amazon Linux which is compatible with the AWS free tier.
On your local machine, store the .pem file in ~/.ssh and set permissions
chmod 400 "your-key.pem"
Setup EC2 Instance
SSH into the instance
ssh -i "~/.ssh/your-key.pem" username@your-public-dns
Update everything on the server
sudo yum update
sudo yum upgrade
Setup Docker
Install docker and start it/set it up to start when the server starts up.
sudo yum install docker -y
sudo systemctl start docker
sudo systemctl enable docker
Build Docker Image
This command builds the docker image with a tag -t that you specify. It will build the image based on the code at your GIT_REPO_LINK.
sudo docker build -t DOCKER_IMAGE_NAME GIT_REPO_LINK
Optional: Test Code is Working
Run the container
sudo docker run -p 8000:8000 --name CONTAINER_NAME IMAGE_NAME
Stop and Remove Container
sudo docker stop CONTAINER_NAME
sudo docker rm CONTAINER_NAME
Create systemd Service File
The service file allows these scripts to be run at startup and run in the background.
Create a service file for your service
sudo vim /etc/systemd/system/NAME_HERE.docker.service
Sample systemd Service File
[Unit]
Description=Marketing API Container
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
Restart=always
ExecStart=/usr/bin/sudo /usr/bin/docker run -p 8000:8000 --name CONTAINER_NAME IMAGE_NAME
[Install]
WantedBy=multi-user.target
Reload systemd and start service.
sudo systemctl daemon-reload
sudo systemctl enable NAME_HERE.docker.service
sudo systemctl start NAME_HERE.docker.service
Install Caddy for Reverse Proxy
Install caddy to setup a reverse proxy to handle network traffic.
sudo yum -y install yum-plugin-copr
sudo yum -y copr enable @caddy/caddy epel-7-$(arch)
sudo yum -y install caddy
sudo vim /etc/caddy/Caddyfile
Setup A Record Specify your domain where it says mydomain.com. Make sure to go to your hosting platform and setup an A record for this domain that maps to the Public IP Address for your EC2 Instance
Setup Caddyfile to configure your domain.
mydomain.com { reverse_proxy localhost:8000 }
sudo systemctl restart caddy
Setting up Github Deployment Action
We are going to setup a Github Action to automatically fetch the new code and build the image. This is not a production grade solution but for small projects we are ssh-ing into the server and running these two commands
This is the script we are going to be including in our action to trigger the updates. Here we build a new image with the same name from the Git repository. Now we have the image containing the new code. using the -f flag on docker rm will stop the current container which since the service file has restart always setup, will trigger the docker container to run again (which will require it to pull the new image).
sudo docker build -t DOCKER_IMAGE_NAME GIT_REPO_LINK
sudo docker rm -f DOCKER_CONTAINER_NAME
sudo systemctl restart NAME_HERE.docker.service
Build the Github Action
See the Github Actions Quickstart for details on the basic syntax of this file. Basically we are setting up a trigger on pushes to main and pull_requests. This trigger will run the appleboy ssh action to ssh into our EC2 instance. From there we just run the two docker commands to rebuild our container.
name: Deployment
run-name: ${{ github.actor }} is pushing changes to production
concurrency: production
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
deployment:
name: Deploy to EC2
runs-on: ubuntu-latest
steps:
- name: Build and Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PASSWORD }}
script: |
sudo docker build -t YOUR_IMAGE_NAME YOUR_GIT_REPO
sudo docker rm -f YOUR_CONTAINER_NAME
sudo systemctl restart NAME_HERE.docker.service