Skip to content

Docker.Container reference

Source: packages/docker/src/Container.ts

A Docker container managed through the active Docker context.

This resource creates, starts, stops, inspects, and removes containers through the Docker CLI. It is not interchangeable with Cloudflare.Container, which manages Cloudflare’s container platform; use pushed image references to bridge Docker-built images into cloud container runtimes.

const nginx = yield* Docker.Container("nginx", {
image: "nginx:alpine",
ports: [{ external: 8080, internal: 80 }],
start: true,
});
const password = yield* Config.Redacted("POSTGRES_PASSWORD");
const db = yield* Docker.Container("postgres", {
image: "postgres:18-alpine",
environment: {
POSTGRES_PASSWORD: password,
},
start: true,
});
const app = yield* Docker.Container("app", {
image: "ghcr.io/acme/app:latest",
envFiles: ["./config/base.env", "./config/production.env"],
// Explicit values are passed after env files and take precedence.
environment: { LOG_LEVEL: "info" },
});

Editing an env file replaces the container on the next deploy, as does adopting a container that uses env files (once, so later edits are tracked). Alchemy reads the files at plan time but keeps only a digest of their contents, on the container’s own label; neither the values nor the digest are written to Alchemy state. Docker exposes the resolved values through docker inspect, so treat env files as secrets.

const network = yield* Docker.Network("app-network");
const data = yield* Docker.Volume("postgres-data");
const postgresName = "app-postgres";
yield* Docker.Container("postgres", {
name: postgresName,
image: "postgres:18-alpine",
ports: [{ external: 15432, internal: 5432 }],
volumes: [{ hostPath: data.name, containerPath: "/var/lib/postgresql/data" }],
networks: [{ name: network.name, aliases: ["postgres"] }],
start: true,
});
const runtime = yield* Docker.inspectContainer(postgresName);

Docker.Network owns subnets, IPAM and driver settings for the shared network. The container declares only its own attachment: alias, optional static IP, gateway priority and endpoint driver options. Docker allocates IPs when omitted.

const network = yield* Docker.Network("backend", {
enableIPv6: true,
ipam: { config: [
{ subnet: "10.51.0.0/24" },
{ subnet: "fd51:3456:789a::/64" },
] },
});
yield* Docker.Container("api", {
image: "nginx:alpine",
hostname: "api",
networks: [{
name: network.name,
aliases: ["api"],
ipv4Address: "10.51.0.10",
ipv6Address: "fd51:3456:789a::10",
gatewayPriority: 1,
}],
start: true,
});

Declaring networks without networkMode attaches only the listed networks: Docker’s implicit default bridge is not added. On legacy/adopted containers, Alchemy detaches that implicit bridge while keeping externally added networks. Omitting both options retains Docker’s ordinary default networking. Each attachment must refer to a distinct Docker network, even when the same network is referenced once by name and again by ID.

Network attachment changes reconnect only the managed endpoint without replacing the container. Docker can temporarily interrupt traffic during reconnection. If Docker rejects an update, Alchemy attempts to restore the previously observed attachment; restoration can also fail if Docker refuses it, so the update is not atomic. Alchemy leaves externally attached networks untouched. hostname, dns, dnsSearch and dnsOptions are creation-time settings and changes replace the container. Invalid options fail with InvalidContainerOptions, whose path identifies the rejected configuration property before replacement.

extraHosts writes lines into the container’s /etc/hosts; it changes name resolution and nothing else. Docker’s host-gateway alias resolves to the host machine, which is how a container reaches a service on the developer’s loopback.

On Linux host-gateway is the Docker bridge gateway (typically 172.17.0.1), so a container’s packets to it arrive on the host’s INPUT chain. Under a default-deny firewall — ufw ships DEFAULT_INPUT_POLICY="DROP" — the hostname resolves correctly and the connection then times out, which reads like an application bug rather than a firewall one. Allow the bridge subnet to fix it: sudo ufw allow from 172.16.0.0/12.

Reach a service on the developer’s machine

