A self-hosted deployment of Memos on AWS ECS Fargate, provisioned with Terraform and deployed via GitHub Actions CI/CD.
π Live URL: https://tm.myegh.com
graph TD
User["π€ User"] -->|HTTPS| Route53["Route 53\ntm.myegh.com"]
Route53 --> ALB["Application Load Balancer\nPort 80 β 443 redirect\nACM TLS Certificate"]
ALB -->|Port 8081| ECS["ECS Fargate\nmemos-cluster\nmemos-service"]
ECS --> ECR["ECR\nDocker Image"]
ECS --> CW["CloudWatch Logs\n/ecs/memos"]
GHA["GitHub Actions\nbuild.yml + deploy.yml"] -->|Push image| ECR
GHA -->|terraform apply| Infra["Terraform\nS3 Backend + DynamoDB Lock"]
subgraph VPC ["VPC 10.0.0.0/16"]
subgraph Public ["Public Subnets (eu-west-1a/1b)"]
ALB
end
subgraph Private ["Private Subnets (eu-west-1a/1b)"]
ECS
end
end
| Component | Details |
|---|---|
| Cloud | AWS eu-west-1 |
| Compute | ECS Fargate (256 CPU, 512MB RAM) |
| Container Registry | Amazon ECR |
| Load Balancer | Application Load Balancer |
| TLS | AWS Certificate Manager |
| DNS | Route 53 |
| State Backend | S3 + DynamoDB locking |
| IaC | Terraform (modular) |
| CI/CD | GitHub Actions with OIDC |
infra/
βββ main.tf
βββ variables.tf
βββ outputs.tf
βββ provider.tf
βββ terraform.tfvars
βββ modules/
βββ vpc/ # VPC, subnets, IGW, NAT, route tables
βββ ecs/ # Cluster, task definition, service
βββ alb/ # ALB, target group, listeners
βββ ecr/ # Container registry
βββ acm/ # TLS certificate
βββ iam/ # Execution and task roles
βββ security/ # Security groups
Two separate GitHub Actions workflows:
- Triggers on push to
mainor manualworkflow_dispatch - Authenticates to AWS via OIDC (no static keys)
- Builds Docker image and tags with Git SHA
- Pushes to ECR
- Triggers automatically when
build.ymlcompletes - Runs
terraform fmt,validate,plan,apply - Waits 60s then hits
/healthzβ fails pipeline if unhealthy
| Name | Description |
|---|---|
AWS_ROLE_ARN |
ARN of the IAM role with OIDC trust policy |
- AWS CLI configured
- Terraform >= 1.6.0
- Docker
- A registered domain in Route 53
git clone https://github.com/EnamulRahman/ECS-Project.git
cd ECS-Projectaws s3api create-bucket \
--bucket your-terraform-state-bucket \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
aws s3api put-bucket-versioning \
--bucket your-terraform-state-bucket \
--versioning-configuration Status=Enabled
aws dynamodb create-table \
--table-name terraform-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTEdit infra/terraform.tfvars with your domain, region, and bucket name.
cd infra
terraform init
terraform applyaws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list ffffffffffffffffffffffffffffffffffffffff
aws iam create-role \
--role-name github-actions-role \
--assume-role-policy-document file://oidc-trust-policy.json
aws iam attach-role-policy \
--role-name github-actions-role \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccessAdd AWS_ROLE_ARN to GitHub repository secrets.
Any push to main will trigger the full build and deploy pipeline.
cd infra
terraform destroy- Fargate over EC2 β no server management, scales to zero
- Private subnets for ECS β containers not directly internet accessible, traffic only via ALB
- OIDC over static keys β no long-lived AWS credentials stored in GitHub
- Modular Terraform β each layer isolated and independently manageable
- Multi-stage Dockerfile β separate frontend (Node/pnpm) and backend (Go) build stages for smaller final image









