Skip to main content
Deploy Inbox Zero to AWS using AWS Copilot. The deployment uses Amazon ECS on Fargate. If you prefer Terraform, see Terraform Deployment Guide.

Prerequisites

  • AWS CLI installed and configured with appropriate credentials
  • AWS Copilot CLI installed (installation guide)
  • Docker installed and running
  • An AWS account with appropriate permissions
  • Inbox Zero repository cloned locally (run all commands from the repo root)

CLI Setup

The CLI automates Copilot setup, addons (RDS + ElastiCache), secrets, and deployment. Run from the cloned repo root:
pnpm setup-aws
Non-interactive mode:
pnpm setup-aws -- --yes
The CLI will update copilot/environments/addons/addons.parameters.yml, configure SSM secrets, deploy the environment, and then deploy the service. It also handles the webhook gateway if enabled. Note: The CLI now writes DATABASE_URL, DIRECT_URL, and REDIS_URL after the environment deploy, because creating those SSM parameters inside addon templates can trigger EarlyValidation failures.
If you use the CLI, you can skip the manual steps below.

Manual Copilot Setup

Use this section if you prefer to drive Copilot directly.

1. Initialize the Copilot Application

First, initialize a new Copilot application with your domain:
copilot app init inbox-zero-app --domain <YOUR DOMAIN HERE>
Replace <YOUR DOMAIN HERE> with your actual domain (without the http:// or https:// prefix), for example: example.com. This creates the Copilot application structure and sets up your domain.
Note: The --domain flag only works if your domain is hosted on AWS Route53. If your domain is managed elsewhere, omit the --domain flag and remove the http section from copilot/inbox-zero-ecs/manifest.yml (the alias and hosted_zone fields). You’ll need to configure your domain’s DNS separately to point to the load balancer.

2. Configure the Service Manifest

Before initializing the service, configure the environment variables in the manifest file. The service manifest (copilot/inbox-zero-ecs/manifest.yml) is already included in the repository. Edit copilot/inbox-zero-ecs/manifest.yml to add your environment variables in the variables section. Required environment variables include:
  • DATABASE_URL - Your PostgreSQL connection string
  • DIRECT_URL - Direct database connection (for migrations)
  • AUTH_SECRET - Authentication secret
  • GOOGLE_CLIENT_ID - Google OAuth client ID
  • GOOGLE_CLIENT_SECRET - Google OAuth client secret
  • NEXT_PUBLIC_BASE_URL - Your application URL
  • And other required variables (see apps/web/env.ts)
For sensitive values, consider using the secrets section instead of variables (see Managing Secrets below).

3. Initialize the Production Environment

Create a production environment:
copilot env init --name production
This will prompt you for:
  • AWS profile/region (if not already configured)
  • Other infrastructure options

4. Initialize the Service

Initialize the Load Balanced Web Service:
copilot init --app inbox-zero-app --name inbox-zero-ecs --type "Load Balanced Web Service" --deploy no
Note: The service manifest is already included in the repository. Copilot will detect the existing manifest and configure infrastructure accordingly.

5. Deploy the Environment

Deploy the production environment infrastructure:
copilot env deploy --force
This creates the necessary AWS resources (VPC, load balancer, etc.) for your environment.

6. Deploy the Service

Deploy your application service:
copilot svc deploy
This will:
  • Use the pre-built Docker image from GitHub Container Registry (ghcr.io/elie222/inbox-zero:latest), or
  • Build your Docker image using docker/Dockerfile.prod if you prefer to build from source
  • Push the image to Amazon ECR (if building)
  • Deploy the service to ECS/Fargate
  • Set up the load balancer and domain
Note: The manifest is configured to use the pre-built public image by default. If you want to build from source instead, you can remove or comment out the image.location line in copilot/inbox-zero-ecs/manifest.yml and Copilot will build using the image.build configuration.

Post-Deployment

The following sections apply whether you used the CLI or manual setup.

Updating Your Deployment

To update your application after making changes:
copilot svc deploy
This will:
  • Pull the latest pre-built image from GitHub Container Registry (if using the default configuration), or
  • Rebuild and redeploy your service with the latest changes (if building from source)

ElastiCache Redis (Optional)

Redis is deployed as an environment addon. You can enable or change its size by editing copilot/environments/addons/addons.parameters.yml:
EnableRedis: 'true'
RedisInstanceClass: 'cache.t4g.micro'
Then deploy the environment:
copilot env deploy --name production

Managing Secrets

For sensitive values, use AWS Systems Manager Parameter Store:
  1. Store secrets in Parameter Store:
    aws ssm put-parameter --name /copilot/inbox-zero-app/production/inbox-zero-ecs/AUTH_SECRET --value "your-secret" --type SecureString
    
  2. Reference them in manifest.yml:
    secrets:
      AUTH_SECRET: AUTH_SECRET  # The key is the env var name, value is the SSM parameter name
    

Viewing Logs

View your application logs:
copilot svc logs
Or follow logs in real-time:
copilot svc logs --follow

Checking Service Status

Check the status of your service:
copilot svc status

Database Migrations

Database migrations run automatically on container startup via the docker/scripts/start.sh script. The script uses prisma migrate deploy to apply any pending migrations. Important: The service manifest includes a grace_period of 320 seconds in the healthcheck configuration to ensure the container is not killed before migrations complete. This is especially important for the initial deployment when all migrations need to be applied. If you have a large number of migrations, you may need to increase this value in copilot/inbox-zero-ecs/manifest.yml. If you need to manually run migrations:
copilot svc exec
# Then inside the container:
prisma migrate deploy --schema=./apps/web/prisma/schema.prisma

Troubleshooting

Service Won’t Start

  1. Check logs: copilot svc logs
  2. Verify environment variables are set correctly
  3. Ensure database is accessible from the ECS task
  4. Check that the Docker image builds successfully

Migration Issues

If migrations fail:
  1. Check database connectivity
  2. Verify DATABASE_URL and DIRECT_URL are correct
  3. Check the container logs for specific error messages
  4. You may need to manually resolve failed migrations using prisma migrate resolve

Addons Change Set EarlyValidation

If copilot env deploy fails with AWS::EarlyValidation::PropertyValidation, make sure addon templates do not create SSM parameters that include dynamic Secrets Manager references. The CLI setup flow creates DATABASE_URL, DIRECT_URL, and REDIS_URL after the environment deploy.

Domain Not Working

  1. Verify DNS settings for your domain
  2. Check that the load balancer is properly configured
  3. Ensure SSL certificate is provisioned (Copilot handles this automatically)

Firewalled Deployments (Webhook Gateway)

For deployments where the main application is behind a firewall or private network (e.g., only accessible to employees via VPN), you need a way for Google Pub/Sub to deliver Gmail webhook notifications. The webhook gateway addon solves this by creating a public API Gateway endpoint that validates Google’s OIDC tokens before forwarding to your private infrastructure.

Prerequisites

  • IAM User (not root): AWS Copilot requires IAM role assumption, which doesn’t work with root account credentials. Create an IAM user with AdministratorAccess policy.
  • AWS CLI Profile: Configure an AWS CLI profile for your deployment:
    aws configure --profile inbox-zero
    # Enter your IAM user's access key and secret
    # Set region (e.g., us-east-1)
    
  • Set environment variables before running Copilot commands:
    export AWS_PROFILE=inbox-zero
    export AWS_REGION=us-east-1
    

Architecture

Google Pub/Sub → API Gateway (public) → VPC Link → Internal ALB → ECS

               JWT validation
               (Google OIDC)
  • API Gateway: Public endpoint that Google Pub/Sub can reach
  • JWT Authorizer: Validates Google’s OIDC tokens cryptographically
  • VPC Link: Connects API Gateway to your private VPC
  • Internal ALB: Your Copilot-managed load balancer

How It Works

  1. Google Pub/Sub sends webhook requests with a signed JWT in the Authorization header
  2. API Gateway validates the JWT:
    • Verifies signature using Google’s public keys
    • Checks issuer is https://accounts.google.com
    • Validates audience matches your configured endpoint
    • Ensures token is not expired
  3. Valid requests are forwarded to your internal ALB via VPC Link
  4. Invalid requests are rejected with 401 (never reach your app)

Deployment

The webhook gateway is an environment addon. However, it requires the ALB’s HTTPS listener which is only created when a Load Balanced Web Service is deployed. Follow this specific order:
Important: The addon references HTTPSListenerArn which only exists after a service is deployed. If you try to deploy the environment addon before the service, it will fail.

First-time Setup (New Deployment)

Keep the webhook gateway template in copilot/templates/ until the service is deployed.
  1. Deploy the environment (without the addon):
    copilot env deploy --name production
    
  2. Deploy the service (this creates the ALB and HTTPS listener):
    copilot svc deploy --name inbox-zero-ecs --env production
    
  3. Add and deploy the addon:
    cp copilot/templates/webhook-gateway.yml copilot/environments/addons/
    copilot env deploy --name production
    

Existing Deployment (Service Already Running)

If you already have a deployed service with an ALB, add the addon then deploy the environment:
cp copilot/templates/webhook-gateway.yml copilot/environments/addons/
copilot env deploy --name production

Get the Webhook Endpoint URL

After the addon is deployed, get the webhook URL from the addon stack outputs:
# Find the addon stack
ADDON_STACK=$(aws cloudformation list-stack-resources \
  --stack-name inbox-zero-app-production \
  --query "StackResourceSummaries[?contains(LogicalResourceId,'AddonsStack')].PhysicalResourceId" \
  --output text)

# Get the webhook URL
aws cloudformation describe-stacks \
  --stack-name "$ADDON_STACK" \
  --query "Stacks[0].Outputs[?OutputKey=='WebhookEndpointUrl'].OutputValue" \
  --output text
The URL will look like: https://abc123xyz.execute-api.us-east-1.amazonaws.com/api/google/webhook

Google Cloud Configuration

Configure your Google Cloud Pub/Sub push subscription to use OIDC authentication:
  1. Create or update the push subscription:
    # Get the webhook URL from the previous step
    WEBHOOK_URL="https://abc123xyz.execute-api.us-east-1.amazonaws.com/api/google/webhook"
    
    gcloud pubsub subscriptions create gmail-push-subscription \
      --topic=projects/YOUR_PROJECT/topics/gmail-notifications \
      --push-endpoint="${WEBHOOK_URL}" \
      --push-auth-service-account=YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com \
      --push-auth-token-audience="${WEBHOOK_URL}"
    
    Or update an existing subscription:
    gcloud pubsub subscriptions modify-push-config gmail-push-subscription \
      --push-endpoint="${WEBHOOK_URL}" \
      --push-auth-service-account=YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com \
      --push-auth-token-audience="${WEBHOOK_URL}"
    
  2. Grant token creation permissions:
    PROJECT_NUMBER=$(gcloud projects describe YOUR_PROJECT --format='value(projectNumber)')
    
    gcloud projects add-iam-policy-binding YOUR_PROJECT \
      --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com" \
      --role="roles/iam.serviceAccountTokenCreator"
    

Custom Domain (Optional)

If you want to use a custom domain for the webhook endpoint:
  1. Edit copilot/environments/addons/addons.parameters.yml:
    Parameters:
      WebhookAudience: 'https://webhook.yourdomain.com/api/google/webhook'
    
  2. Set up a custom domain in API Gateway (via AWS Console or additional CloudFormation)
  3. Update the Google Pub/Sub subscription with the custom domain URL

Verification

Test that the endpoint correctly rejects unauthenticated requests:
# This should return 401 Unauthorized
curl -X POST https://abc123xyz.execute-api.us-east-1.amazonaws.com/api/google/webhook

Security Notes

AspectDetails
AuthenticationCryptographic JWT validation using Google’s public keys
IssuerFixed to https://accounts.google.com
AudienceMust match exactly between AWS and Google configurations
Token lifetimeGoogle tokens are valid for up to 1 hour
ThrottlingAPI Gateway applies rate limiting (50 req/sec, 100 burst)

Troubleshooting

401 Unauthorized from API Gateway:
  • Verify the audience in Google Pub/Sub matches the AWS configuration exactly
  • Check that the service account has iam.serviceAccountTokenCreator permissions
  • Ensure the push subscription has OIDC authentication enabled
502 Bad Gateway:
  • The VPC Link may not have connectivity to the ALB
  • Check security group rules allow traffic from API Gateway to ALB
  • Verify the ALB listener is healthy
Logs:
# View API Gateway logs
aws logs tail /aws/apigateway/inbox-zero-app-production-webhook-api --follow

Additional Resources