Deploy Dockerized Application To AWS Elastic Beanstalk

Ben Tran
9 min readNov 1, 2023

--

In this tutorial, I will provide a step-by-step walkthrough on deploying a dockerized application (NestJS API + PostgreSQL database) to AWS Elastic Beanstalk (AWS EB).

Don’t have much deep explanation of related services, so you should have fundamental understanding of IAM, EC2 (Load Balancing, Auto Scaling), RDS, S3, CloudWatch, CodePipeline, Route53, Certificate Manager, …

This guide includes 3 parts:

You can read more about AWS Elastic Beanstalk infrastructure here:

Image from docs.aws.amazon.com

Manage IAM user permissions

Open IAM — Users Console (use root or iam user have sufficient permissions), then Create User with following permissions — it is recommended to assign these permissions within a user group:

IAMOwnedView custom inline policy content:

ServiceRoleManagement custom inline policy content (ref document):

Manage AWS EB instance profiles (ref document)

Open IAM — Roles Console, choose Create Role:

Add permissions policies:

  • AmazonEC2ContainerRegistryReadOnly
  • AWSElasticBeanstalkMulticontainerDocker
  • AWSElasticBeanstalkWebTier
  • AWSElasticBeanstalkWorkerTier

Set the role name aws-elasticbeanstalk-ec2-role , then save it.

Manage AWS EB service roles (ref document)

Open IAM— Roles Console, choose Create Role:

Add permissions policies:

  • AWSElasticBeanstalkEnhancedHealth
  • AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy
  • AWSElasticBeanstalkService

Set the role name to aws-elasticbeanstalk-service-role, then save it.

Setup CLIs & AWS profiles

Install awscli, awsebcli in macos:

brew update

# install awscli
brew install awscli
aws --version

# install awsebcli
brew install awsebcli
eb --version

Add aws profile to your awscli:

# Use: aws configure --profile AWS_PROFILE
aws configure --profile crewcall

Then, fill in your Access key ID, Secret access key, Region— use the IAM account we already created in previous steps.

Suggestion: I recommend using the project name for your AWS profile. This approach simplifies the management of multiple AWS profiles on your local machine, you won’t have to memorize which profile is used for each project.

FYI: aws profiles are stored in ~/.aws/credentials and ~/.aws/config. This makes it easy to update your profile by modifying these files.

Build Docker image

In project root path, add Dockerfile file with the following content:

Add .dockerignore file:

Ensure your docker image is successfully built before proceeding to the next step:

docker build --target production -t crewcall-api-production:latest .

Create AWS ECR Repository

Open ECR dashboard, create new private repository:

After creating, on the repository page you can find the push commands under View push commands. Moreover, you have the ability to view all the images that have been pushed.

Push your Docker image to ECR Repository

Retrieve an authentication token and authenticate your docker client to your registry:

aws ecr get-login-password --profile crewcall --region ap-southeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-southeast-1.amazonaws.com

Build your docker image using the following command. You can skip this step if your image is already built:

docker build --target production -t crewcall-api-production:latest .

After the build completes, create new tag for your image:

docker tag crewcall-api-production:latest xxxxxxxxxxxx.dkr.ecr.ap-southeast-1.amazonaws.com/crewcall-api-production:latest

Push this image to your newly created AWS repository:

docker push xxxxxxxxxxxxx.dkr.ecr.ap-southeast-1.amazonaws.com/crewcall-api-production:latest

Suggestion: To reduce costs on ECR storage, you should configure a Lifecycle Policy that automatically deletes previous images. Let’s establish the rules that remove all untagged images & keep only the most recent 5 images.

Build docker-compose.yml

AWS Elastic Beanstalk with Docker environment require to have docker-compose.yml to run containers base on defined services. By default, EB will forward all requests to the machine directly to PORT 80. Therefore, your docker-compose.yml must include a mapping to port 80 on the host.

In my scenario, there are two services:

  • api: primary NestJS application.
  • nginx: reverse proxy support to serve public static files.

Finally, ensure that your docker-compose.yml can execute without any issues on your local system: set environment variables, then run docker-compose up.

Setup PostgreSQL database with AWS RDS

Although EB does have integrated functionality to create an RDS instance, I believe it’s more straightforward if you set it up manually.

Follow the official document to create PostgreSQL database instance here. While creating, you need to pay attention to these settings:

  • Availability and durability: Depending on your application requirements, you can choose Multi-AZ DB instance (with one primary database instance) or Multi-AZ DB Cluster (with one primary database instance + 2 readable standby database instances).
  • Credentials Settings: Provide the username and password for the database.
  • DB instance class: Choose the database instance class. You can view the available classes in the list provided here: https://aws.amazon.com/rds/instance-types.
  • Virtual private cloud (VPC): Choose Default VPC. Alternatively, you can use your own VPC if you have one.
  • Public access: Choose No. Only Amazon EC2 instances and other resources inside the VPC can connect to your cluster.
  • VPC security group (firewall): Choose Create New then provide the security group name.
  • Additional configuration: Set Initial database name to ask RDS create it automatically. If not specified, you will need to create the database manually.

