1. Learn Pulumi
  2. Pulumi Fundamentals
  3. Configuring and Provisioning Containers

Configuring and Provisioning Containers

Now that we’ve created our images, we can provision our application with a network and containers. First, we’re going to add configuration to our Pulumi program. Pulumi is a tool to configure your infrastructure, and that includes being able to configure the different stacks with different values. As a result, it makes sense to include the basic configurations as variables at the top of your program.

Configure the application

Add the following configuration variables to your Pulumi program:

These configuration declarations go below your imports.

// Get configuration values
const config = new pulumi.Config();
const frontendPort = config.requireNumber("frontendPort");
const backendPort = config.requireNumber("backendPort");
const mongoPort = config.requireNumber("mongoPort");

These configuration declarations go below your imports.

# Get configuration values
config = pulumi.Config()
frontend_port = config.require_int("frontendPort")
backend_port = config.require_int("backendPort")
mongo_port = config.require_int("mongoPort")

First, add this to the end of your import section:

"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"

Run go mod tidy to update the go.mod and go.sum files as necessary.

Next, add these configuration declarations near the top of the main() function, just after the pulumi.Run line.

// Get configuration values
cfg := config.New(ctx, "")
frontendPort := cfg.RequireFloat64("frontendPort")
backendPort := cfg.RequireFloat64("backendPort")
mongoPort := cfg.RequireFloat64("mongoPort")
_ = frontendPort + backendPort + mongoPort

That last line is, again, just to satisfy Go’s requirement that no variables can be declared that aren’t used later. We’ll remove this as we proceed.

These configuration declarations go in the static stack() method:

// Get configuration values
final var config = ctx.config();
final var frontendPort = config.requireInteger("frontendPort");
final var backendPort = config.requireInteger("backendPort");
final var mongoPort = config.requireInteger("mongoPort");

These statements go between the description and the resources where configuration and variables have been:

# Get configuration values
configuration:
  frontendPort:
    type: Number
  backendPort:
    type: Number
  mongoPort:
    type: Number

# Define variables
variables:
  backendImageName: backend
  frontendImageName: frontend

You’ll end up using these configuration values later, passing them as environment variables to the containers.

Your Pulumi program should now match this code:

import * as pulumi from "@pulumi/pulumi";
import * as docker from "@pulumi/docker";

// Get configuration values
const config = new pulumi.Config();
const frontendPort = config.requireNumber("frontendPort");
const backendPort = config.requireNumber("backendPort");
const mongoPort = config.requireNumber("mongoPort");

const stack = pulumi.getStack();

// Pull the backend image
const backendImageName = "backend";
const backend = new docker.RemoteImage(`${backendImageName}Image`, {
    name: "pulumi/tutorial-pulumi-fundamentals-backend:latest",
});

// Pull the frontend image
const frontendImageName = "frontend";
const frontend = new docker.RemoteImage(`${frontendImageName}Image`, {
    name: "pulumi/tutorial-pulumi-fundamentals-frontend:latest",
});

// Pull the MongoDB image
const mongoImage = new docker.RemoteImage("mongoImage", {
    name: "pulumi/tutorial-pulumi-fundamentals-database-local:latest",
});
import pulumi
import pulumi_docker as docker

# Get configuration values
config = pulumi.Config()
frontend_port = config.require_int("frontendPort")
backend_port = config.require_int("backendPort")
mongo_port = config.require_int("mongoPort")

stack = pulumi.get_stack()

# Pull the backend image
backend_image_name = "backend"
backend = docker.RemoteImage(f"{backend_image_name}_image",
                             name="pulumi/tutorial-pulumi-fundamentals-backend:latest"
                            )

# Pull the frontend image
frontend_image_name = "frontend"
frontend = docker.RemoteImage(f"{frontend_image_name}_image",
                              name="pulumi/tutorial-pulumi-fundamentals-frontend:latest"
                             )

# Pull the MongoDB image
mongo_image = docker.RemoteImage("mongo_image",
                                 name="pulumi/tutorial-pulumi-fundamentals-database-local:latest"
                                )
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-docker/sdk/v3/go/docker"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Get configuration values
		cfg := config.New(ctx, "")
		frontendPort := cfg.RequireFloat64("frontendPort")
		backendPort := cfg.RequireFloat64("backendPort")
		mongoPort := cfg.RequireFloat64("mongoPort")
		_ = frontendPort + backendPort + mongoPort

		// Pull the backend image
		backendImageName := "backend"
		backendImage, err := docker.NewRemoteImage(ctx, fmt.Sprintf("%v-image", backendImageName), &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-backend:latest"),
		})
		if err != nil {
			return err
		}
		ctx.Export("backendDockerImage", backendImage.Name)

		// Pull the frontend image
		frontendImageName := "frontend"
		frontendImage, err := docker.NewRemoteImage(ctx, fmt.Sprintf("%v-image", frontendImageName), &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-frontend:latest"),
		})
		if err != nil {
			return err
		}
		ctx.Export("frontendDockerImage", frontendImage.Name)

		// Pull the MongoDB image
		mongoImage, err := docker.NewRemoteImage(ctx, "mongo-image", &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-database-local:latest"),
		})
		if err != nil {
			return err
		}
		ctx.Export("mongoDockerImage", mongoImage.Name)

		return nil
	})
}
package my_first_app;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.docker.RemoteImage;
import com.pulumi.docker.RemoteImageArgs;

