Day 3: Seamless Deployment — Hosting on Lightsail Container Service and Automating with GitHub Actions

n-tien
May 01, 2025
7 mins read
Day 3: Seamless Deployment — Hosting on Lightsail Container Service and Automating with GitHub Actions

Seamless Deployment: Hosting on Lightsail Container Service and Automating with GitHub Actions

📢 If you haven't read the earlier parts of our 7 Days Website Revamp series, catch up here:

📚 The entire series is now live—explore all 7 posts to follow our full journey from start to finish!


Why We Chose Lightsail Container Service

Instead of using traditional virtual machines, we chose AWS Lightsail Container Service to deploy our Next.js application.

Key reasons:

  • Managed container hosting without full server maintenance
  • Built-in domain and SSL support
  • Simple horizontal scaling by adjusting container service settings
  • Affordable and predictable pricing

This decision allowed us to focus on building and deploying features rather than server management.


Provisioning Lightsail Container Service with Terraform

To automate and version control our infrastructure, we leveraged Terraform.

Here’s a simplified version of how we provisioned our resources:

1. Lightsail Container Service

resource "aws_lightsail_container_service" "atware_home" {
  name  = "atware-asia-home"
  power = "medium"
  scale = 1

  private_registry_access {
    ecr_image_puller_role {
      is_active = true
    }
  }
}

We configured:

  • Power: medium — enough for production traffic
  • Scale: 1 — single replica to start
  • Private registry access: enabled for secure ECR pulling

2. Container Deployment Version

resource "aws_lightsail_container_service_deployment_version" "latest" {
  container {
    container_name = "next"
    # I created an ECR repository manually and used it here using a data block; you can still create it here.
    image          = "${data.aws_ecr_repository.atware_home.repository_url}:${var.image_version}"

    environment = {
      # needed ENV
    }

    ports = {
      3000 = "HTTP"
    }
  }

  public_endpoint {
    container_name = "next"
    container_port = 3000

    health_check {
      healthy_threshold   = 2
      unhealthy_threshold = 2
      timeout_seconds     = 2
      interval_seconds    = 5
      path                = "/api/health"
      success_codes       = "200"
    }
  }

  service_name = aws_lightsail_container_service.atware_home.name
}

✅ This configuration ensured:

  • Health checks were enabled (/api/health)
  • Public endpoint exposed on port 3000
  • Dynamic environment variables were injected

Automating Deployment with GitHub Actions

To automate our deployment workflow, we used GitHub Actions to:

  • Build a Docker image of our Next.js application
  • Push the image to AWS ECR
  • Update the AWS Lightsail Container Service automatically
  • Apply updated infrastructure changes using Terraform

This setup allowed seamless deployment every time we pushed new code to the main branch.

Overview of the Deployment Workflow

  1. Build and Push Docker Image The GitHub Action automates the process by building the Docker image from the Next.js project, tagging it with the Git SHA, and securely pushing it to AWS ECR.

  2. Deploy Infrastructure with Terraform Once the new image is pushed, Terraform re-applies the infrastructure changes to update the deployment version on Lightsail containers.

GitHub Actions Workflow (Simplified)

# File: .github/workflows/deploy.yml
name: Deploy

permissions:
  id-token: write # Required for requesting the JWT
  contents: read # Required for actions/checkout

on:
  push:
    branches: ["main"]

jobs:
  deploy-app:
    name: Deploy app
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build and Push Docker Image
        uses: ./.github/actions/deploy-to-ecr
        with:
          role-to-assume: arn:aws:iam::123456xxx000:role/role-github-runner
          aws-region: ap-southeast-1
          aws-account-id: 123456xxx000
          image-name: atware-home
          git-sha: ${{ github.sha }}
          site-url: https://atware.asia

  deploy-infra:
    name: Deploy infrastructure
    runs-on: ubuntu-latest
    needs: deploy-app

    steps:
      - uses: actions/checkout@v4

      - name: Deploy with Terraform
        uses: ./.github/actions/terraform-env-action
        with:
          role-to-assume: arn:aws:iam::123456xxx000:role/role-github-runner
          working-directory: "./infra"
          apply: "yes"
          image_version: ${{ github.sha }}

Supporting Composite Actions

Our deployment workflow uses two custom composite GitHub Actions to keep the main pipeline simple and maintainable.

1. deploy-to-ecr: Build and Push Docker Image to AWS ECR

This action handles:

  • Authenticating to AWS using GitHub OIDC
  • Building the Docker image
  • Tagging the image with Git SHA and latest
  • Pushing the image to AWS ECR

Simplified action steps:

# File: .github/actions/deploy-to-ecr/action.yml
name: "Deploy to ECR"

inputs:
  role-to-assume:
    required: true
  aws-region:
    required: true
  aws-account-id:
    required: true
  image-name:
    required: true
  git-sha:
    required: true
  site-url:
    required: true

runs:
  using: "composite"
  steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ inputs.role-to-assume }}
        aws-region: ${{ inputs.aws-region }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: .
        push: true
        tags: |
          ${{ inputs.aws-account-id }}.dkr.ecr.${{ inputs.aws-region }}.amazonaws.com/${{ inputs.image-name }}:${{ inputs.git-sha }}
          ${{ inputs.aws-account-id }}.dkr.ecr.${{ inputs.aws-region }}.amazonaws.com/${{ inputs.image-name }}:latest

2. terraform-env-action: Apply Terraform Infrastructure Changes

This action handles:

  • Configuring AWS credentials
  • Running terraform init
  • Running terraform plan or terraform apply
  • Passing dynamic environment variables into Terraform

Simplified action steps:

# File: .github/actions/terraform-env-action/action.yml
name: "Terraform by Environment"

inputs:
  role-to-assume:
    required: true
  aws-region:
    default: "ap-southeast-1"
  apply:
    default: "no"
  working-directory:
    required: true
  image_version:
    required: true

runs:
  using: "composite"
  steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ inputs.role-to-assume }}
        aws-region: ${{ inputs.aws-region }}

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    - name: Terraform Init
      run: terraform init
      working-directory: ${{ inputs.working-directory }}

    - name: Terraform Plan or Apply
      run: |
        if [[ "${{ inputs.apply }}" == "yes" ]]; then
          terraform apply -auto-approve
        else
          terraform plan
        fi
      env:
        # If you need any env for Terraform, you can customize them here
        # TF_VAR_xxx: ${{ inputs.xxx }}
      working-directory: ${{ inputs.working-directory }}

These two composite actions help keep the main deployment workflow clean and modular, while managing all the heavy lifting behind the scenes.


Domain Mapping and SSL (Note)

After setting up the container service, we mapped it to our domain atware.asia using Lightsail's DNS management features.

We also configured SSL certificates separately to ensure secure HTTPS access. (We won't dive into SSL setup details here — this article focuses mainly on deployment automation.)


Key Insights and Best Practices

  • Managing Secrets Securely: Using GitHub Secrets and AWS IAM roles correctly was critical to protecting our ECR, Lightsail, and Terraform resources.

  • Container Health Checks: Setting up a proper health check (/api/health) inside the Lightsail deployment ensured smooth rollouts without service downtime.

  • Terraform Modularization: Splitting Terraform into clean, reusable modules helped us scale and update infrastructure safely.

  • Minimal Downtime Deployments: Utilizing health checks in Lightsail Container Service deployments enabled us to achieve almost zero downtime, even during new releases.


Final Thoughts

Using AWS Lightsail Container Service combined with Terraform and GitHub Actions allowed us to modernize our deployment pipeline:

  • Fully automated builds and deployments
  • Reproducible, version-controlled infrastructure
  • Scalable, containerized production hosting
  • Minimal operational overhead

This setup not only improved our delivery speed but also aligned perfectly with the best DevOps practices.


In the next article, we’ll dive into creating a smart Contact Page where user form submissions are directly sent to Slack, bypassing traditional email servers! Read it here: Day 4: Building a Smart Contact Page with Slack Webhooks

📚 You can also explore all posts from the 7 Days Website Revamp series to follow our full journey!