const api = yield* Docker.Container("api", {
image: "ghcr.io/acme/api:latest",
// `host-gateway` resolves to the host machine, so a database listening
// on the developer's loopback is reachable from inside the container.
extraHosts: ["host.docker.internal:host-gateway"],
environment: {
DATABASE_URL: "postgres://postgres@host.docker.internal:5432/app",
},
start: true,
});

Pin a hostname to a fixed address

const api = yield* Docker.Container("api", {
image: "ghcr.io/acme/api:latest",
// Any `hostname:address` pair — host access is just the common case.
extraHosts: ["service.example:192.0.2.10"],
start: true,
});

Allocate shared memory for PostgreSQL

const password = yield* Alchemy.makeRandom("DatabasePassword", { bytes: 16 });
yield* Docker.Container("db", {
image: "postgres:18-alpine",
environment: { POSTGRES_PASSWORD: password },
sharedMemorySize: "256m", // --shm-size, not the container memory limit
start: true,
});

Configure process, platform, and namespace settings

yield* Docker.Container("job", {
image: "alpine:3.22",
platform: "linux/amd64",
entrypoint: "/bin/sh",
command: ["-c", "echo ready"],
workingDirectory: "/workspace",
ipcNamespace: "private",
sysctls: { "net.ipv4.ip_unprivileged_port_start": "1024" },
});

Contain a background worker

const worker = yield* Docker.Container("worker", {
image: "oven/bun:1",
memory: "512m",
memorySwap: "512m",
memoryReservation: "256m",
cpus: 0.5,
pidsLimit: 128,
readOnlyRootFilesystem: true,
capDrop: ["ALL"],
noNewPrivileges: true,
init: true,
user: "1000:1000",
tmpfs: [{ path: "/tmp", size: "32m", mode: 0o1777 }],
});

Customize confinement, process limits, and log rotation

const api = yield* Docker.Container("api", {
image: "ghcr.io/acme/api:latest",
seccompProfile: "./security/api-seccomp.json", // client-side JSON; edits roll the container
appArmorProfile: "my-loaded-profile", // must already be loaded on the host
groupAdd: ["1000"],
ulimits: { nofile: { soft: 1024, hard: 4096 } },
logDriver: "local",
logOptions: { "max-size": "10m", "max-file": "3" },
oomScoreAdjustment: 200,
stopSignal: "SIGTERM",
});

Share a donor container’s network namespace

const donor = yield* Docker.Container("donor", { image: "redis:alpine" });
const sidecar = yield* Docker.Container("sidecar", {
image: "busybox:latest",
networkMode: { container: donor.id },
});

Add capabilities and devices

const worker = yield* Docker.Container("worker", {
image: "ubuntu:latest",
capAdd: ["SYS_ADMIN"],
devices: [{ hostPath: "/dev/fuse", containerPath: "/dev/fuse" }],
});

Publish on any free host port

const api = yield* Docker.Container("api", {
image: "ghcr.io/acme/api:latest",
// `external: 0` lets Docker choose; the assigned port is reported back.
ports: [{ external: 0, internal: 3000 }],
start: true,
});
const hostPort = api.ports["3000/tcp"];

Route a container through Traefik

const api = yield* Docker.Container("api", {
image: "ghcr.io/acme/api:latest",
networks: [{ name: "traefik" }],
labels: {
"traefik.enable": "true",
"traefik.http.routers.api.rule": "Host(`api.example.com`)",
"traefik.http.services.api.loadbalancer.server.port": "3000",
},
stopTimeout: "30 seconds",
start: true,
});

Use a Docker.Context resource

const remote = yield* Docker.Context("remote", {
name: "remote-build",
docker: "host=ssh://docker@example.com",
});
const api = yield* Docker.Container("api", {
image: "nginx:alpine",
context: remote,
});

Container: Looking up an existing container

Section titled “Container: Looking up an existing container”

Container.lookup references a container Alchemy doesn’t manage (created with the Docker CLI, Compose or another stack). It is read during plan/deploy, never created, updated or removed.

const db = Docker.Container.lookup({ name: "postgres" });