import java.util.List;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    private static void stack(Context ctx) {
        // Get configuration values
        final var config = ctx.config();
        final var frontendPort = config.requireInteger("frontendPort");
        final var backendPort = config.requireInteger("backendPort");
        final var mongoPort = config.requireInteger("mongoPort");

        final var stackName = ctx.stackName();

        // Pull the backend image
        final String backendImageName = "backend";
        final var backendImage = new RemoteImage(
                backendImageName,
                RemoteImageArgs.builder()
                        .name(String.format("pulumi/tutorial-pulumi-fundamentals-%s:latest",backendImageName))
                        .build()
        );

        // Pull the frontend image
        final String frontendImageName = "frontend";
        final var frontendImage = new RemoteImage(
                frontendImageName,
                RemoteImageArgs.builder()
                        .name(String.format("pulumi/tutorial-pulumi-fundamentals-%s:latest",frontendImageName))
                        .build()
        );

        // Pull the MongoDB image
        final var mongoImage = new RemoteImage(
                "mongoImage",
                RemoteImageArgs.builder()
                        .name("pulumi/tutorial-pulumi-fundamentals-database-local:latest")
                        .build()
        );
    }
}
name: my_first_app
runtime: yaml
description: A minimal Pulumi YAML program

# Get configuration values
configuration:
  frontendPort:
    type: Number
  backendPort:
    type: Number
  mongoPort:
    type: Number

# Define variables
variables:
  backendImageName: backend
  frontendImageName: frontend
resources:
  # Pull the backend image
  backend-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-backend:latest

  # Pull the frontend image
  frontend-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-frontend:latest

  # Pull the MongoDB image
  mongo-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-database-local:latest
outputs:       {}

Try and run your pulumi up again at this point. You should get an error that looks something like this:

Diagnostics:
  pulumi:pulumi:Stack (my_first_app-dev):
    error: Missing required configuration variable 'my_first_app:frontendPort'
        please set a value using the command `pulumi config set my_first_app:frontendPort <value>`
    error: an unhandled error occurred: <...> exited with non-zero exit code: 1

This is because we have specified that this config option is required. Remember how we can use the same program to define multiple stacks? Let’s set the ports for this stack, which the Pulumi command line knows already from when you first initialized the project (it’s the dev stack by default):

pulumi config set frontendPort 3001
pulumi config set backendPort 3000
pulumi config set mongoPort 27017

This set of commands creates a file in your directory called Pulumi.dev.yaml to store the configuration for this stack.

The content of the file should be like this:

config:
  my_first_app:backendPort: "3000"
  my_first_app:frontendPort: "3001"
  my_first_app:mongoPort: "27017"

Now, try and rerun your Pulumi program with pulumi up command.

Your Pulumi program should now run, but you’re not actually using these newly configured ports just yet! That’s because we don’t have any container resources that use the ports; we only have image resources.

Create a Container resource

In the last topic, we retrieved Docker images from a remote registry. Now we want to create Docker containers and pass these containers the configuration values we just defined. Our containers will need to connect to each other, so we will need to create a Network, which is another resource.

Add the following code at the bottom of your program:

// Create a Docker network
const network = new docker.Network("network", {
    name: `services-${stack}`,
});

Add the following code at the bottom of your program:

# Create a Docker network
network = docker.Network("network", name=f"services_{stack}")

Add this code at the bottom of your program, just before the last return nil statement:

// Create a Docker network
network, err := docker.NewNetwork(ctx, "network", &docker.NetworkArgs{
	Name: pulumi.String(fmt.Sprintf("services-%v", ctx.Stack())),
})
if err != nil {
	return err
}
ctx.Export("containerNetwork", network.Name)

Add these imports to the top:

import com.pulumi.docker.Network;
import com.pulumi.docker.NetworkArgs;
import com.pulumi.docker.Container;
import com.pulumi.docker.ContainerArgs;
import com.pulumi.docker.inputs.ContainerNetworksAdvancedArgs;
import com.pulumi.docker.inputs.ContainerPortArgs;
import com.pulumi.resources.CustomResourceOptions;

Add this code at the bottom:

// Create a Docker Network
final var network = new Network(
        "network",
        NetworkArgs.builder()
                .name(String.format("services-%s",stackName))
                .build()
);

Add the following code at the bottom of your resources section:

# Create a Docker network
network:
  type: docker:index:Network
  properties:
    name: services-${pulumi.stack}

Define a new Container resource in your Pulumi program below the Network resource, like this:

// Create the backend container
const backendContainer = new docker.Container("backendContainer", {
    name: `backend-${stack}`,
    image: backend.repoDigest,
    ports: [
        {
            internal: backendPort,
            external: backendPort,
        },
    ],
    envs: [
        `DATABASE_HOST=${mongoHost}`,
        `DATABASE_NAME=${database}`,
        `NODE_ENV=${nodeEnvironment}`,
    ],
    networksAdvanced: [
        {
            name: network.name,
        },
    ],
}, { dependsOn: [ mongoContainer ]});
# Create the backend container
backend_container = docker.Container("backend_container",
                        name=f"backend-{stack}",
                        image=backend.repo_digest,
                        ports=[docker.ContainerPortArgs(
                            internal=backend_port,
                            external=backend_port)],
                        envs=[
                            f"DATABASE_HOST={mongo_host}",
                            f"DATABASE_NAME={database}",
                            f"NODE_ENV={node_environment}"
                        ],
                        networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                            name=network.name
                        )],
                        opts=pulumi.ResourceOptions(depends_on=[mongo_container])
                        )
