Oct 27, 20266 min read

How to Deploy Actix Web to AWS

Running an Actix Web 4 API on AWS with RDS Postgres and a Secrets Manager key, using Encore, the AWS CDK or Terraform, with a non-root container and every IAM grant spelled out.

Actix Web compiles to one binary, which keeps the container simple. The AWS side has more parts: a VPC, ECS on Fargate, a load balancer, RDS Postgres, a Secrets Manager entry, and IAM roles so the task can read the secret and reach the database.

This guide deploys an Actix Web 4 orders API to AWS with Encore, the AWS CDK and Terraform. Each version runs on Postgres and reads a payments API key from Secrets Manager.

What you need to decide:

  • How the AWS resources are defined and who reviews changes to them.
  • Which role reads each secret.
  • How migrations run before new code serves traffic.
  • How local development gets the same database.

Actix Web on AWS options at a glance

CapabilityEncoreAWS CDKTerraform
Defined inRust code of the appTypeScript or Python stackHCL files
RDS PostgresFrom Database::newrds.DatabaseInstanceaws_db_instance
SecretsSecret::new, set per environmentsecretsmanager.Secret and a grantaws_secretsmanager_secret and a policy
ContainerBuilt on deployYour DockerfileYour Dockerfile and ECR
Local Postgresencore run starts itRun your ownRun your own

Encore: deploy Actix Web to AWS from your code

Encore

Encore combines the database and secrets your Actix Web code declares with a local environment and a deploy to your own AWS account, where it provisions Fargate, RDS and Secrets Manager from that same code. It is an infrastructure SDK, the encore-dev crate, added to an existing app, and the handlers stay plain Actix Web 4.

The database and the secret are statics:

use encore::secrets::Secret; use encore::storage::postgres::Database; // RDS on AWS; `encore run` starts Postgres locally and applies ./migrations. static DB: Database = Database::new("orders").migrations("./migrations"); // Set per environment with `encore secret set`, stored in AWS Secrets Manager. static PAYMENTS_KEY: Secret = Secret::new("PaymentsApiKey");

The migration creates the orders table:

CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, total_cents BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

The handler refuses orders when the payments key is missing for the environment, and a health route gives the load balancer something cheap to check. HttpServer binds to the address Encore assigns:

use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; use serde::Deserialize; #[derive(Deserialize)] struct NewOrder { email: String, total_cents: i64, } #[post("/orders")] async fn create_order(body: web::Json<NewOrder>) -> impl Responder { if PAYMENTS_KEY.value().is_err() { return HttpResponse::ServiceUnavailable().body("payments not configured\n"); } match DB .query_one( "INSERT INTO orders (email, total_cents) VALUES ($1, $2) RETURNING id", &[&body.email, &body.total_cents], ) .await { Ok(row) => { let id: i64 = row.get("id"); HttpResponse::Created().json(serde_json::json!({ "id": id })) } Err(_) => HttpResponse::InternalServerError().finish(), } } #[get("/healthz")] async fn healthz() -> impl Responder { HttpResponse::Ok().body("ok\n") } #[actix_web::main] async fn main() -> std::io::Result<()> { encore::initialize(); // Bind to the address Encore assigns, locally and on AWS. let addr = std::env::var("ENCORE_LISTEN_ADDR").unwrap_or("0.0.0.0:8080".into()); HttpServer::new(|| App::new().service(create_order).service(healthz)) .bind(addr)? .run() .await }

Locally, the key comes from .secrets.user.toml and encore run starts Postgres and applies the migration. For AWS, set the secret per environment type and deploy:

~/orders
$ encore run$ curl -X POST localhost:4000/orders -H 'Content-Type: application/json' -d '{"email":"ada@example.com","total_cents":4200}'{"id":1}$ encore secret set --type production PaymentsApiKey$ git push encore

On deploy, Encore provisions the service on Fargate, RDS Postgres with the migration applied and the secret in Secrets Manager in your AWS account, and provisions an IAM role for the service. Each environment has its own database and secret values, and you can connect your AWS account once for all of them.

An agent that adds a table or a secret runs it under encore run first, and the AWS resources follow on deploy from the same commit.

Good to know

Encore builds the container on deploy from the app code. Each request has a trace with its SQL.

Try Encore →

AWS CDK

With the CDK, you write the container yourself. A multi-stage build keeps the runtime image small and runs as a non-root user:

