Deploy containerized web services on AWS Fargate with an Application Load Balancer, using AWS CDK.
Supports custom domains, environment variables, secrets, and CI/CD with GitHub Actions.
- Deploy any web service as a Docker container to AWS Fargate
- Automatic VPC and ECS Cluster creation
- Application Load Balancer (ALB) with public DNS
- Environment variables and AWS Secrets Manager integration
- Custom domain and SSL via Route53 and Certificate Manager
- CI/CD ready with Github Actions
You need an AWS account to create and deploy the required resources for the site on AWS.
Before you begin, make sure you have the following:
-
Node.js and npm: Ensure you have Node.js (v20 or later) and npm installed.
-
AWS CLI: Install and configure the AWS Command Line Interface.
-
AWS CDK: Install the AWS CDK globally
npm install -g aws-cdk
- Before deploying, bootstrap your AWS environment:
cdk bootstrap aws://your-aws-account-id/us-east-1
This package uses the npm package manager and is an ES6+ Module.
You can use Nixpacks to build your container images automatically. To enable Nixpacks, set buildProps.buildSystem to 'Nixpacks' in your WebServiceProps configuration:
const svcProps: WebServiceProps = {
// ... other props
buildProps: {
buildSystem: 'Nixpacks',
installcmd: 'pnpm install', // optional
buildcmd: 'pnpm run build', // optional
startcmd: 'pnpm start', // optional
environment: [
{ NODE_ENV: 'production' }
],
secrets: [
{ key: 'DB_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_URL-abc123' }
]
},
};When enabled, the pipeline will:
- Install Nixpacks in the CodeBuild environment
- Generate a Dockerfile using Nixpacks
- Build and push the image to ECR
- Deploy the image to ECS Fargate
You can customize install, build, and start commands using the respective properties. If not set, Nixpacks will auto-detect the best commands for your project.
For more advanced usage, see the Nixpacks documentation.
Navigate to your project directory and install the package and its required dependencies:
npm i tsx @thunderso/cdk-webservice --save-dev-
Login into the AWS console and note the
Account ID. You will need it in the configuration step. -
Run the following commands to create the required CDK stack entrypoint at
stack/index.ts.
mkdir stack
cd stack
touch index.ts You should adapt the file to your project's needs.
Note
Use different filenames such as production.ts and dev.ts for environments.
// stack/index.ts
import { Cdk, WebServiceStack, type WebServiceProps } from "@thunderso/cdk-webservice";
const svcProps: WebServiceProps = {
env: {
account: 'your-account-id',
region: 'us-west-2'
},
application: 'your-application-id',
service: 'your-service-id',
environment: 'production',
rootDir: '', // supports monorepos. e.g. app/
// ... other props
};
new WebServiceStack(
new Cdk.App(),
`${svcProps.application}-${svcProps.service}-${svcProps.environment}-stack`,
svcProps
);Run npm run build before you deploy.
By running the following script, the CDK stack will be deployed to AWS.
npx cdk deploy --all --app="npx tsx stack/index.ts" After deployment, the stack outputs the public DNS name of the Application Load Balancer:
Outputs:
LoadBalancerDNS: <your-load-balancer-url>
In your GitHub repository, add a new workflow file under .github/workflows/deploy.yml with the following content:
name: Deploy WebService to AWS
on:
push:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run build # if you have a build step
- run: npx cdk deploy --require-approval never --all --app="npx tsx stack/index.ts"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as repository secrets in GitHub.
If you want to destroy the stack and all its resources (including storage, e.g., access logs), run the following script:
npx cdk destroy --all --app="npx tsx stack/index.ts" - Create a hosted zone in Route53 for the desired domain, if you don't have one yet.
This is required to create DNS records for the domain to make the app publicly available on that domain. On the hosted zone details you should see the Hosted zone ID of the hosted zone.
- Request a public regional certificate in the AWS Certificate Manager (ACM) for the desired domain in the same region as the service and validate it, if you don't have one yet.
This is required to provide the app via HTTPS on the public internet. Take note of the displayed ARN for the certificate.
Important
The certificate must be issued in the same region as the service.
// stack/index.ts
const svcProps: WebServiceProps = {
// ... other props
domain: 'api.example.com',
hostedZoneId: 'XXXXXXXXXXXXXXX',
regionalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-abcd-1234-abcd-1234abcd1234',
};Each configuration property provides a means to fine-tune the performance and operational characteristics.
// stack/index.ts
import { CpuArchitecture } from 'aws-cdk-lib/aws-ecs';
const svcProps: WebServiceProps = {
// ... other props
serviceProps: {
// ... other props
dockerFile: 'Dockerfile',
architecture: CpuArchitecture.ARM64,
desiredCount: 1,
cpu: 256,
memorySize: 512,
port: 3000,
},
};Path to your Dockerfile relative to your project directory.
- Type:
text - Examples:
Dockerfile.bun,dir/Dockerfile, - Default: Looks for a
Dockerfilein the root directory.
Defines the instruction set architecture for the task.
- Type:
CpuArchitecture - Examples:
CpuArchitecture.ARM64,CpuArchitecture.X86_64 - Default: The architecture defaults to
CpuArchitecture.ARM64.
Specifies the number of container instances of your service to run.
- Type:
number - Examples:
1,2,3 - Default:
1
The amount of CPU units to allocate to the container (per task).
- Type:
number - Examples:
256,512,1024 - Default:
256
The amount of memory (in MiB) to allocate to the container (per task).
- Type:
number - Examples:
512,1024,2048 - Default:
512
The port number on which your container listens for incoming traffic.
- Type:
number - Examples:
4321,3000,8080 - Default:
3000
Pass environment variables to your service by:
-
variables: Array of key-value pairs for plain environment variables. -
secrets: Array of objects withkeyandresource(Secrets Manager ARN). The library automatically adds permissions for Lambda to read these secrets.
To create a plaintext secret in AWS Secrets Manager using the AWS CLI:
aws secretsmanager create-secret --name "your-secret-name" --secret-string "your-secret-value"// stack/index.ts
const svcProps: WebServiceProps = {
serviceProps: {
// ... other props
variables: [
{ PUBLIC_FRONTEND_URL: 'https://example.com' },
{ PUBLIC_ANALYTICS_ID: 'UA-XXXXXX' }
],
secrets: [
{
key: 'DB_URL',
resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_URL-abc123'
},
{
key: 'DB_KEY',
resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_KEY-def456'
},
],
},
};Here is an example Dockerfile which can serve any Node application:
# ---- Build Stage ----
FROM public.ecr.aws/docker/library/node:22-alpine AS builder
WORKDIR /app
# Copy all files
COPY . .
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Install dependencies
RUN pnpm install
# Build Next.js app
RUN pnpm run build
# ---- Production Stage ----
FROM public.ecr.aws/docker/library/node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Install pnpm in production image
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy only necessary files from builder
COPY --from=builder /app/ ./
EXPOSE 3000
CMD ["pnpm", "start"]You might want to add a .dockerignore to minimize the context size:
.DS_Store
.git
.gitignore
node_modules
stack
cdk.out
Below are estimated AWS costs for a typical deployment of this stack.
Example: Next.js container in us-east-1 with default configuration.
| Component | Monthly Cost (No Free Tier) |
|---|---|
| Fargate (1 task) | ~$9 |
| ALB | ~$22 |
| ECR | $0 (first 500MB free) |
| VPC/Subnets | $0 |
| CloudWatch Logs | <$1 |
| Route53 | $0.50 |
| ACM (SSL) | $0 |
| Data Transfer | $0 (first 100GB free) |
| Total | ~$33/month |
- With Free Tier: $0 (if eligible, for 12 months, and under limits)
- Without Free Tier: ~$33/month
Assumptions:
- Each user visits once/day, 5 pageviews, 500KB/page (2.5MB/user/day)
- Total data: 2.5GB/day, ~75GB/month
- ALB: 1 LCU covers up to 25 new connections/sec, 3,000 active connections, 5Gbps, or 1M requests/hour (well within 1 LCU for 1000 DAU)
- 2 Fargate tasks for redundancy
| Component | Monthly Cost (No Free Tier) |
|---|---|
| Fargate (2 tasks) | ~$18 |
| ALB | ~$22 |
| ECR | $0 |
| VPC/Subnets | $0 |
| CloudWatch Logs | <$1 |
| Route53 | $0.50 |
| ACM (SSL) | $0 |
| Data Transfer | ~$6.75 (75GB @ $0.09/GB) |
| Total | ~$47/month |
- With Free Tier: ~$41/month (if still eligible, and under 100GB data out)
- Without Free Tier: ~$47/month
- These are estimates; actual costs may vary based on usage, region, and AWS pricing changes.
- If you add more tasks, NAT gateways, or use more storage/logs, costs will increase.
- For production, consider scaling, backups, and monitoring costs.
For help or feature requests, open an issue on GitHub.