// Create the backend container
// Use _ instead of a variable name since this container isn't referenced
_, err = docker.NewContainer(ctx, "backend-container", &docker.ContainerArgs{
	Name:  pulumi.String(fmt.Sprintf("backend-%v", ctx.Stack())),
	Image: backendImage.RepoDigest,
	Ports: &docker.ContainerPortArray{
		&docker.ContainerPortArgs{
			Internal: pulumi.Int(backendPort),
			External: pulumi.Int(backendPort),
		},
	},
	Envs: pulumi.StringArray{
		pulumi.String(fmt.Sprintf("DATABASE_HOST=%v", mongoHost)),
		pulumi.String(fmt.Sprintf("DATABASE_NAME=%v", database)),
		pulumi.String(fmt.Sprintf("NODE_ENV=%v", nodeEnvironment)),
	},
	NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
		&docker.ContainerNetworksAdvancedArgs{
			Name: network.Name,
			Aliases: pulumi.StringArray{
				pulumi.String(fmt.Sprintf("backend-%v", ctx.Stack())),
			},
		},
	},
}, pulumi.DependsOn([]pulumi.Resource{
	mongoContainer,
}))
if err != nil {
	return err
}

Because we now have something that is referencing the backendImage and network resources we defined earlier, we can now remove the ctx.Export statements for those resources from our code.

// Create the backend container
final var backendContainer = new Container(
                "backendContainer",
                ContainerArgs.builder()
                        .name(String.format("backend-%s",stackName))
                        .image(backendImage.repoDigest())
                        .ports(ContainerPortArgs.builder()
                                .internal(backendPort)
                                .external(backendPort)
                                .build())
                        .envs(List.of(
                                String.format("DATABASE_HOST=%s",mongoHost),
                                String.format("DATABASE_NAME=%s",database),
                                String.format("NODE_ENV=%s",nodeEnvironment)
                        ))
                        .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                                .name(network.name())
                                .build()
                        )
                        .build(),
                CustomResourceOptions.builder()
                        .dependsOn(mongoContainer)
                        .build()
        );
# Create the backend container
backend-container:
  type: docker:index:Container
  properties:
    name: ${backendImageName}-${pulumi.stack}
    image: ${backend-image.repoDigest}
    ports:
      - internal: ${backendPort}
        external: ${backendPort}
    envs:
      [
        "DATABASE_HOST=${mongoHost}",
        "DATABASE_NAME=${database}",
        "NODE_ENV=${nodeEnvironment}"
      ]
    networksAdvanced:
      - name: ${network.name}
        aliases: ["${backendImageName}-${pulumi.stack}"]
  options:
    dependsOn:
      - ${mongo-container}

It is important to note something here. In the Container resource, we are referencing repoDigest from the RemoteImage resource. Pulumi now knows there is a dependency between these two resources and will know to create the Container resource after the RemoteImage resource. Another dependency to note is that the backendContainer depends on the mongoContainer. If we tried to run pulumi up without the mongoContainer running or present somewhere in state, Pulumi would let us know that the resource didn’t exist and would stop.

It is important to note something here. In the Container resource, we are referencing repo_digest from the RemoteImage resource. Pulumi now knows there is a dependency between these two resources and will know to create the Container resource after the Image resource. Another dependency to note is that the backend_container depends on the mongo_container. If we tried to run pulumi up without the mongo_container running or present somewhere in state, Pulumi would let us know that the resource didn’t exist and would stop.

It is important to note something here. In the Container resource, we are referencing repoDigest from the RemoteImage resource. Pulumi now knows there is a dependency between these two resources and will know to create the Container resource after the RemoteImage resource. Another dependency to note is that the backendContainer depends on the mongoContainer. If we tried to run pulumi up without the mongoContainer running or present somewhere in state, Pulumi would let us know that the resource didn’t exist and would stop.

It is important to note something here. In the Container resource, we are referencing repoDigest from the RemoteImage resource. Pulumi now knows there is a dependency between these two resources and will know to create the Container resource after the RemoteImage resource. Another dependency to note is that the backendContainer depends on the mongoContainer. If we tried to run pulumi up without the mongoContainer running or present somewhere in state, Pulumi would let us know that the resource didn’t exist and would stop.

It is important to note something here. In the Container resource, we are referencing repoDigest from the RemoteImage resource. Pulumi now knows there is a dependency between these two resources and will know to create the Container resource after the RemoteImage resource. Another dependency to note is that the backend-container depends on the mongo-container. If we tried to run pulumi up without the mongo-container running or present somewhere in state, Pulumi would let us know that the resource didn’t exist and would stop.

It’s also important to note the backend container requires some environment variables to connect to the Mongo container and to set the Node environment for Express.js. These are set in the backend/src/.env file in the tutorial-pulumi-fundamentals repository. We don’t want to hardcode these values; we want them to be configurable. To do that, we’ll need to define some additional configuration values. Like before, we can set them using pulumi config on the command line:

pulumi config set mongoHost mongodb://mongo:27017
pulumi config set database cart
pulumi config set nodeEnvironment development
pulumi config set protocol http://

Then, we need to add them to the top of our program with the rest of the configuration values.

