How Docker-Backed Services Work
Most of Simfra's 88 services run entirely in-memory - S3, DynamoDB, SQS, SNS, IAM, KMS, CloudWatch, EventBridge, Step Functions, and dozens more simulate the full API without any external dependencies.
But some services need to run real software. Lambda needs to execute your code. RDS needs to run a PostgreSQL or MySQL server. ElastiCache needs a Redis process. For these, Simfra creates Docker containers.
When SIMFRA_DOCKER=true (the default when Docker is detected), Simfra manages Docker containers as an implementation detail of the service simulation. You interact with the AWS API as usual - CreateFunction, CreateDBInstance, CreateCacheCluster - and Simfra handles the Docker lifecycle behind the scenes.
Architecture Overview
┌──────────────────────────────────────────────────────┐
│ Your Machine (Host) │
│ │
│ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ Terraform / │───>│ Simfra Gateway (:4599) │ │
│ │ AWS CLI / │ │ │ │
│ │ SDK │<───│ All 88 services │ │
│ └──────────────┘ │ IAM enforcement │ │
│ │ CloudTrail logging │ │
│ └─────────────┬─────────────┘ │
│ │ Docker API │
│ ┌─────────────▼─────────────┐ │
│ │ Docker Engine │ │
│ │ │ │
│ │ ┌─────────┐ ┌───────────┐ │ │
│ │ │ Lambda │ │ RDS │ │ │
│ │ │ func │ │ Postgres │ │ │
│ │ └─────────┘ └───────────┘ │ │
│ │ ┌─────────┐ ┌───────────┐ │ │
│ │ │ EC2 │ │ Redis │ │ │
│ │ │ inst │ │ cluster │ │ │
│ │ └─────────┘ └───────────┘ │ │
│ │ ┌─────────┐ ┌───────────┐ │ │
│ │ │ ALB │ │ DNS │ │ │
│ │ │ proxy │ │ server │ │ │
│ │ └─────────┘ └───────────┘ │ │
│ └───────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Simfra is always the gateway. Your SDK talks to Simfra, Simfra talks to Docker, and containers talk back to Simfra. Containers never expose their own APIs directly to your code.
What Runs in Docker vs What Doesn't
Docker-backed - these services create real containers that run real software:
| Service | What Runs in Docker |
|---|---|
| Lambda | Your function code in runtime-specific containers (Node.js, Python, Go, Java, .NET, Ruby, custom) |
| EC2 | Instance containers with SSH access and IMDS |
| RDS | MySQL, PostgreSQL, and MariaDB database servers |
| Aurora DSQL | PostgreSQL-compatible distributed SQL |
| ElastiCache | Redis, Valkey, and Memcached servers |
| EKS | Kind (Kubernetes in Docker) clusters |
| ELBv2 | Reverse proxy containers for ALB/NLB |
| Route53 | DNS server that resolves hosted zones and service DNS names |
| CloudFront | CDN proxy with Lambda@Edge and CloudFront Functions |
| EFS | NFS server containers |
| MQ | ActiveMQ and RabbitMQ message brokers |
| MSK | Apache Kafka brokers |
| OpenSearch | OpenSearch/Elasticsearch clusters |
| Redshift | PostgreSQL-based analytics clusters |
| Directory Service | Samba Active Directory domain controllers |
| CodeCommit | Git server with SSH/HTTPS access |
| CodeBuild | Build runner containers |
| Bedrock | Ollama or stable-diffusion.cpp for model inference |
| SES | SMTP relay containers for email delivery |
In-memory only - these services simulate the full API without Docker:
S3, DynamoDB, SQS, SNS, IAM, STS, KMS, CloudWatch, CloudWatch Logs, EventBridge, Step Functions, Kinesis, Firehose, DynamoDB Streams, SSM Parameter Store, Secrets Manager, ACM, CloudTrail, Config, Glue, Athena, CloudFormation, WAFv2, Cognito, Organizations, and all other services.
The distinction matters for two reasons: Docker-backed services take longer to create (container startup time) and require Docker to be running. In-memory services respond instantly and have no external dependencies.
The Gateway Pattern
Simfra follows a strict gateway pattern: containers never expose their APIs directly to your code. Here's why.
When you call rds:CreateDBInstance, Simfra creates a PostgreSQL container, but you don't connect to it by finding the container's port. Instead, the RDS endpoint you receive (e.g., mydb.abc123.us-east-1.rds.simfra.dev) resolves through Simfra's DNS system to the container's IP. The same is true for Lambda - when you call lambda:Invoke, the request goes to Simfra, which invokes the function container internally and returns the result.
This gateway pattern exists because Simfra needs to stay in the loop for IAM enforcement, CloudTrail logging, resource policy evaluation, and cross-service integration. If containers exposed their APIs directly, none of those behaviors would apply to requests that bypassed the gateway.
The exception is data-plane connections that are inherently direct: database TCP connections (RDS, ElastiCache, Redshift), Kafka client connections (MSK), NFS mounts (EFS), and SSH access (EC2). For these, the client connects directly to the container - there's no HTTP gateway in between. Simfra handles the control plane (creating, configuring, deleting resources), and the container handles the data plane.
Container Communication Model
Simfra uses a pull/webhook model for container communication. Containers don't mount host files and don't expose admin ports. Instead:
Config Pull
When Simfra creates or updates a container's configuration, it stores the config in an in-memory ConfigStore, keyed by container name. The container periodically polls GET /_internal/container/config?name={name} with ETag-based conditional requests:
- 200 OK - new or changed config, with the JSON body and an ETag header.
- 304 Not Modified - config hasn't changed since the last poll (ETag matches).
- 404 Not Found - container not yet registered (waits and retries).
This means configuration updates propagate to containers within one poll interval (typically 1-2 seconds). For example, when you update an ALB's listener rules, the new routing config is stored in the ConfigStore and the reverse proxy container picks it up on its next poll.
Status Webhook
Containers report their health and status by posting to POST /_internal/container/status:
{
"container_name": "simfra-rds-mydb-abc123",
"status": "healthy",
"data": { "connections": 3, "uptime_seconds": 1200 }
}
Simfra stores the latest status in a StatusStore. Service handlers read from this store to report resource status - for example, ELBv2 reads target health from the StatusStore to respond to DescribeTargetHealth calls.
Command Dispatch
Some containers need to handle synchronous RPC calls from Simfra. For example, Directory Service needs to execute samba-tool commands to create computer accounts. Instead of exposing an admin API, containers poll for commands:
- Simfra submits a command to the
CommandStorewith a method, path, body, and timeout. - The container polls
GET /_internal/container/commands(typically every 200ms) and picks up the pending command. - The container executes the command and posts the result to
POST /_internal/container/command-result. - Simfra's
Submitcall unblocks and returns the result.
This eliminates the need for admin ports on containers while still supporting bidirectional operations.
Startup Environment
Every container receives three environment variables:
SIMFRA_ENDPOINT- the URL to reach Simfra (e.g.,http://simfra.local:4599).SIMFRA_TOKEN- the internal bearer token for authenticating to/_internal/*endpoints.SIMFRA_CONTAINER_NAME- the container's name, used as the key for config pull and status reporting.
Containers use these to locate Simfra, authenticate, and identify themselves. The simfra.local hostname resolves to the Docker host IP, allowing containers to reach Simfra regardless of the Docker network topology.
VPC as Docker Networks
When SIMFRA_VPC_ISOLATION=true (the default when Docker is enabled), Simfra maps AWS VPCs to Docker bridge networks. Each VPC gets its own Docker network named simfra-vpc-{accountID}-{region}-{vpcID}.
Containers are attached to their VPC's Docker network, which means:
- Containers in the same VPC can reach each other by IP, just like real AWS. An ECS task can connect to an RDS instance in the same VPC without port publishing.
- Private resources have no host port publishing. An internal ALB or a private RDS instance exists only on the VPC Docker network. It's not reachable from the host, matching how private resources work in a real VPC.
- Public resources get host port publishing. An internet-facing ALB or a publicly accessible RDS instance publishes ports to the host, matching how public resources are reachable from outside the VPC.
The host machine is treated as "the public internet." If a resource is private in AWS (internal ALB, private subnet RDS), it's not reachable from the host in Simfra. If it's public, it is.
DNS Resolution
Simfra runs a per-account DNS container that resolves:
- Service DNS names - ALB DNS names (
myalb-123.us-east-1.elb.simfra.dev), RDS endpoints (mydb.abc123.us-east-1.rds.simfra.dev), and other service-generated hostnames resolve to the container's IP on the VPC network. - Route53 hosted zones - records you create in Route53 are served by the DNS container, including ALIAS record resolution to service DNS names.
- simfra.local - resolves to Simfra's IP, allowing containers to call back to the gateway.
VPC-attached containers use --dns {dns-container-ip} to route DNS queries through this container. This means that when an ECS task running in a VPC resolves an RDS endpoint, it gets the container's VPC network IP - not a host port mapping.
Credential Injection
Different service types inject credentials differently, matching how AWS provides credentials to compute resources:
Lambda - Execution role credentials are provided as environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN). These are real STS temporary credentials obtained by assuming the function's execution role. The function code uses them to call other AWS services through Simfra. AWS_ENDPOINT_URL points back to Simfra so SDK calls are routed correctly.
ECS - Task role credentials are available via the ECS credential endpoint (AWS_CONTAINER_CREDENTIALS_RELATIVE_URI). Container secrets (from Secrets Manager or SSM Parameter Store) are resolved at task launch time and injected as environment variables, matching how ECS handles the secrets field in task definitions.
EC2 - Instance Metadata Service (IMDS) is available at 169.254.169.254. Instances can retrieve their instance profile credentials from the metadata endpoint, the same way they do on real AWS. When SIMFRA_IMDS_LINK_LOCAL=true, Simfra binds to the link-local address so containers can reach IMDS at the standard IP.
In all cases, AWS_ENDPOINT_URL is set to Simfra's address so that SDK calls from within containers are routed back to Simfra for processing through the standard gateway pipeline (authentication, authorization, handler execution).
Cross-Service Invocation
Docker containers sometimes need to invoke other Simfra services. For example:
- An ALB container receives an HTTP request and needs to invoke a Lambda function (Lambda target group).
- An API Gateway container needs to invoke a Lambda integration.
- A CloudFront container needs to invoke a Lambda@Edge function.
These cross-service calls go through Simfra's internal API (/_internal/), not through the public gateway. The container posts a request to Simfra with the function ARN and payload, and Simfra handles the invocation internally - including delivery authorization (the Lambda function's resource policy must allow the source service to invoke it).
This design means containers don't need AWS credentials to make service-to-service calls. They use the internal token instead, and Simfra handles authorization using the resource-policy model - matching how AWS services authenticate to each other using service principals rather than IAM credentials.
Container Lifecycle
When you create a resource backed by Docker (e.g., CreateDBInstance), the API returns immediately with the resource in a transitional state (e.g., creating). A background worker then:
- Pulls the required Docker image (if not already present).
- Creates the container with the correct configuration, network attachments, and environment variables.
- Waits for the container to become healthy (via Docker HEALTHCHECK).
- Updates the resource state to
available. - Registers DNS names if applicable.
This matches AWS behavior - resource creation is asynchronous. If you poll DescribeDBInstances after CreateDBInstance, you'll see the status transition from creating to available.
When you delete a resource, the process reverses: the state transitions to deleting, the container is stopped and removed, DNS names are deregistered, and the resource is removed from the store.
On Simfra shutdown, containers are cleaned up if SIMFRA_DOCKER_CLEANUP_ON_SHUTDOWN=true. On startup, stale containers from previous runs are cleaned up if SIMFRA_DOCKER_CLEANUP_ON_START=true (the default).
When Docker Is Disabled
If SIMFRA_DOCKER=false or Docker isn't available, Docker-backed services still accept API calls. Resource metadata is created and stored (you can CreateDBInstance and DescribeDBInstances), but no actual container is created. This means:
- The resource will show in API responses with correct metadata.
- You won't be able to connect to the data plane (no database to connect to, no function to invoke).
- State transitions may complete instantly instead of going through
creating -> available.
This mode is useful for testing Terraform plans and resource creation logic without needing Docker, but it doesn't provide a working data plane.