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