const mongoHost = config.require("mongoHost"); // Note that strings are the default, so it's not `config.requireString`, just `config.require`.
const database = config.require("database");
const nodeEnvironment = config.require("nodeEnvironment");
const protocol = config.require("protocol")
mongo_host = config.require("mongoHost") # Note that strings are the default, so it's not `config.require_str`, just `config.require`.
database = config.require("database")
node_environment = config.require("nodeEnvironment")
protocol = config.require("protocol")
mongoHost := cfg.Require("mongoHost") // Note that strings are the default, so it's not `cfg.RequireStr`, just `cfg.Require`
database := cfg.Require("database")
nodeEnvironment := cfg.Require("nodeEnvironment")
protocol := cfg.Require("protocol")
final var mongoHost = config.require("mongoHost"); // Note that strings are the default, so it's not `config.requireString`, just `config.require`
final var database = config.require("database");
final var nodeEnvironment = config.require("nodeEnvironment");
final var protocol = config.require("protocol");
mongoHost:
  type: string
database:
  type: string
nodeEnvironment:
  type: string
protocol:
  type: string

All these configuration values are set as required, meaning if you forget to set them with pulumi config set, then pulumi up will report an error.

We also need to create Container resources for the frontend and Mongo containers. Put the mongoContainer declaration just before the backendContainer one, and the frontendContainer declaration after the backendContainer declaration. Here’s the code for the Mongo container:

We also need to create Container resources for the frontend and Mongo containers. Put the mongo_container declaration just before the backend_container one, and the frontend_container declaration after backend_container. Here’s the code for the Mongo container:

We also need to create Container resources for the frontend and Mongo containers. Put the mongoContainer declaration just before the declaration for the backend container and declaration for the frontend container just after the declaration for the backend container. Here’s the code for the Mongo container:

We also need to create Container resources for the frontend and Mongo containers. Put the mongoContainer declaration just before the backendContainer one, and the frontendContainer declaration after the declaration for backendContainer. Here’s the code for the Mongo container:

We also need to create Container resources for the frontend and Mongo containers. Put the mongo-container declaration just before the backend-container one, and the frontend-container declaration after the backend-container declaration. Here’s the code for the Mongo container:

// Create the MongoDB container
const mongoContainer = new docker.Container("mongoContainer", {
    image: mongoImage.repoDigest,
    name: `mongo-${stack}`,
    ports: [
        {
            internal: mongoPort,
            external: mongoPort,
        },
    ],
    networksAdvanced: [
        {
            name: network.name,
            aliases: ["mongo"],
        },
    ],
});
# Create the MongoDB container
mongo_container = docker.Container("mongo_container",
                        image=mongo_image.repo_digest,
                        name=f"mongo-{stack}",
                        ports=[docker.ContainerPortArgs(
                          internal=mongo_port,
                          external=mongo_port
                        )],
                        networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                            name=network.name,
                            aliases=["mongo"]
                        )]
                        )
// Create the MongoDB container
mongoContainer, err := docker.NewContainer(ctx, "mongo-container", &docker.ContainerArgs{
	Name:  pulumi.String(fmt.Sprintf("mongo-%v", ctx.Stack())),
	Image: mongoImage.RepoDigest,
	Ports: &docker.ContainerPortArray{
		&docker.ContainerPortArgs{
			Internal: pulumi.Int(mongoPort),
			External: pulumi.Int(mongoPort),
		},
	},
	NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
		&docker.ContainerNetworksAdvancedArgs{
			Name: network.Name,
			Aliases: pulumi.StringArray{
				pulumi.String("mongo"),
			},
		},
	},
})
if err != nil {
	return err
}
// Create the MongoDB container
final var mongoContainer = new Container(
        "mongoContainer",
        ContainerArgs.builder()
                .name(String.format("mongo-%s",stackName))
                .image(mongoImage.repoDigest())
                .ports(ContainerPortArgs.builder()
                        .internal(mongoPort)
                        .external(mongoPort)
                        .build())
                .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                        .name(network.name())
                        .aliases("mongo")
                        .build()
                )
                .build()
);
# Create the MongoDB container
mongo-container:
  type: docker:index:Container
  properties:
    name: mongo-${pulumi.stack}
    image: ${mongo-image.repoDigest}
    ports:
      - internal: ${mongoPort}
        external: ${mongoPort}
    networksAdvanced:
      - name: ${network.name}
        aliases: ["mongo"]

And the code for the frontend container:

// Create the frontend container
const frontendContainer = new docker.Container("frontendContainer", {
    image: frontend.repoDigest,
    name: `frontend-${stack}`,
    ports: [
        {
            internal: frontendPort,
            external: frontendPort,
        },
    ],
    envs: [
        `PORT=${frontendPort}`,
        `HTTP_PROXY=backend-${stack}:${backendPort}`,
        `PROXY_PROTOCOL=${protocol}`
    ],
    networksAdvanced: [
        {
            name: network.name,
        },
    ],
});
# Create the frontend container
frontend_container = docker.Container("frontend_container",
                                      image=frontend.repo_digest,
                                      name=f"frontend-{stack}",
                                      ports=[docker.ContainerPortArgs(
                                          internal=frontend_port,
                                          external=frontend_port
                                      )],
                                      envs=[
                                          f"PORT={frontend_port}",
                                          f"HTTP_PROXY=backend-{stack}:{backend_port}",
                                          f"PROXY_PROTOCOL={protocol}"
                                      ],
                                      networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                                          name=network.name
                                      )]
                                      )
