ECS init containers are a surprisingly effective way to run setup tasks before your main application containers even start in an ECS service.

Let’s see this in action. Imagine you have a service that needs to download a large configuration file from S3, initialize a database schema, or perform some other bootstrapping step. Instead of baking this into your application image (which can lead to bloated images and slow deployments) or running it as a separate, one-off task, you can use init containers.

Here’s a simplified task-definition.json snippet demonstrating the concept:

{
  "family": "my-app-with-init",
  "containerDefinitions": [
    {
      "name": "init-setup",
      "image": "amazon/aws-cli:latest",
      "command": ["sh", "-c", "aws s3 cp s3://my-bucket/config.yaml /app/config.yaml && echo 'Setup complete'"],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app-with-init/init-setup",
          "awslogs-stream-prefix": "ecs"
        }
      }
    },
    {
      "name": "app-container",
      "image": "my-docker-repo/my-app:v1.2.0",
      "essential": true,
      "command": ["python", "app.py", "--config", "/app/config.yaml"],
      "portMappings": [
        {
          "containerPort": 8080,
          "hostPort": 8080
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app-with-init/app-container",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "1024",
  "memory": "2048"
}

In this setup, the init-setup container is defined first and marked as essential: true. This is key. ECS treats containers within a task definition in the order they are defined. If an essential container exits with a non-zero status, the entire task is stopped. This means if init-setup fails, app-container will never even start.

The init-setup container uses the amazon/aws-cli image and executes a simple shell command. It copies a configuration file from S3 to a shared volume (or a path accessible by other containers in the same task). Once this command succeeds (exits with status 0), ECS proceeds to start the next container, app-container. The app-container is then configured to use the file created by the init container.

The problem this solves is decoupling pre-application setup from your core application logic. Your application image remains lean and focused on its primary responsibility. Deployment becomes faster because you’re not rebuilding the app image for every minor config change. It also provides a robust mechanism for handling dependencies that might not be present or configured correctly at the time the application container starts.

Internally, ECS orchestrates this by launching containers sequentially for tasks. When you define multiple containers with essential: true at the beginning of the containerDefinitions array, ECS will start them in that order. Each container must successfully complete (exit code 0) before the next one is initiated. If any essential container fails, the task is terminated.

The primary lever you control here is the command in your init container definition. This is where you place all your setup logic. You can chain multiple commands using && or ||, use shell scripts, or even invoke more complex tools. The essential flag is critical; without it, an init container could exit successfully without preventing the application container from starting, or worse, if the app container failed, the init container might still be running, masking the real issue. The order of containers in the containerDefinitions array dictates the execution sequence.

A common pattern is to use a shared EFS volume or even just rely on the container’s ephemeral storage if the data doesn’t need to persist beyond the task’s lifecycle. For example, you could download a large dataset into a persistent volume that your application container then reads from. The init container would handle the download, and the app container would simply mount the volume.

What most people miss is that the command in the init container can be as complex as any other container’s command. You can execute npm install, pip install, run database migration scripts, or even trigger other AWS services. The essential flag isn’t just about failure; it’s also about ensuring the sequence of operations is maintained. If you have an init container that must run before another init container, you’d place them in sequence and both marked essential.

The next concept you’ll likely explore is how to handle more complex initialization logic that doesn’t fit neatly into a single command string, often involving custom scripts and potentially passing secrets or configuration values securely.

Want structured learning?

Take the full Ecs course →