2.3. Resources
In this lab you declare your first real-world resources using the Docker provider — a local nginx container with a published port.
Step 1 — Add the Docker provider
Open requirements.txt and add pulumi_docker:
Install the updated dependencies:
Explanation
The Docker provider translates Pulumi resource declarations into Docker API calls — it can pull images, create containers, build images, and manage networks and volumes, all as Pulumi-managed resources.
Like any Pulumi provider, it is a standard Python package. Add it to requirements.txt, pin the version, and
pip install — the same workflow you used for pulumi_random.
Docker must be running
The Docker provider communicates with the local Docker daemon. Make sure Docker Desktop (or the Docker Engine) is
running before running pulumi up.
Step 2 — Pull an nginx image
Replace __main__.py with the following:
Run a preview to see the plan:
Explanation
Every Pulumi resource call shares the same three-part signature:
| Part | Example | Purpose |
|---|---|---|
| Type | docker.RemoteImage | The resource class — determines which API the provider calls |
| Logical name | "nginx-image" | Pulumi’s stable identifier for this resource in state |
| Args | name="nginxdemos/hello:0.4-plain-text" | Provider-specific configuration for the resource |
docker.RemoteImage instructs Docker to pull nginxdemos/hello:0.4-plain-text and records the image reference in
Pulumi state.
Step 3 — Add a Container with a port mapping
Extend __main__.py to declare a container that uses the image:
Apply the changes:
Once the stack is up, verify the container is serving:
You should see a plain-text response with the server address and hostname.
Explanation
The container declaration introduces two key concepts.
Chaining resources — image=image.image_id passes the RemoteImage output directly into the container
declaration. Pulumi sees this reference and ensures the image is pulled before the container is created. No explicit
ordering is needed when you pass an output directly.
Port mapping — ContainerPortArgs(internal=80, external=8080) maps port 80 inside the container to port 8080 on
your machine — the Docker equivalent of -p 8080:80.
keep_locally=True tells the provider not to delete the pulled image when the stack is destroyed — useful during
development where images are large and slow to re-pull.
Step 4 — Understand resource naming
After pulumi up completes, list the running containers:
Compare the container name in the output with the logical name "nginx-container" you declared in code.
Explanation
Every resource has two distinct names:
| Name | Example | Set by |
|---|---|---|
| Logical name | nginx-container | You — the first argument to the resource constructor |
| Physical name | nginx-container-a1b2c3 | Provider — derived from the logical name plus a random suffix |
Pulumi uses the logical name to track the resource across pulumi up runs. The physical name is what actually
appears in Docker (or Azure, or AWS) — the random suffix is added by the provider to avoid collisions.
Why the random suffix?
When Pulumi replaces a resource (delete-and-recreate), the new physical resource must exist briefly alongside the old one. A unique suffix prevents name collisions during that overlap window.
If your resource can’t tolerate two instances existing at the same time, you can tell Pulumi to delete the old one
first with the delete_before_replace resource option — covered in the next step.
You can pin the physical name by setting the provider’s name argument — for a container: name="my-nginx". Do this
only when the name must be stable and predictable; you lose Pulumi’s collision-avoidance guarantee.
Step 5 — Protect a resource from deletion
Add protect=True to the container:
Apply the change:
Now try to tear down the stack:
Observe the error. Then remove protect=True from the code and run pulumi up once more to lift the protection —
pulumi destroy will now succeed.
Explanation
Resource options are passed via opts=pulumi.ResourceOptions(...) and control lifecycle, dependencies, and
protection — independent of any specific provider.
| Option | Effect |
|---|---|
protect=True | Blocks pulumi destroy on this resource — useful for databases and storage accounts |
ignore_changes=["field"] | Skips updates when the named fields drift from the declared values |
depends_on=[res1, res2] | Forces Pulumi to wait for the listed resources even without a direct output reference |
parent=other_resource | Declares logical ownership; child appears nested under parent in the preview tree |
protect=True causes pulumi destroy to fail with an error like:
The only way to remove a protected resource is to set protect=False (or remove the option entirely) and run
pulumi up first — making deletion a deliberate two-step action. In the Azure labs you will add this to the
PostgreSQL server and ACR registry.
ignore_changes is useful when a field is managed outside Pulumi — for example, a tag that operations staff update
directly in the Azure portal. Listing it prevents Pulumi from reverting those manual changes on the next pulumi up.
depends_on vs output chaining
Prefer passing outputs directly between resources over depends_on. Direct references give Pulumi the actual value
(not just an ordering hint) and make the dependency obvious to a reader.
Use depends_on only when there is no value to pass — for example, ensuring a namespace exists before applying a
Kubernetes manifest that references it by string.