// Create the frontend container
_, err = docker.NewContainer(ctx, "frontend-container", &docker.ContainerArgs{
	Name:  pulumi.String(fmt.Sprintf("frontend-%v", ctx.Stack())),
	Image: frontendImage.RepoDigest,
	Ports: &docker.ContainerPortArray{
		&docker.ContainerPortArgs{
			Internal: pulumi.Int(frontendPort),
			External: pulumi.Int(frontendPort),
		},
	},
	Envs: pulumi.StringArray{
		pulumi.String(fmt.Sprintf("PORT=%v", frontendPort)),
		pulumi.String(fmt.Sprintf("HTTP_PROXY=backend-%v:%v", ctx.Stack(), backendPort)),
		pulumi.String(fmt.Sprintf("PROXY_PROTOCOL=%v", protocol)),
	},
	NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
		&docker.ContainerNetworksAdvancedArgs{
			Name: network.Name,
			Aliases: pulumi.StringArray{
				pulumi.String(fmt.Sprintf("frontend-%v", ctx.Stack())),
			},
		},
	},
})
if err != nil {
	return err
}

At this point, all the variables we’ve declared have been used, so you can remove all the ctx.Export statements, and remove this line from the configuration declarations near the top of your program:

_ = frontendPort + backendPort + mongoPort

We used these lines to temporarily satisfy Go’s requirements as we were building out the program; now that our program is complete, they are no longer needed.

// Create the frontend container
final var frontendContainer = new Container(
        "frontendContainer",
        ContainerArgs.builder()
                .name(String.format("frontend-%s",stackName))
                .image(frontendImage.repoDigest())
                .ports(ContainerPortArgs.builder()
                        .internal(frontendPort)
                        .external(frontendPort)
                        .build())
                .envs(List.of(
                        String.format("PORT=%d",frontendPort),
                        String.format("HTTP_PROXY=backend-%s:%d",stackName,backendPort),
                        String.format("PROXY_PROTOCOL=%s",protocol)
                ))
                .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                        .name(network.name())
                        .build())
                .build()
);
# Create the frontend container
frontend-container:
  type: docker:index:Container
  properties:
    name: ${frontendImageName}-${pulumi.stack}
    image: ${frontend-image.repoDigest}
    ports:
      - internal: ${frontendPort}
        external: ${frontendPort}
    envs:
      [
        "PORT=${frontendPort}",
        "HTTP_PROXY=backend-${pulumi.stack}:${backendPort}",
        "PROXY_PROTOCOL=${protocol}"
      ]
    networksAdvanced:
      - name: ${network.name}
        aliases: ["${frontendImageName}-${pulumi.stack}"]

Let’s see what the whole program looks like next.

Put it all together

Now that we know how to create a container we can complete our program.

import * as pulumi from "@pulumi/pulumi";
import * as docker from "@pulumi/docker";

// Get configuration values
const config = new pulumi.Config();
const frontendPort = config.requireNumber("frontendPort");
const backendPort = config.requireNumber("backendPort");
const mongoPort = config.requireNumber("mongoPort");
const mongoHost = config.require("mongoHost"); // Note that strings are the default, so it's not `config.requireString`, just `config.require`.
const database = config.require("database");
const nodeEnvironment = config.require("nodeEnvironment");
const protocol = config.require("protocol")

const stack = pulumi.getStack();

// Pull the backend image
const backendImageName = "backend";
const backend = new docker.RemoteImage(`${backendImageName}Image`, {
    name: "pulumi/tutorial-pulumi-fundamentals-backend:latest",
});

// Pull the frontend image
const frontendImageName = "frontend";
const frontend = new docker.RemoteImage(`${frontendImageName}Image`, {
    name: "pulumi/tutorial-pulumi-fundamentals-frontend:latest",
});

// Pull the MongoDB image
const mongoImage = new docker.RemoteImage("mongoImage", {
    name: "pulumi/tutorial-pulumi-fundamentals-database-local:latest",
});

// Create a Docker network
const network = new docker.Network("network", {
    name: `services-${stack}`,
});

// Create the MongoDB container
const mongoContainer = new docker.Container("mongoContainer", {
    image: mongoImage.repoDigest,
    name: `mongo-${stack}`,
    ports: [
        {
            internal: mongoPort,
            external: mongoPort,
        },
    ],
    networksAdvanced: [
        {
            name: network.name,
            aliases: ["mongo"],
        },
    ],
});

// Create the backend container
const backendContainer = new docker.Container("backendContainer", {
    name: `backend-${stack}`,
    image: backend.repoDigest,
    ports: [
        {
            internal: backendPort,
            external: backendPort,
        },
    ],
    envs: [
        `DATABASE_HOST=${mongoHost}`,
        `DATABASE_NAME=${database}`,
        `NODE_ENV=${nodeEnvironment}`,
    ],
    networksAdvanced: [
        {
            name: network.name,
        },
    ],
}, { dependsOn: [ mongoContainer ]});

// Create the frontend container
const frontendContainer = new docker.Container("frontendContainer", {
    image: frontend.repoDigest,
    name: `frontend-${stack}`,
    ports: [
        {
            internal: frontendPort,
            external: frontendPort,
        },
    ],
    envs: [
        `PORT=${frontendPort}`,
        `HTTP_PROXY=backend-${stack}:${backendPort}`,
        `PROXY_PROTOCOL=${protocol}`
    ],
    networksAdvanced: [
        {
            name: network.name,
        },
    ],
});
import pulumi
import pulumi_docker as docker

# Get configuration values
config = pulumi.Config()
frontend_port = config.require_int("frontendPort")
backend_port = config.require_int("backendPort")
mongo_port = config.require_int("mongoPort")
mongo_host = config.require("mongoHost") # Note that strings are the default, so it's not `config.require_str`, just `config.require`.
database = config.require("database")
node_environment = config.require("nodeEnvironment")
protocol = config.require("protocol")