FROM rust:1.90 AS build WORKDIR /src COPY . . RUN cargo build --release FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* && useradd --system --uid 10001 app COPY --from=build /src/target/release/orders /usr/local/bin/orders USER app EXPOSE 8080 CMD ["orders"]

The stack creates the database, the secret, the Fargate service and the grants that connect them:

import * as cdk from "aws-cdk-lib"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as ecs from "aws-cdk-lib/aws-ecs"; import * as patterns from "aws-cdk-lib/aws-ecs-patterns"; import * as rds from "aws-cdk-lib/aws-rds"; import * as sm from "aws-cdk-lib/aws-secretsmanager"; export class OrdersStack extends cdk.Stack { constructor(scope: cdk.App, id: string) { super(scope, id); const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 2 }); const db = new rds.DatabaseInstance(this, "Db", { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_17 }), vpc, storageEncrypted: true, credentials: rds.Credentials.fromGeneratedSecret("orders"), }); const paymentsKey = new sm.Secret(this, "PaymentsApiKey"); const svc = new patterns.ApplicationLoadBalancedFargateService(this, "Api", { vpc, taskImageOptions: { image: ecs.ContainerImage.fromAsset("."), containerPort: 8080, secrets: { PAYMENTS_API_KEY: ecs.Secret.fromSecretsManager(paymentsKey), DB_PASSWORD: ecs.Secret.fromSecretsManager(db.secret!, "password"), }, environment: { DB_HOST: db.dbInstanceEndpointAddress }, }, }); svc.targetGroup.configureHealthCheck({ path: "/healthz" }); // Let the tasks reach Postgres. db.connections.allowDefaultPortFrom(svc.service); } }

Good to know

ecs.Secret.fromSecretsManager grants the task execution role read access to each secret, and ECS injects the values as environment variables at task start. You still set the payments key value after the first deploy, run migrations as a separate task, and run Postgres yourself locally. AWS now recommends CDK L3 constructs as one replacement for the retired Copilot CLI.

Terraform

Terraform describes the same pieces in HCL. The secret and the policy that lets the task execution role read it look like this:

resource "aws_secretsmanager_secret" "payments_key" { name = "orders/payments-api-key" } data "aws_iam_policy_document" "read_secret" { statement { actions = ["secretsmanager:GetSecretValue"] resources = [aws_secretsmanager_secret.payments_key.arn] } } resource "aws_iam_role_policy" "exec_read_secret" { role = aws_iam_role.task_execution.id policy = data.aws_iam_policy_document.read_secret.json }

Good to know

A complete configuration also covers the VPC, subnets, security groups, the load balancer and listener, the ECS cluster, task definition and service, the RDS instance and subnet group, ECR and the state backend. It is the most explicit option and the longest to write and keep in sync with the app.

How to choose

  • Encore if you want Fargate, RDS and Secrets Manager to follow from the Actix Web code, with the same Postgres running locally.
  • The AWS CDK if you want AWS-native infrastructure in TypeScript and are happy to own the Dockerfile and grants.
  • Terraform if your team already manages AWS with it.

Rust support in Encore is in beta, and you can sign up for access to deploy your Actix Web app to AWS.

Frequently asked questions

How do I deploy an Actix Web app to AWS?

Build a container, run it on ECS Fargate behind a load balancer, connect it to RDS and load secrets from Secrets Manager. With Encore, declare static DB: Database = Database::new("orders").migrations("./migrations") and static PAYMENTS_KEY: Secret = Secret::new("PaymentsApiKey"), set the secret with encore secret set and deploy. Encore provisions the compute, RDS and the secret in your AWS account.

What is the smallest Docker image for Actix Web?

Build with a Rust image in one stage and copy the release binary into a slim runtime image such as debian:bookworm-slim or a distroless image in a second stage. Run as a non-root user and include CA certificates if the app calls HTTPS APIs.

Which AWS service should run an Actix Web container?

ECS on Fargate is the common default for a container behind a load balancer without managing servers. EKS fits teams that already run Kubernetes, and Lambda needs an adapter and suits sparse traffic.

How does Actix Web on ECS read secrets?

The task definition maps a Secrets Manager ARN to an environment variable, and ECS injects it at task start. The task execution role needs secretsmanager:GetSecretValue on that secret.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders