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
-
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.
-
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!