stack = pulumi.get_stack()

# Pull the backend image
backend_image_name = "backend"
backend = docker.RemoteImage(f"{backend_image_name}_image",
                             name="pulumi/tutorial-pulumi-fundamentals-backend:latest"
                            )

# Pull the frontend image
frontend_image_name = "frontend"
frontend = docker.RemoteImage(f"{frontend_image_name}_image",
                              name="pulumi/tutorial-pulumi-fundamentals-frontend:latest"
                             )

# Pull the MongoDB image
mongo_image = docker.RemoteImage("mongo_image",
                                 name="pulumi/tutorial-pulumi-fundamentals-database-local:latest"
                                )

# Create a Docker network
network = docker.Network("network", name=f"services_{stack}")

# Create the MongoDB container
mongo_container = docker.Container("mongo_container",
                        image=mongo_image.repo_digest,
                        name=f"mongo-{stack}",
                        ports=[docker.ContainerPortArgs(
                          internal=mongo_port,
                          external=mongo_port
                        )],
                        networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                            name=network.name,
                            aliases=["mongo"]
                        )]
                        )

# Create the backend container
backend_container = docker.Container("backend_container",
                        name=f"backend-{stack}",
                        image=backend.repo_digest,
                        ports=[docker.ContainerPortArgs(
                            internal=backend_port,
                            external=backend_port)],
                        envs=[
                            f"DATABASE_HOST={mongo_host}",
                            f"DATABASE_NAME={database}",
                            f"NODE_ENV={node_environment}"
                        ],
                        networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                            name=network.name
                        )],
                        opts=pulumi.ResourceOptions(depends_on=[mongo_container])
                        )

# Create the frontend container
frontend_container = docker.Container("frontend_container",
                                      image=frontend.repo_digest,
                                      name=f"frontend-{stack}",
                                      ports=[docker.ContainerPortArgs(
                                          internal=frontend_port,
                                          external=frontend_port
                                      )],
                                      envs=[
                                          f"PORT={frontend_port}",
                                          f"HTTP_PROXY=backend-{stack}:{backend_port}",
                                          f"PROXY_PROTOCOL={protocol}"
                                      ],
                                      networks_advanced=[docker.ContainerNetworksAdvancedArgs(
                                          name=network.name
                                      )]
                                      )
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-docker/sdk/v3/go/docker"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Get configuration values
		cfg := config.New(ctx, "")
		frontendPort := cfg.RequireFloat64("frontendPort")
		backendPort := cfg.RequireFloat64("backendPort")
		mongoPort := cfg.RequireFloat64("mongoPort")
		mongoHost := cfg.Require("mongoHost") // Note that strings are the default, so it's not `cfg.RequireStr`, just `cfg.Require`
		database := cfg.Require("database")
		nodeEnvironment := cfg.Require("nodeEnvironment")
		protocol := cfg.Require("protocol")

		// Pull the backend image
		backendImageName := "backend"
		backendImage, err := docker.NewRemoteImage(ctx, fmt.Sprintf("%v-image", backendImageName), &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-backend:latest"),
		})
		if err != nil {
			return err
		}

		// Pull the frontend image
		frontendImageName := "frontend"
		frontendImage, err := docker.NewRemoteImage(ctx, fmt.Sprintf("%v-image", frontendImageName), &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-frontend:latest"),
		})
		if err != nil {
			return err
		}

		// Pull the MongoDB image
		mongoImage, err := docker.NewRemoteImage(ctx, "mongo-image", &docker.RemoteImageArgs{
			Name: pulumi.String("pulumi/tutorial-pulumi-fundamentals-database-local:latest"),
		})
		if err != nil {
			return err
		}

		// Create a Docker network
		network, err := docker.NewNetwork(ctx, "network", &docker.NetworkArgs{
			Name: pulumi.String(fmt.Sprintf("services-%v", ctx.Stack())),
		})
		if err != nil {
			return err
		}

		// Create the MongoDB container
		mongoContainer, err := docker.NewContainer(ctx, "mongo-container", &docker.ContainerArgs{
			Name:  pulumi.String(fmt.Sprintf("mongo-%v", ctx.Stack())),
			Image: mongoImage.RepoDigest,
			Ports: &docker.ContainerPortArray{
				&docker.ContainerPortArgs{
					Internal: pulumi.Int(mongoPort),
					External: pulumi.Int(mongoPort),
				},
			},
			NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
				&docker.ContainerNetworksAdvancedArgs{
					Name: network.Name,
					Aliases: pulumi.StringArray{
						pulumi.String("mongo"),
					},
				},
			},
		})
		if err != nil {
			return err
		}

		// Create the backend container
		// Use _ instead of a variable name since this container isn't referenced
		_, err = docker.NewContainer(ctx, "backend-container", &docker.ContainerArgs{
			Name:  pulumi.String(fmt.Sprintf("backend-%v", ctx.Stack())),
			Image: backendImage.RepoDigest,
			Ports: &docker.ContainerPortArray{
				&docker.ContainerPortArgs{
					Internal: pulumi.Int(backendPort),
					External: pulumi.Int(backendPort),
				},
			},
			Envs: pulumi.StringArray{
				pulumi.String(fmt.Sprintf("DATABASE_HOST=%v", mongoHost)),
				pulumi.String(fmt.Sprintf("DATABASE_NAME=%v", database)),
				pulumi.String(fmt.Sprintf("NODE_ENV=%v", nodeEnvironment)),
			},
			NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
				&docker.ContainerNetworksAdvancedArgs{
					Name: network.Name,
					Aliases: pulumi.StringArray{
						pulumi.String(fmt.Sprintf("backend-%v", ctx.Stack())),
					},
				},
			},
		}, pulumi.DependsOn([]pulumi.Resource{
			mongoContainer,
		}))
		if err != nil {
			return err
		}

		// Create a frontend container
		_, err = docker.NewContainer(ctx, "frontend-container", &docker.ContainerArgs{
			Name:  pulumi.String(fmt.Sprintf("frontend-%v", ctx.Stack())),
			Image: frontendImage.RepoDigest,
			Ports: &docker.ContainerPortArray{
				&docker.ContainerPortArgs{
					Internal: pulumi.Int(frontendPort),
					External: pulumi.Int(frontendPort),
				},
			},
			Envs: pulumi.StringArray{
				pulumi.String(fmt.Sprintf("PORT=%v", frontendPort)),
				pulumi.String(fmt.Sprintf("HTTP_PROXY=backend-%v:%v", ctx.Stack(), backendPort)),
				pulumi.String(fmt.Sprintf("PROXY_PROTOCOL=%v", protocol)),
			},
			NetworksAdvanced: &docker.ContainerNetworksAdvancedArray{
				&docker.ContainerNetworksAdvancedArgs{
					Name: network.Name,
					Aliases: pulumi.StringArray{
						pulumi.String(fmt.Sprintf("frontend-%v", ctx.Stack())),
					},
				},
			},
		})
		if err != nil {
			return err
		}

		return nil
	})
}
package my_first_app;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.docker.RemoteImage;
import com.pulumi.docker.RemoteImageArgs;
import com.pulumi.docker.Network;
import com.pulumi.docker.NetworkArgs;
import com.pulumi.docker.Container;
import com.pulumi.docker.ContainerArgs;
import com.pulumi.docker.inputs.ContainerNetworksAdvancedArgs;
import com.pulumi.docker.inputs.ContainerPortArgs;
import com.pulumi.resources.CustomResourceOptions;

