Installation Steps
Step 0. Before we start
We recommend running Tines in a dedicated VPC. You can follow these instructions to create a new VPC with recommended configuration options. If you are running Tines in a VPC shared with other resources, you must ensure that there are two public and two private subnets that you can use for your Tines deployment.
There are some environment variables that are needed by multiple steps in this guide, so it's easiest to set them up in advance.
We recommend storing all environment variables in a file like tines-variables.env
. This way, you can re-use the environment variables even if you switch terminal shells. Once you create this file, you can add all the export
lines mentioned in rest of this document into this file. Including the comments that start with #
and then run source tines-variables.env
. Each time you add a new line to this file, you will need to run source tines-variables.env
, so that your terminal shell has loaded all the environment variables.
Next, to set up these variables, you would first need access to the AWS CLI:
# Replace this with the name of AWS region you're running Tines in:
export AWS_REGION="eu-west-1"
# Replace this with the name of AWS account you're running Tines in:
export AWS_ACCOUNT_ID="123456789012"
# Replace this with the ID of the VPC you're running Tines in:
export VPC_ID="vpc-xxxxxxxx"
# Replace these with the IDs of two private subnets from different AZs in your VPC:
# Ensure there is an outbound route configured within the private subnets route table with a Nat Gateway or similar
export PRIVATE_SUBNET_ID_1="subnet-xxxxxxxx"
export PRIVATE_SUBNET_ID_2="subnet-xxxxxxxx"
# Replace these with the IDs of two public subnets from different AZs in your VPC:
export PUBLIC_SUBNET_ID_1="subnet-xxxxxxxx"
export PUBLIC_SUBNET_ID_2="subnet-xxxxxxxx"
# Replace this with the name of the latest Tines image:
export IMAGE="tines-app:latest"
# Replace this with the name of the S3 bucket that will contain the environment files:
export ENV_FILE_S3_BUCKET="tines-test-env"
Before you beigin, It's a good practice to execute each step individually. This makes it easier to debug by isolating any exceptions or errors that occur during specific steps.
Step 1. Prepare an SSL certificate
Following the instructions here, create a certificate for the domain that you ultimately want your Tines instance to be accessible at in your browser.
For this step, it's easiest to just use the AWS console and follow the instructions for validation. The remaining steps will use the CLI.
ℹ️ Ensure to keep note of the CERTIFICATE_ARN
from the cert created via AWS console, you will need it in a later step.
Step 2. Prepare the Tines Docker image
For this step, you'll need access to the tines-app Docker Hub repository. If you don't have access then the Tines support team can provide it.
To make things a little easier later on, we'll create an AWS ECR repository and copy the image from Docker Hub into that repository:
aws ecr create-repository --repository-name tines-app
export REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
# First you must login with your personal docker account that has access to the tines repo login
docker login
# Make sure and update the below region if necessary
aws ecr get-login-password --region eu-west-1 | \
docker login --username AWS --password-stdin $REGISTRY
docker pull tines/$IMAGE
docker tag tines/$IMAGE $REGISTRY/$IMAGE
docker push $REGISTRY/$IMAGE
Step 3. Prepare some security groups
To more clearly illustrate how the different components talk to each other, we'll create a security group for each one and open only the necessary ports. First we create the groups:
# Keep note of each GroupId value - we'll need them for later steps
aws ec2 create-security-group \
--group-name tines-lb \
--vpc-id $VPC_ID \
--description "Load balancer security group for the Tines application"
aws ec2 create-security-group \
--group-name tines-db \
--vpc-id $VPC_ID \
--description "Database security group for the Tines application"
aws ec2 create-security-group \
--group-name tines-redis \
--vpc-id $VPC_ID \
--description "Redis security group for the Tines application"
aws ec2 create-security-group \
--group-name tines-app \
--vpc-id $VPC_ID \
--description "tines-app container security group for the Tines application"
aws ec2 create-security-group \
--group-name tines-sidekiq \
--vpc-id $VPC_ID \
--description "tines-sidekiq container security group for the Tines application"
Then we assign their IDs to environment variables that we can use throughout the rest of the process:
# Replace this with the ID of the tines-lb security group:
export LOAD_BALANCER_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-db security group:
export DATABASE_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-redis security group:
export REDIS_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-app security group:
export APP_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-sidekiq security group:
export SIDEKIQ_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
Then, we set up the necessary rules:
aws ec2 authorize-security-group-ingress \
--group-id $LOAD_BALANCER_SECURITY_GROUP_ID \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id $APP_SECURITY_GROUP_ID \
--source-group $LOAD_BALANCER_SECURITY_GROUP_ID \
--protocol tcp --port 3000
aws ec2 authorize-security-group-ingress \
--group-id $DATABASE_SECURITY_GROUP_ID \
--source-group $APP_SECURITY_GROUP_ID \
--protocol tcp --port 5432
aws ec2 authorize-security-group-ingress \
--group-id $DATABASE_SECURITY_GROUP_ID \
--source-group $SIDEKIQ_SECURITY_GROUP_ID \
--protocol tcp --port 5432
aws ec2 authorize-security-group-ingress \
--group-id $REDIS_SECURITY_GROUP_ID \
--source-group $APP_SECURITY_GROUP_ID \
--protocol tcp --port 6379
aws ec2 authorize-security-group-ingress \
--group-id $REDIS_SECURITY_GROUP_ID \
--source-group $SIDEKIQ_SECURITY_GROUP_ID \
--protocol tcp --port 6379
💡Note
Step 4. Create a Postgres database
First, we create a database subnet group:
aws rds create-db-subnet-group \
--db-subnet-group-name tines-db \
--db-subnet-group-description "Tines database" \
--subnet-ids $PRIVATE_SUBNET_ID_1 $PRIVATE_SUBNET_ID_2
Then, we create an Aurora PostgreSQL cluster with a single instance:
# Enter a password at the prompt after running this command.
# A value that contains punctuation other than underscores and dashes may cause errors.
echo "Type in the password you'd like to use for your database, then press (Enter): "
read -rs DB_PASSWORD
# Keep note of the Endpoint of the created object - we'll need it later
aws rds create-db-cluster \
--db-cluster-identifier tines \
--engine aurora-postgresql \
--engine-version 14.6 \
--backup-retention-period 7 \
--master-username tines \
--master-user-password $DB_PASSWORD \
--database-name tines \
--db-subnet-group-name tines-db \
--vpc-security-group-ids $DATABASE_SECURITY_GROUP_ID
aws rds create-db-instance \
--db-instance-identifier tines-db-1 \
--engine aurora-postgresql \
--db-instance-class db.t4g.large \
--db-cluster-identifier tines \
--db-subnet-group-name tines-db
Later, additional instances can be added to the cluster to support failover for higher availability.
See documentation on how to configure this here.
ℹ️Info
Step 5. Create a Redis cluster
aws elasticache create-cache-subnet-group \
--cache-subnet-group-name tines-redis \
--cache-subnet-group-description "Tines Redis" \
--subnet-ids $PRIVATE_SUBNET_ID_1 $PRIVATE_SUBNET_ID_2
aws elasticache create-cache-cluster \
--cache-cluster-id "tines-redis" \
--engine redis \
--cache-node-type cache.t4g.small \
--engine-version 6.x \
--num-cache-nodes 1 \
--cache-subnet-group-name tines-redis \
--security-group-ids $REDIS_SECURITY_GROUP_ID
ℹ️Info
Step 6. Create a load balancer
# Keep note of the ARN of the created object - we'll need it later in this step
aws elbv2 create-load-balancer \
--name tines \
--subnets $PUBLIC_SUBNET_ID_1 $PUBLIC_SUBNET_ID_2 \
--security-groups $LOAD_BALANCER_SECURITY_GROUP_ID
ℹ️ Ensure to keep note of the LOAD_BALANCER_ARN
returned from the above command, you will need it in a later step.
Then create a target group:
# Keep note of the ARN of the created object - we'll need it later in this step
aws elbv2 create-target-group \
--name tines-app \
--protocol HTTP \
--port 3000 \
--target-type ip \
--health-check-path '/is_up' \
--vpc-id $VPC_ID
ℹ️ Ensure to keep note of the TARGET_GROUP_ARN
returned from the above command, you will need it in a later step.
Then create a listener:
# Replace this with the ARN of the load balancer created by the previous command:
export LOAD_BALANCER_ARN="arn:aws:elasticloadbalancing:eu-west-1:123456789012:loadbalancer/app/tines/ad446b3207c26fe7"
# Replace this with the ARN of the target group created by the previous command:
export TARGET_GROUP_ARN="arn:aws:elasticloadbalancing:eu-west-1:123456789012:targetgroup/tines-app/66559e249e21308d"
# Replace this with the ARN of a certificate that you created at step 1
export CERTIFICATE_ARN="arn:aws:acm:eu-west-1:123456789012:certificate/85b79526-e45f-4e76-8e3a-1d407142a62a"
aws elbv2 create-listener \
--load-balancer-arn $LOAD_BALANCER_ARN \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=$CERTIFICATE_ARN \
--ssl-policy ELBSecurityPolicy-2016-08 \
--default-actions '[{"Type": "forward", "TargetGroupArn": "'$TARGET_GROUP_ARN'"}]'
Step 7. Create a .env file
Make sure to make any necessary changes to the values in the .env
file before moving on to step 8. Our tines.env template is available here.
Step 8. Upload the .env file to S3
Both the tines-app
and tines-sidekiq
containers need to run with the exact same environment variables. To make this a bit easier, we have them both fetch the same .env file from S3.
aws s3api create-bucket \
--bucket $ENV_FILE_S3_BUCKET \
--region $AWS_REGION \
--create-bucket-configuration "LocationConstraint=$AWS_REGION"
aws s3api put-public-access-block \
--bucket $ENV_FILE_S3_BUCKET \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
aws s3 cp tines.env s3://$ENV_FILE_S3_BUCKET/tines.env
Step 9. Create the IAM roles for running the containers
# This only needs to be create once for an AWS account - if you're already using ECS, you can skip this command:
aws iam create-service-linked-role \
--aws-service-name ecs.amazonaws.com
aws iam create-role \
--role-name tinesTaskExecutionRole \
--assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [{ "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" }]}'
aws iam attach-role-policy \
--role-name tinesTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam put-role-policy \
--role-name tinesTaskExecutionRole \
--policy-name TinesEnvAccess \
--policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env"]}, { "Effect": "Allow", "Action": ["s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::'$ENV_FILE_S3_BUCKET'"]}]}'
Step 10. Create the ECS task definitions and cluster
aws logs create-log-group --log-group-name tines
export EXECUTION_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/tinesTaskExecutionRole"
aws ecs register-task-definition \
--family "tines-app" \
--memory 3072 \
--network-mode awsvpc \
--cpu 1024 \
--execution-role-arn $EXECUTION_ROLE_ARN \
--container-definitions '[{"name": "tines-app", "command": ["start-tines-app"], "image": "'$REGISTRY'/'$IMAGE'", "environmentFiles": [{"value": "arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env", "type": "s3"}], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "tines", "awslogs-region": "'$AWS_REGION'", "awslogs-stream-prefix": "tines" }}, "portMappings": [{"containerPort": 3000}]}]'
aws ecs register-task-definition \
--family "tines-sidekiq" \
--memory 3072 \
--network-mode awsvpc \
--cpu 1024 \
--execution-role-arn $EXECUTION_ROLE_ARN \
--container-definitions '[{"name": "tines-sidekiq", "command": ["start-tines-sidekiq"], "image": "'$REGISTRY'/'$IMAGE'", "environmentFiles": [{"value": "arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env", "type": "s3"}], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "tines", "awslogs-region": "'$AWS_REGION'", "awslogs-stream-prefix": "tines" }}}]'
aws ecs create-cluster --cluster-name tines
Step 11. Seed the database
We run a one-off task, with a one-off command to prepare the database. This command will also trigger an invite email to the seed email address you specified in step 7, once the services in step 12 have been started. The invite link in this email is how you will sign into Tines for the first time.
aws ecs run-task \
--cluster tines \
--task-definition tines-app \
--launch-type "FARGATE" \
--network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=DISABLED}" \
--overrides '{ "containerOverrides": [{ "name": "tines-app", "command": ["prepare-database"]}] }'
If you run into any issues during this initial setup, you can delete and recreate the database by running the following command. Please note that this will delete all data in your Tines instance, so it should only be used if the initial setup fails. After this command is run, you can repeat the command above to re-seed the database. If you have created the services from step 12 already, you should update them to set their Desired tasks to 0 while you run these commands.
# WARNING - this command will delete any data in the database.
aws ecs run-task \
--cluster tines \
--task-definition tines-app \
--launch-type "FARGATE" \
--network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=DISABLED}" \
--overrides '{ "containerOverrides": [{ "name": "tines-app", "command": ["bundle", "exec", "rake", "db:drop", "db:create"], "environment": [{"name": "DISABLE_DATABASE_ENVIRONMENT_CHECK", "value": "1"}] }]}'
Step 12. Start the services
aws ecs create-service \
--cluster tines \
--service-name tines-sidekiq \
--task-definition tines-sidekiq \
--desired-count 2 \
--launch-type "FARGATE" \
--network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=DISABLED}"
aws ecs create-service \
--cluster tines \
--service-name tines-app \
--task-definition tines-app \
--desired-count 2 \
--launch-type "FARGATE" \
--network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$APP_SECURITY_GROUP_ID],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=tines-app,containerPort=3000"
ℹ️Info
Once the tines-app
and tines-sidekiq
services are both up and running, it will send the email that was set up during step 11. You can then accept the invite to get started.