After completed, you can get the connection endpoint and port in the main page:

Init EB environment with sample application

Go to AWS EB dashboard, create new environment with Docker platform.

Configure service access: Select AWSEB service role and instance profile we already created before.

Set up networking, database, and tags:

  • Use default VPC (you can use your own VPC if you have one).
  • Select all instance subnets & active public IP address.
  • Regarding the database, you can ignore it as we have already configured it previously.

Configure instance traffic and scaling:

  • EC2 security groups: Leave it blank, EB will generate default security groups for the environment.
  • Auto scaling group: Choose Load balanced environment type; Set Min — Max instances (set Min == Max should disable auto scaling trigger); Select instance types based on your application requirements (sufficient vCPUs and memory); Setup scaling trigger (metric on average CPUUtilization, ≥ 70% will trigger to add 1 more instance, ≤ 30% will trigger to remove 1 instance).
  • Load balancer network settings: Set visibility to Public, select all available load balancer subnets.
  • Load Balancer Type: Choose Application load balancer.
  • Health Check Process: Default is / path.

Configure updates, monitoring and logging:

  • Deployment Policy: Use Rolling policy for zero downtime deployment. Read more about deployment policies in this document.
  • Proxy server: Choose none . We already have nginx setup in docker compose.
  • Instance log streaming to CloudWatch logs: Activated, select retention and lifecycle method.
  • Environment properties: Skip for sample application, we can make changes later.

Then, double-check the configurations before proceeding to create and submit.

After creation is finished, you will have an environment dashboard similar to this one. You can visit the environment domain to see if the sample application can run successfully.

Viewing logs: You can check all deploy process, application run logs in CloudWatch — Log groups, mostly should focus on:

  • /var/log/eb-engine.log — aws eb activity logs.
  • /var/log/eb-docker/containers/eb-current-app/stdouterr.log — all docker container logs.

Stream the logs in local machine:

aws logs tail /aws/elasticbeanstalk/crewcall-api-production/var/log/eb-docker/containers/eb-current-app/stdouterr.log --follow --profile crewcall

You can read more about viewing logs of EB environment in from aws offical document.

Environment configuration: You can find or change any EB environment settings here.

Get main security group name of instances in the environment. Any resources used in the application (like RDS PostgreSQL, ElasticCache — Redis, …) need to have Inbound traffic rules with source to this security group.

Deploy your application to created EB environment

Edit RDS database security group Inbound rules: Choose Custom source, then add EC2 Security Groups of the EB environment to allowed list.

Add required application ENVs to EB environment:

  • Open environment Configuration
  • Edit Updates, monitoring, and logging
  • In Environment properties section, set all required environment variables, then Apply.

Run eb init in your project directory to initialize an awsebcli repository: Choose the appropriate region and application to use, and disable CodeCommit.

eb init --profile crewcall

Notes: This command generate changes the .gitignore. Please ensure to commit your changes.

Begin deploying your application to the EB environment:

# eb deploy EB_ENVIRONMENT --profile AWS_PROFILE
eb deploy crewcall-api-production --profile crewcall

Wait until the deployment process is fully completed, and then recheck. If have any errors during deployment, you can investigate them using CloudWatch — Log groups.

SSH access to EC2 instances — required to enable public IP Address in instance settings:

# Setup keypair to instances
eb ssh crewcall-api-production --profile crewcall --setup

# Connect to EC2 instances with created keypair
eb ssh crewcall-api-production --profile crewcall

# Give root permission if you want to use docker commands
sudo su
docker ps
docker exec -it container_id_or_name sh

# Most of service logs can be found in the /var/log directory.
cd /var/log

# View docker container logs
tail -f /var/log/eb-docker/containers/eb-current-app/eb-stdouterr.log

# View eb activity logs
tail -f /var/log/eb-engine.log

Smaller bundle zip file — optional:

Normally, awsebcli use git archive to create bundle zip file — it excludes files specified in .gitignore and .git directory. However, with Docker platform, we need only docker-compose.yml and its related configuration files. To manage this, add .ebignore file with the following content.

Commit changes, then redeploy the application.

Set application version lifecycle — optional: When you deploy code, EB creates a new application version in zip format and saves it in S3. To reduce S3 storage costs, we should restrict number of stored versions. Like ECR lifecycle policy, I keep only the latest 5 versions.

We already completed all necessary steps to deploy dockerized application to AWS EB environment. If there are changes in the source code, please follow these steps to redeploy the application:

  • If you have introduced new ENVs, add them in both docker-compose.yml and EB environment settings.
  • Build and push new docker image to ECR.
  • Update docker-compose.yml file to use the newly created docker image.
  • Initiate the deployment by executing the command eb deploy EB_ENVIRONMENT --profile AWS_PROFILE.
  • Wait until the deployment is completed, then recheck to new code is working as expected.

That's all for first part of this series, mostly all the steps are done manually using commands from local machine. For next part, we will setup CI/CD for automated deployment using AWS CodePipeline or GitHub Actions.

--

--

Ben Tran
Ben Tran

Written by Ben Tran

Full-Stack Developer -- Eat, Code, Sleep at www.goldenowl.asia

Responses (4)