import java.util.List;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    private static void stack(Context ctx) {
        // Get configuration values
        final var config = ctx.config();
        final var frontendPort = config.requireInteger("frontendPort");
        final var backendPort = config.requireInteger("backendPort");
        final var mongoPort = config.requireInteger("mongoPort");
        final var mongoHost = config.require("mongoHost");
        final var database = config.require("database");
        final var nodeEnvironment = config.require("nodeEnvironment");
        final var protocol = config.require("protocol");

        final var stackName = ctx.stackName();

        // Create the backend image
        final String backendImageName = "backend";
        final var backendImage = new RemoteImage(
                backendImageName,
                RemoteImageArgs.builder()
                        .name(String.format("pulumi/tutorial-pulumi-fundamentals-%s:latest",backendImageName))
                        .build()
        );

        // Create the frontend image
        final String frontendImageName = "frontend";
        final var frontendImage = new RemoteImage(
                frontendImageName,
                RemoteImageArgs.builder()
                        .name(String.format("pulumi/tutorial-pulumi-fundamentals-%s:latest",frontendImageName))
                        .build()
        );

        // Create the MongoDB image
        final var mongoImage = new RemoteImage(
                "mongoImage",
                RemoteImageArgs.builder()
                        .name("pulumi/tutorial-pulumi-fundamentals-database-local:latest")
                        .build()
        );

        // Create a Docker network
        final var network = new Network(
                "network",
                NetworkArgs.builder()
                        .name(String.format("services-%s",stackName))
                        .build()
        );

        // Create the MongoDB container
        final var mongoContainer = new Container(
                "mongoContainer",
                ContainerArgs.builder()
                        .name(String.format("mongo-%s",stackName))
                        .image(mongoImage.repoDigest())
                        .ports(ContainerPortArgs.builder()
                                .internal(mongoPort)
                                .external(mongoPort)
                                .build())
                        .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                                .name(network.name())
                                .aliases("mongo")
                                .build()
                        )
                        .build()
        );

        // Create the backend container
        final var backendContainer = new Container(
                "backendContainer",
                ContainerArgs.builder()
                        .name(String.format("backend-%s",stackName))
                        .image(backendImage.repoDigest())
                        .ports(ContainerPortArgs.builder()
                                .internal(backendPort)
                                .external(backendPort)
                                .build())
                        .envs(List.of(
                                String.format("DATABASE_HOST=%s",mongoHost),
                                String.format("DATABASE_NAME=%s",database),
                                String.format("NODE_ENV=%s",nodeEnvironment)
                        ))
                        .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                                .name(network.name())
                                .build()
                        )
                        .build(),
                CustomResourceOptions.builder()
                        .dependsOn(mongoContainer)
                        .build()
        );

        // Create the frontend container
        final var frontendContainer = new Container(
                "frontendContainer",
                ContainerArgs.builder()
                        .name(String.format("frontend-%s",stackName))
                        .image(frontendImage.repoDigest())
                        .ports(ContainerPortArgs.builder()
                                .internal(frontendPort)
                                .external(frontendPort)
                                .build()
                        )
                        .envs(List.of(
                                String.format("PORT=%d",frontendPort),
                                String.format("HTTP_PROXY=backend-%s:%d",stackName,backendPort),
                                String.format("PROXY_PROTOCOL=%s",protocol)
                        ))
                        .networksAdvanced(ContainerNetworksAdvancedArgs.builder()
                                .name(network.name())
                                .build())
                        .build()
        );

        ctx.export("link", Output.of("http://localhost:3001"));
    }
}
name: my_first_app
runtime: yaml
description: A minimal Pulumi YAML program

