Containers
The Ansible container is published as four image variants:
| Tag | Cloud | VS Code pre-installed | Registry |
|---|---|---|---|
aws |
Amazon Web Services | No | ghcr.io/sapphire-health/ansible-epic:aws |
azure |
Microsoft Azure | No | ghcr.io/sapphire-health/ansible-epic:azure |
aws-web |
Amazon Web Services | Yes | ghcr.io/sapphire-health/ansible-epic:aws-web |
azure-web |
Microsoft Azure | Yes | ghcr.io/sapphire-health/ansible-epic:azure-web |
The aws and azure variants share the same runtime behaviour. Substitute the appropriate tag in the examples below.
The aws-web and azure-web variants extend the base images with a pre-installed VS Code Server and the Ansible VS Code extension. Use these variants in environments with restricted outbound internet access where VS Code cannot be downloaded at runtime. See VS Code Web Server.
When running as a Coder workspace, the aws or azure base image is used. The Coder agent binary is downloaded at container startup directly from the Coder server, so the image never needs to be rebuilt when the Coder server is upgraded.
The container supports several runtime configurations. It can be accessed over SSH, via a VS Code tunnel, or as a Coder workspace, and optionally run a GitHub Actions self-hosted runner in the background concurrently with any access method.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
AUTHORIZED_KEYS |
No | — | SSH public key(s). When set, starts sshd and accepts connections on port 22. |
ANSIBLE_REPO_URL |
No | — | HTTPS URL of the Ansible repository to clone on first start. |
GITHUB_TOKEN |
No | — | GitHub PAT used to clone a private repository. See PAT Requirements. |
TUNNEL_NAME |
No | ansible_container |
VS Code tunnel display name. Used when AUTHORIZED_KEYS is not set. |
GITHUB_RUNNER_URL |
No | — | Repository or organization URL to register the GitHub Actions runner against, e.g. https://github.com/org/repo. |
GITHUB_RUNNER_PAT |
No | — | Long-lived GitHub PAT. The container exchanges this for a fresh registration token at startup. Recommended for automated deployments. |
GITHUB_RUNNER_TOKEN |
No | — | Short-lived GitHub Actions runner registration token. Use for manual or one-off deployments. |
GITHUB_RUNNER_NAME |
No | ECS task ID or hostname | Runner display name shown in GitHub. When running in ECS the task ID is used automatically so each container gets a unique name. On other platforms the hostname is used. |
GITHUB_RUNNER_LABELS |
No | self-hosted,linux |
Comma-separated labels assigned to the runner. |
RUNNER_DEBUG |
No | — | When set, runner output goes to stdout. By default runner output is written to ~/runner.log. |
CODER_AGENT_TOKEN |
No | — | Coder workspace agent token. When set, starts the Coder agent instead of SSH or VS Code tunnel. See Coder Workspace. |
CODER_AGENT_URL |
No | — | URL of the Coder server, e.g. https://coder.example.com. Required when CODER_AGENT_TOKEN is set. |
VSCODE_WEB |
No | — | When set to any non-empty value, starts the VS Code web server on startup. Intended for use with the aws-web / azure-web image variants. See VS Code Web Server. |
VSCODE_WEB_PORT |
No | 8080 |
Port the VS Code web server listens on. Only used when VSCODE_WEB is set. |
Access Modes
SSH
When AUTHORIZED_KEYS is set the container generates SSH host keys, writes the provided key(s) to ~/.ssh/authorized_keys, and starts sshd on port 22.
VS Code Tunnel
When AUTHORIZED_KEYS is not set the container launches a VS Code tunnel. A device login code is printed to the container logs each time the container starts until Microsoft credentials are cached inside the container.
Note
Check the container logs for a device login URL and code. Navigate to https://login.microsoft.com/device and enter the code to authenticate with your Microsoft account. The code refreshes every 15 minutes until authentication completes.
VS Code Web Server
When VSCODE_WEB is set, the container starts code serve-web on startup without requiring a Microsoft login or outbound tunnel. This mode is intended for the aws-web and azure-web image variants, which pre-install the VS Code Server and the Ansible extension so no downloads are needed at runtime.
The server binds to 0.0.0.0 when running standalone, making it accessible on the container's published port. When CODER_AGENT_TOKEN is also set the server binds to 127.0.0.1 instead, and the Coder agent proxies access through the Coder UI.
To use a custom port:
The GitHub Actions runner works alongside the VS Code web server when GITHUB_RUNNER_URL is also set.
Note
The VS Code web server has no authentication when started with --without-connection-token. Ensure the container port is not exposed to untrusted networks, or place a reverse proxy with authentication in front of it.
Coder Workspace
When CODER_AGENT_TOKEN is set the container downloads the Coder agent binary from CODER_AGENT_URL at startup, then starts it. The agent connects back to the Coder server and makes the container available as a workspace. VS Code (via code serve-web) and a terminal are accessible through the Coder UI without any additional configuration. Use the standard aws or azure image variants — no separate Coder-specific image is needed.
The GitHub Actions runner works alongside the Coder agent when GITHUB_RUNNER_URL is also set:
Obtaining the Agent Token
The agent token is generated when a Coder workspace is created from a template. To retrieve it for manual use:
Coder Template
The Coder workspace templates for this container are in the ansible-epic repository under coder/dev-container/dev-container.tf (general-purpose development container) and coder/gh-runner/gh-runner.tf (development container with a GitHub Actions self-hosted runner). Both provision an AWS ECS Fargate task running the aws-coder image, backed by EFS for persistent storage, and wire up the Coder agent, VS Code web access, and optional tooling automatically.
This template is deployed to the Sapphire Health Coder instance at https://coder.sapphirehealth.org. Access requires a Sapphire Health account — sign in using Microsoft Entra ID (single sign-on) from the Coder login page.
GitHub Actions Runner
Setting GITHUB_RUNNER_URL and either GITHUB_RUNNER_PAT or GITHUB_RUNNER_TOKEN causes the container to configure and start a GitHub Actions self-hosted runner in the background before starting SSH or the VS Code tunnel. The runner and the access method run concurrently within the same container.
Automated Deployment (Terraform / ECS)
For automated deployments, supply a long-lived GitHub PAT via GITHUB_RUNNER_PAT. The container calls the GitHub API at startup to generate a fresh registration token each time the container starts, so no manual token rotation is required.
Store the PAT as an AWS Secrets Manager secret and inject it into the ECS task definition using the secrets field. ECS fetches the secret value and injects it as an environment variable before the container starts.
resource "aws_secretsmanager_secret" "github_runner_pat" {
name = "ansible/github-runner-pat"
}
resource "aws_secretsmanager_secret_version" "github_runner_pat" {
secret_id = aws_secretsmanager_secret.github_runner_pat.id
secret_string = var.github_runner_pat
}
Note
Store the secret as a plain string (the raw token value), not as a JSON object. The valueFrom field in the task definition references the secret ARN directly, which instructs ECS to inject the entire secret value as the environment variable. If the secret is stored as JSON (e.g. {"GITHUB_RUNNER_PAT":"github_pat_..."}), ECS will inject the JSON string rather than the token.
Reference the secret in the ECS task definition alongside the other runner environment variables:
resource "aws_ecs_task_definition" "ansible" {
...
container_definitions = jsonencode([{
...
secrets = [
{
name = "GITHUB_RUNNER_PAT"
valueFrom = aws_secretsmanager_secret.github_runner_pat.arn
}
]
environment = [
{ name = "GITHUB_RUNNER_URL", value = "https://github.com/Sapphire-Health/ansible-epic" },
{ name = "GITHUB_RUNNER_NAME", value = "ansible-container" },
{ name = "GITHUB_RUNNER_LABELS", value = "self-hosted,linux,aws" }
]
}])
}
Note
The ECS task execution role must have secretsmanager:GetSecretValue on the secret ARN. See AWS Container Requirements.
Manual Token
For testing or one-off deployments, supply a short-lived registration token directly via GITHUB_RUNNER_TOKEN:
To obtain a registration token from GitHub:
- Navigate to the repository or organization Settings.
- Select Actions → Runners → New self-hosted runner.
- Copy the token shown in the configuration step, e.g.
--token ABCD1234....
Note
Runner registration tokens expire after one hour. Start the container before the token expires.
Runner Logs
By default the runner writes its output to ~/runner.log inside the container rather than stdout, keeping container logs uncluttered. To follow the log over SSH or a VS Code terminal:
To send runner output to stdout instead (visible via docker logs), set RUNNER_DEBUG to any non-empty value:
Deregistration on Shutdown
When GITHUB_RUNNER_PAT is set, the container automatically deregisters the runner from GitHub when it stops. This keeps the runner list clean when containers are frequently rebuilt or when multiple containers are running simultaneously — each container registers with a unique name and removes itself on exit.
Note
Automatic deregistration requires GITHUB_RUNNER_PAT. If only GITHUB_RUNNER_TOKEN was supplied, the runner will appear offline in GitHub and be removed automatically after 30 days.
GitHub PAT Requirements
Repository Clone Token (GITHUB_TOKEN)
GITHUB_TOKEN is only required when ANSIBLE_REPO_URL points to a private repository. The token is used exclusively to clone the repository and is removed from the git remote URL immediately after cloning.
| PAT Type | Required Permission |
|---|---|
| Classic | repo |
| Fine-grained | Contents → Read |
Runner PAT (GITHUB_RUNNER_PAT)
GITHUB_RUNNER_PAT is a long-lived PAT used to generate a fresh runner registration token at container startup. The container determines whether to use the repository or organization API endpoint based on the number of path segments in GITHUB_RUNNER_URL.
| PAT Type | Runner Scope | Required Permission |
|---|---|---|
| Classic | Repository | repo |
| Classic | Organization | admin:org |
| Fine-grained | Repository | Administration → Write |
| Fine-grained | Organization | Organization self-hosted runners → Write |
Publishing Images
The container images are built locally and pushed to the GitHub Container Registry (ghcr.io). Both variants are published from the same repository under the ghcr.io/sapphire-health/ansible-epic namespace.
Prerequisites
- Git — to clone or pull the repository
- Docker — to build and push images
- On Windows: Docker Desktop must be installed and running in Linux container mode
- GitHub account with write access to the
Sapphire-Health/ansible-epicrepository and thewrite:packagespermission
Authenticate to GHCR
Use the GitHub CLI to authenticate Docker against ghcr.io:
If your current gh token does not have write:packages scope, re-authenticate with it:
Authenticate to ECR
Use the AWS CLI to authenticate Docker against ECR. Your AWS credentials must have ecr:GetAuthorizationToken permission.
ECR tokens expire after 12 hours. Re-run this command if Docker reports an authentication error during a push.
Build the Images
Run from the root of the repository.
Tip
Add --no-cache to any build command to force Docker to pull the latest versions of all packages and tools, ignoring the layer cache. This is useful when you want to pick up updated apt packages, a newer VS Code CLI, or a newer GitHub Actions runner without bumping the pinned version arguments. Omitting it (the default) reuses cached layers for a faster incremental build when only source files changed.
AWS image:
Azure image:
AWS Web image:
Azure Web image:
Note
The Web images layer on top of the base images. Build and push aws / azure before building the aws-web / azure-web variants, or ensure the base images are already available in the registry.
Note
The aws and azure Dockerfiles contain an ARG BUILD_DATE line near the top of the final stage. Updating this value forces Docker to invalidate the cache from that layer onward while keeping the expensive builder-stage cache warm. This is a more targeted alternative to --no-cache when you want to pick up updated apt packages or tool downloads (VS Code CLI, GitHub runner, AWS/Azure CLI).
Push the Images
Push to ECR
AWS image variants (aws, aws-coder, aws-web) can be pushed to Amazon ECR so that ECS pulls them over AWS's internal network rather than from ghcr.io. This eliminates internet egress for image pulls and significantly reduces container startup time.
Create the Repository (first time only)
Tag and Push
Repeat the tag and push for any other variants being mirrored to ECR (e.g. aws-web), substituting the tag name.
Note
No IAM changes are needed. The AmazonECSTaskExecutionRolePolicy already attached to the ECS execution role includes the permissions required to pull images from ECR (ecr:GetAuthorizationToken, ecr:BatchGetImage, etc.).
Update the Task Definition
After pushing to ECR, update the image URI in coder/dev-container/dev-container.tf and coder/gh-runner/gh-runner.tf to point to ECR instead of ghcr.io:
Push the updated template and start a new workspace for the change to take effect.
Redeploy the AWS Container
After pushing, force ECS to pull the new image:
ECS will start a new task using the updated image and stop the old one. The runner in the old container deregisters itself during shutdown before the new container registers a replacement.
Updating the GitHub Actions Runner Version
The runner binary version is pinned in both base Dockerfiles via the GITHUB_RUNNER_VERSION build argument. To update it, change the value in both files and rebuild:
Find the latest release version at github.com/actions/runner/releases.
Updating the Coder Agent
The Coder agent binary is not baked into the image. At container startup, startup.sh downloads the agent directly from $CODER_AGENT_URL/bin/coder-linux-amd64, so it always matches the running Coder server version automatically. No image rebuild is required when the Coder server is upgraded.