# Get configuration values
configuration:
  frontendPort:
    type: Number
  backendPort:
    type: Number
  mongoPort:
    type: Number
  mongoHost:
    type: String
  database:
    type: String
  nodeEnvironment:
    type: String
  protocol:
    type: String

# Define variables
variables:
  backendImageName: backend
  frontendImageName: frontend

resources:
  # Pull the backend image
  backend-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-backend:latest

  # Pull the frontend image
  frontend-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-frontend:latest

  # Pull the MongoDB image
  mongo-image:
    type: docker:index:RemoteImage
    properties:
      name: pulumi/tutorial-pulumi-fundamentals-database-local:latest

  # Create a Docker network
  network:
    type: docker:index:Network
    properties:
      name: services-${pulumi.stack}

  # Create the MongoDB container
  mongo-container:
    type: docker:index:Container
    properties:
      name: mongo-${pulumi.stack}
      image: ${mongo-image.repoDigest}
      ports:
        - internal: ${mongoPort}
          external: ${mongoPort}
      networksAdvanced:
        - name: ${network.name}
          aliases: ["mongo"]

  # Create the backend container
  backend-container:
    type: docker:index:Container
    properties:
      name: ${backendImageName}-${pulumi.stack}
      image: ${backend-image.repoDigest}
      ports:
        - internal: ${backendPort}
          external: ${backendPort}
      envs:
        [
          "DATABASE_HOST=${mongoHost}",
          "DATABASE_NAME=${database}",
          "NODE_ENV=${nodeEnvironment}"
        ]
      networksAdvanced:
        - name: ${network.name}
          aliases: ["${backendImageName}-${pulumi.stack}"]
    options:
      dependsOn:
        - ${mongo-container}

  # Create the frontend container
  frontend-container:
    type: docker:index:Container
    properties:
      name: ${frontendImageName}-${pulumi.stack}
      image: ${frontend-image.repoDigest}
      ports:
        - internal: ${frontendPort}
          external: ${frontendPort}
      envs:
        [
          "PORT=${frontendPort}",
          "HTTP_PROXY=backend-${pulumi.stack}:${backendPort}",
          "PROXY_PROTOCOL=${protocol}"
        ]
      networksAdvanced:
        - name: ${network.name}
          aliases: ["${frontendImageName}-${pulumi.stack}"]
outputs:       {}

With Docker networking, we can use image names to refer to a container. In our example, the React frontend client sends requests to the Express backend client. The URL to the backend is set via the setupProxy.js file in the app/frontend/src directory with the HTTP_PROXY environment variable.

Run pulumi up to get the application running. Open a browser to http://localhost:3001, and our application is now deployed.

Update the database

What if we want to add to the products on the page? We can POST to the API just as we would any API. Generally speaking, you would typically wire the database to an API and update it that way with any cloud, so we’re going to do exactly that here.

Open a terminal and run the following command.

curl --location --request POST 'http://localhost:3000/api/products' \
--header 'Content-Type: application/json' \
--data-raw '{
    "ratings": {
        "reviews": [],
        "total": 63,
        "avg": 5
    },
    "created": 1600979464567,
    "currency": {
        "id": "USD",
        "format": "$"
    },
    "sizes": [
        "M",
        "L"
    ],
    "category": "boba",
    "teaType": 2,
    "status": 1,
    "_id": "5f6d025008a1b6f0e5636bc7",
    "images": [
        {
            "src": "classic_boba.png"
        }
    ],
    "name": "My New Milk Tea",
    "price": 5,
    "description": "none",
    "productCode": "852542-107"
}'

You should get back the following response:

{"status":"ok","data":{"product":{"ratings":{"reviews":[],"total":63,"avg":5},"created":1600979464567,"currency":{"id":"USD","format":"$"},"sizes":["M","L"],"category":"boba","teaType":2,"status":1,"_id":"5f6d025008a1b6f0e5636bc7","images":[{"_id":"62608f2a9ad5d90026847b0f","src":"classic_boba.png"}],"name":"My New Milk Tea","price":5,"description":"none","productCode":"852542-107","__v":0}}}

Refresh the app on http://localhost:3001, and our data is now updated!

Cleaning up

Whenever you’re working on learning something new with Pulumi, it’s always a good idea to clean up any resources you’ve created so you don’t get charged on a free tier or otherwise leave behind resources you’ll never use. Let’s clean up.

Run the pulumi destroy command to remove all of the resources:

$ pulumi destroy
Previewing destroy (dev)

View Live: https://app.pulumi.com/<org>/<project>/<stack>/previews/<build-id>

...
Do you want to perform this destroy? yes
Destroying (dev)

View Live: https://app.pulumi.com/<org>/<project>/<stack>/updates/<update-id>

...

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run 'pulumi stack rm dev'.

Now your resources should all be cleared! That last comment you see in the output notes that the stack and all of the configuration and history will stay in your dashboard on the Pulumi Service (app.pulumi.com). For now, that’s okay. We’ll talk more about removing the project from your history in another pathway.


Congratulations, you’ve now finished Pulumi Fundamentals! You learned to create a Pulumi project; work on your Pulumi program to build Docker images, containers, and networks; and deploy the infrastructure locally with your first resource provider. Now, head back to the main page and explore some other tutorials to understand more about Pulumi. The best next step to take is to explore the Building with Pulumi pathway.