---
title: "Use containers for development"
keywords: get started, go, golang, local, development
description: Learn how to develop your application locally.
redirect_from:
- /get-started/golang/develop/
---

{% include_relative nav.html selected="3" %}

## Prerequisites

Work through the steps of the [run your image as a container](run-containers.md) module to learn how to manage the lifecycle of your containers.

## Introduction

In this module, we'll take a look at running a database engine in a container and connecting it to the extended version of the example application. We are going to see some options for keeping persistent data and for wiring up the containers to talk to one another. Finally, we'll learn how to use Docker Compose to manage such multi-container local development environments effectively.

## Local database and containers

The database engine we are going to use is called [CockroachDB](https://www.cockroachlabs.com/product/). It is a modern, Cloud-native, distributed SQL database.

Instead of compiling CockroachDB from the source code or using the operating system's native package manager to install CockroachDB, we are going to use the [Docker image for CockroachDB](https://hub.docker.com/r/cockroachdb/cockroach) and run it in a container.

CockroachDB is compatible with PostgreSQL to a significant extent, and shares many conventions with the latter, particularly the default names for the environment variables. So, if you are familiar with Postgres, don't be surprised that we are going to use some familiar environment variables names. The Go modules that work with Postgres, such as [pgx](https://pkg.go.dev/github.com/jackc/pgx), [pq](https://pkg.go.dev/github.com/lib/pq), [GORM](https://gorm.io/index.html), and [upper/db](https://upper.io/v4/) also work with CockroachDB.

For more information on the relation between Go and CockroachDB, please refer to the [CockroachDB documentation](https://www.cockroachlabs.com/docs/v20.2/build-a-go-app-with-cockroachdb.html), although this is not necessary to continue with the present guide.

### Storage

The point of a database is to have a persistent store of data. [Volumes](../../storage/volumes.md) are the preferred mechanism for persisting data generated by and used by Docker containers. Thus, before we start CockroachDB, let's create the volume for it.

To create a managed volume, run :

```console
$ docker volume create roach
roach
```

We can view the list of all managed volumes in our Docker instance with the following command:

```console
$ docker volume list
DRIVER    VOLUME NAME
local     roach
```

### Networking

The example application and the database engine are going to talk to one another over the network. There are different kinds of network configuration possible, and we are going to use what is called a user-defined _bridge network_. It is going to provide us with a DNS lookup service so that we can refer to our database engine container by its hostname.

The following command creates a new bridge network named `mynet`:

```console
$ docker network create -d bridge mynet
51344edd6430b5acd121822cacc99f8bc39be63dd125a3b3cd517b6485ab7709
```

As it was the case with the managed volumes, there is a command to list all networks set up in your Docker instance:

```console
$ docker network list
NETWORK ID     NAME          DRIVER    SCOPE
0ac2b1819fa4   bridge        bridge    local
51344edd6430   mynet         bridge    local
daed20bbecce   host          host      local
6aee44f40a39   none          null      local
```

Our bridge network `mynet` has been created successfully. The other three networks, named `bridge`, `host`, and `none` are the _default_ networks and they had been created by the Docker itself. While it is not relevant to our current discussion, you can learn more about Docker networking in the [networking overview](../../network/index.md) section.

### Choose good names for volumes and networks

As the saying goes, there are only two hard things in Computer Science: cache invalidation and naming things. And off-by-one errors.

When choosing a name for a network or a managed volume, it's best to choose a name that is indicative of the intended purpose. In this module, though, we aimed for brevity, so we settled for short, generic names.

### Start the database engine

Now that the housekeeping chores are done, we can run CockroachDB in a container and attach it to the volume and network we had just created. When you run the folllowing command, Docker will pull the image from Docker Hub and run it for you locally:

```console
$ docker run -d \
  --name roach \
  --hostname db \
  --network mynet \
  -p 26257:26257 \
  -p 8080:8080 \
  -v roach:/cockroach/cockroach-data \
  cockroachdb/cockroach:latest-v20.1 start-single-node \
  --insecure

# ... output omitted ...
```

Notice a clever use of the tag `latest-v20.1` to make sure that we are pulling the latest patch version of 20.1.  The diversity of available tags depend on the image maintainer. Here, our intent was to have the latest patched version of CockroachDB while not straying too far away from the known working version as the time goes by. To see the tags available for CockroachDB image, we went to [CockroachDB page on Docker Hub](https://hub.docker.com/r/cockroachdb/cockroach/tags).

### Configure the database engine

Now that the database engine is live, there is some configuration to do before our application can begin using it. Fortunately, it's not a lot. We must:

1. Create a blank database.
2. Register a new user account with the database engine.
3. Grant that new user access rights to the database.

We can do that with the help of CockroachDB built-in SQL shell. To start the SQL shell in the _same_ container where the database engine is running, type:

```console
$ docker exec -it roach ./cockroach sql --insecure
```

1. In the SQL shell, create the database that our example application is going to use:

   ```sql
   CREATE DATABASE mydb;
   ```

2. Register a new SQL user account with the database engine. We pick the username `totoro`.

   ```sql
   CREATE USER totoro;
   ```

3. Give the new user the necessary permissions:
   
   ```sql
   GRANT ALL ON DATABASE mydb TO totoro;
   ```

4. Type `quit` to exit the shell. 

An example of interaction with the SQL shell is presented below.

{% raw %}
```console
$ sudo docker exec -it roach ./cockroach sql --insecure
#
# Welcome to the CockroachDB SQL shell.
# All statements must be terminated by a semicolon.
# To exit, type: \q.
#
# Server version: CockroachDB CCL v20.1.15 (x86_64-unknown-linux-gnu, built 2021/04/26 16:11:58, go1.13.9) (same version as client)
# Cluster ID: 7f43a490-ccd6-4c2a-9534-21f393ca80ce
#
# Enter \? for a brief introduction.
#
root@:26257/defaultdb> CREATE DATABASE mydb;
CREATE DATABASE

Time: 22.985478ms

root@:26257/defaultdb> CREATE USER totoro;
CREATE ROLE

Time: 13.921659ms

root@:26257/defaultdb> GRANT ALL ON DATABASE mydb TO totoro;
GRANT

Time: 14.217559ms

root@:26257/defaultdb> quit
oliver@hki:~$
```
{% endraw %}

### Meet the example application

Now that we have started and configured the database engine, we can switch our attention to the application. 

The example application for this module is an extended version of `docker-gs-ping` application we've used in the previous modules. You have two options:

* You can update your local copy of `docker-gs-ping` to match the new extended version presented in this chapter; or
* You can clone the [olliefr/docker-gs-ping-roach](https://github.com/olliefr/docker-gs-ping-roach) repo. This latter approach is recommended.

To checkout the example application, run:

```console
$ git clone https://github.com/olliefr/docker-gs-ping-roach.git
# ... output omitted ...
```

The application's `main.go` now includes database initialization code, as well as the code to implement a new business requirement:

* An HTTP `POST` request to `/send` containing a `{ "value" : string }` JSON must save the value to the database.

We also have an update for another business requirement. The requirement _was_:

* The application responds with a text message containing a heart symbol ("`<3`") on requests to `/`.

And _now_ it's going to be:

* The application responds with the string containing the count of messages stored in the database, enclosed in the parentheses. 

   Example output: `Hello, Docker! (7)`

The full source code listing of `main.go` follows.

{% raw %}
```go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/cenkalti/backoff/v4"
	"github.com/cockroachdb/cockroach-go/v2/crdb"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	db, err := initStore()
	if err != nil {
		log.Fatalf("failed to initialise the store: %s", err)
	}
	defer db.Close()

	e.GET("/", func(c echo.Context) error {
		return rootHandler(db, c)
	})

	e.GET("/ping", func(c echo.Context) error {
		return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
	})

	e.POST("/send", func(c echo.Context) error {
		return sendHandler(db, c)
	})

	httpPort := os.Getenv("HTTP_PORT")
	if httpPort == "" {
		httpPort = "8080"
	}

	e.Logger.Fatal(e.Start(":" + httpPort))
}

type Message struct {
	Value string `json:"value"`
}

func initStore() (*sql.DB, error) {

	pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
		os.Getenv("PGHOST"),
		os.Getenv("PGPORT"),
		os.Getenv("PGDATABASE"),
		os.Getenv("PGUSER"),
		os.Getenv("PGPASSWORD"),
	)

	var (
		db  *sql.DB
		err error
	)
	openDB := func() error {
		db, err = sql.Open("postgres", pgConnString)
		return err
	}

	err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
	if err != nil {
		return nil, err
	}

	if _, err := db.Exec(
		"CREATE TABLE IF NOT EXISTS message (value STRING PRIMARY KEY)"); err != nil {
		return nil, err
	}

	return db, nil
}

func rootHandler(db *sql.DB, c echo.Context) error {
	r, err := countRecords(db)
	if err != nil {
		return c.HTML(http.StatusInternalServerError, err.Error())
	}
	return c.HTML(http.StatusOK, fmt.Sprintf("Hello, Docker! (%d)\n", r))
}

func sendHandler(db *sql.DB, c echo.Context) error {

	m := &Message{}

	if err := c.Bind(m); err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	err := crdb.ExecuteTx(context.Background(), db, nil,
		func(tx *sql.Tx) error {
			_, err := tx.Exec(
				"INSERT INTO message (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = excluded.value",
				m.Value,
			)
			if err != nil {
				return c.JSON(http.StatusInternalServerError, err)
			}
			return nil
		})

	if err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	return c.JSON(http.StatusOK, m)
}

func countRecords(db *sql.DB) (int, error) {

	rows, err := db.Query("SELECT COUNT(*) FROM message")
	if err != nil {
		return 0, err
	}
	defer rows.Close()

	count := 0
	for rows.Next() {
		if err := rows.Scan(&count); err != nil {
			return 0, err
		}
		rows.Close()
	}

	return count, nil
}
```
{% endraw %}

The repository also includes the `Dockerfile`, which is almost exactly the same as the multi-stage `Dockerfile` introduced in the previous modules. It uses the official Docker Go image to build the application and then builds the final image by placing the compiled binary into the much slimmer, "distroless" image.

Regardless of whether we had updated the old example application, or checked out the new one, this new Docker image has to be built to reflect the changes to the application source code.

### Build the application

We can build the image with the familiar `build` command:

```console
$ docker build --tag docker-gs-ping-roach .
```

### Run the application

Now, let’s run our container. This time we’ll need to set some environment variables so that our application would know how to access the database. For now, we’ll do this right in the `docker run` command. Later we will see a more convenient method with Docker Compose.

> **Note**
>
> Since we are running our CockroachDB cluster in "insecure" mode, the value for the password can be anything. 
> 
> **Don't run in insecure mode in production, though!**

```console
$ docker run -it --rm -d \
  --network mynet \
  --name rest-server \
  -p 80:8080 \
  -e PGUSER=totoro \
  -e PGPASSWORD=myfriend \
  -e PGHOST=db \
  -e PGPORT=26257 \
  -e PGDATABASE=mydb \
  docker-gs-ping-roach
```

There are a few points to note about this command.

* We map container port `8080` to host port `80` this time. Thus, for `GET` requests we can get away with _literally_ `curl localhost`:

  ```console
  $ curl localhost
  Hello, Docker! (0)
  ```

  Or, if you prefer, a proper URL would work just as well:

  ```console
  $ curl http://localhost/
  Hello, Docker! (0)
  ```

* The total number of stored messages is `0` for now. This is fine, because we had not posted anything to our application yet.
* We refer to the database container by its hostname, which is `db`. This is what we had the `--name db` for when we started the database container.

* The actual password does not matter, but it must be set to something to avoid confusing the example application.
* The container we've just run is named `rest-server`. These names are useful for managing the container lifecycle:

  ```console
  # Don't do this just yet, it's only an example:
  $ docker container rm --force rest-server
  ```

### Test the application

In the previous section, we have already tested querying our application with `GET` and it returned zero for the stored message counter. Now, let's post some messages to it:

```console
$ curl --request POST \
  --url http://localhost/send \
  --header 'content-type: application/json' \
  --data '{"value": "Hello, Docker!"}'
```

The application responds with the contents of the message, which means it had been saved in the database:

```json
{"value":"Hello, Docker!"}
```

Let's send another message:

```console
$ curl --request POST \
  --url http://localhost/send \
  --header 'content-type: application/json' \
  --data '{"value": "Hello, Oliver!"}'
```

And again, we get the value of the message back:

```json
{"value":"Hello, Oliver!"}
```

Let's see what the message counter says:

```console
$ curl localhost
Hello, Docker! (2)
```

Hey, that's exactly right! We sent two messages and the database kept them. Or has it? Let's stop and remove all our containers, but not the volumes, and try again.

First, let's stop the containers:

```console
$ docker container stop rest-server roach
rest-server
roach
```

Then, let's remove them:

```console
$ docker container rm rest-server roach
rest-server
roach
```

Verify that they are gone:

```console
$ docker container list --all
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

And start them again, database first:

```console
$ docker run -d \
  --name roach \
  --hostname db \
  --network mynet \
  -p 26257:26257 \
  -p 8080:8080 \
  -v roach:/cockroach/cockroach-data \
  cockroachdb/cockroach:latest-v20.1 start-single-node \
  --insecure
```

And the service next:

```console
$ docker run -it --rm -d \
  --network mynet \
  --name rest-server \
  -p 80:8080 \
  -e PGUSER=totoro \
  -e PGPASSWORD=myfriend \
  -e PGHOST=db \
  -e PGPORT=26257 \
  -e PGDATABASE=mydb \
  docker-gs-ping-roach
```

Lastly, let's query our service:

```console
$ curl localhost
Hello, Docker! (2)
```

Great! The count of records from the database is correct although we had not only _stopped_ the containers, but also removed them before starting new instances. The difference is in the managed volume for CockroachDB, which we had reused. The new CockroachDB container has read the database files from the disk, just as it normally would if it were running outside the container.

Such is the power of managed volumes. Use it wisely.

### Wind down everything

Remember, that we are running CockroachDB in "insecure" mode. Now that we had built and tested our application, it's time to wind everything down before moving on. You can list the containers that you are running with the `list` command:

```console
$ docker container list
```

Now that you know the container IDs, you can use `docker container stop` and `docker container rm`, as demonstrated in the previous modules.

Please make sure that you stop the CockroachDB and `docker-gs-ping-roach` containers before moving on.

## Better productivity with Docker Compose

At this point, you might be wondering if there is a way to avoid having to deal with long lists of arguments to the `docker` command. The toy example we used in this series requires five environment variables to define the connection to the database. A real application might need many, _many_ more. Then there is also a question of dependencies &ndash; ideally, we would like to make sure that the database is started _before_ our application is run. And spinning up the database instance may require another Docker command with many options. But there is a better way to orchestrate these deployments for local development purposes.

In this section, we’ll create a Docker Compose file to start our `docker-gs-ping-roach` application and CockroachDB database engine with a single command.

### Configure Docker Compose

In our application's directory, create a new text file named `docker-compose.yml` with the following content.

{% raw %}
```yaml
version: '3.8'

services:
  docker-gs-ping-roach:
    depends_on: 
      - roach
    build:
      context: .
    container_name: rest-server
    hostname: rest-server
    networks:
      - mynet
    ports:
      - 80:8080
    environment:
      - PGUSER=${PGUSER:-totoro}
      - PGPASSWORD=${PGPASSWORD:?database password not set}
      - PGHOST=${PGHOST:-db}
      - PGPORT=${PGPORT:-26257}
      - PGDATABASE=${PGDATABASE:-mydb}
    deploy:
      restart_policy:
        condition: on-failure
  roach:
    image: cockroachdb/cockroach:latest-v20.1
    container_name: roach
    hostname: db
    networks:
      - mynet
    ports:
      - 26257:26257
      - 8080:8080
    volumes:
      - roach:/cockroach/cockroach-data
    command: start-single-node --insecure

volumes:
  roach:

networks:
  mynet:
    driver: bridge
```
{% endraw %}

This Docker Compose configuration is super convenient as we do not have to type all the parameters to pass to the `docker run` command. We can declaratively do that in the Docker Compose file. The [Docker Compose documentation pages](../../compose/index.md) are quite extensive and include a full reference for the Docker Compose file format.

### The `.env` file

Docker Compose will automatically read environment variables from a `.env` file if it is available. Since our Compose file requires `PGPASSWORD` to be set, we add the following content to the `.env` file:

```bash
PGPASSWORD=whatever
```

The exact value does not really matter for our example, because we run CockroachDB in `insecure` mode, but we have to set the variable to _some_ value to avoid getting an error.

### Merging Compose files

The file name `docker-compose.yml` is the default file name which `docker-compose` command recognises if no `-f` flag is provided. This means you can have multiple Docker Compose files if your environment has such requirements. Furthermore, Docker Compose files are... composable (pun intended), so multiple files can be specified on the command line to merge parts of the configuration together. The following list is just a few examples of scenarios where such a feature would be very useful:

* Using a bind mount for the source code for local development but not when running the CI tests;
* Switching between using a pre-built image for the frontend for some API application vs creating a bind mount for source code;
* Adding additional services for integration testing;
* And many more...

We are not going to cover any of these advanced use cases here.

### Variable substitution in Docker Compose

One of the really cool features of Docker Compose is [variable substitution](../../compose/compose-file/compose-file-v3.md#variable-substitution). You can see some examples in our Compose file, `environment` section. By means of an example:

* `PGUSER=${PGUSER:-totoro}` means that inside the container, the environment variable `PGUSER` shall be set to the same value as it has on the host machine where Docker Compose is run. If there is no environment variable with this name on the host machine, the variable inside the container gets the default value of `totoro`.
* `PGPASSWORD=${PGPASSWORD:?database password not set}` means that if the environment variable `PGPASSWORD` is not set on the host, Docker Compose will display an error. This is OK, because we don't want to hard-code default values for the password. We set the password value in the `.env` file, which is local to our machine. It is always a good idea to add `.env` to `.gitignore` to prevent the secrets being checked into the version control.

Other ways of dealing with undefined or empty values exist, as documented in the [variable substitution](../../compose/compose-file/compose-file-v3.md#variable-substitution) section of the Docker documentation.

### Validating Docker Compose configuration

Before you apply changes made to a Compose configuration file, there is an opportunity to validate the content of the configuration file with the following command:

```console
$ docker-compose config
```

When this command is run, Docker Compose would read the file `docker-compose.yml`, parse it into a data structure in memory, validate where possible, and print back the _reconstruction_ of that configuration file from its internal representation. If this is not possible due to errors, it would print an error message instead.

### Build and run the application using Docker Compose

Let’s start our application and confirm that it is running properly.

```console
$ docker-compose up --build
```

We pass the `--build` flag so Docker will compile our image and then starts it.

> **Note**
>
> Docker Compose is a useful tool, but it has its own quirks. For example, no rebuild is triggered on the update to the source code unless the `--build` flag is provided. It is a very common pitfall to edit one's source code, and forget to use the `--build` flag when running `docker-compose up`.

Since our set-up is now run by Docker Compose, it has assigned it a "project name", so we got a new volume for our CockroachDB instance. This means that our application would fail to connect to the database, because the database does not exist in this new volume. The terminal would display an authentication error for the database:

```
# ... omitted output ...
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
roach                   | *
roach                   | * INFO: Replication was disabled for this cluster.
roach                   | * When/if adding nodes in the future, update zone configurations to increase the replication factor.
roach                   | *
roach                   | CockroachDB node starting at 2021-05-10 00:54:26.398177 +0000 UTC (took 3.0s)
roach                   | build:               CCL v20.1.15 @ 2021/04/26 16:11:58 (go1.13.9)
roach                   | webui:               http://db:8080
roach                   | sql:                 postgresql://root@db:26257?sslmode=disable
roach                   | RPC client flags:    /cockroach/cockroach <client cmd> --host=db:26257 --insecure
roach                   | logs:                /cockroach/cockroach-data/logs
roach                   | temp dir:            /cockroach/cockroach-data/cockroach-temp349434348
roach                   | external I/O path:   /cockroach/cockroach-data/extern
roach                   | store[0]:            path=/cockroach/cockroach-data
roach                   | storage engine:      rocksdb
roach                   | status:              initialized new cluster
roach                   | clusterID:           b7b1cb93-558f-4058-b77e-8a4ddb329a88
roach                   | nodeID:              1
rest-server exited with code 0
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server exited with code 1
# ... omitted output ...
```

Because of the way we set up our deployment using `restart_policy`, the failing container is being restarted every 20 seconds. So, in order to fix the problem, we need to log into the database engine and create the user, we've done it before in the []

This is not a big deal. All we have to do is to connect to CockroachDB instance and run the three SQL commands to create the database and the user, as described above in the _Configure the database engine_ section above.

So we login into the database engine from another terminal:

```console
$ docker exec -it roach ./cockroach sql --insecure
```

And execute the same commands as before to create the database `mydb`, the user `totoro`, and to grant that user necessary permissions. Once we do that (and the example application container is automatically restarted), the `rest-service` stops failing and restarting and the console goes quiet.

It would have been possible to connect the volume that we had previously used, but for the purposes of our example it's more trouble than it's worth and it also provided an opportunity to show how to introduce resilience into our deployment via the `restart_policy` Compose file feature.

### Testing the application

Now let’s test our API endpoint. In the new terminal, run the following command:

```console
$ curl http://localhost/
```

You should receive the following response:

```json
Hello, Docker! (0)
```

### Shutting down

To _stop_ the containers started by Docker Compose, press ctrl+c in the terminal where we run `docker-compose up`. To _remove_ those containers after they had been stopped, run `docker-compose down`.

### Detached mode

You can run containers started by the `docker-compose` command in detached mode, just as you would with the `docker` command, by using the `-d` flag. 

To start the stack, defined by the Compose file in detached mode, run:

```console
$ docker-compose up --build -d
```

Then, you can use `docker-compose stop` to stop the containers and `docker-compose down` to remove them.

## Further exploration

We would suggest running `docker-compose` to see what other commands are available.

## Wrap up

There are some tangential, yet interesting points that were purposefully not covered in this chapter. For the more adventurous reader, this section offers some pointers for further study.

### Persistent storage

A _managed volume_ isn't the only way to provide your container with persistent storage. It is highly recommended to get acquainted with available storage options and their use cases, covered in the following part of Docker documentation: [Manage data in Docker](../../storage/index.md).

### CockroachDB clusters

We run a single instance of CockroachDB, which was enough for our demonstration. But it is possible to run a CockroachDB _cluster_, which is made of multiple instances of CockroachDB, each instance running in its own container. Since CockroachDB engine is distributed by design, it would have taken us surprisingly little change to our procedure to run a cluster with multiple nodes.

Such distributed set-up offers interesting possibilities, such as applying _Chaos Engineering_ techniques to simulate parts of the cluster failing and evaluating our application's ability to cope with such failures.

If you are interested in experimenting with CockroachDB clusters, check out:

* [Start a CockroachDB Cluster in Docker](https://www.cockroachlabs.com/docs/v20.2/start-a-local-cluster-in-docker-mac.html) article; and
* Documentation for Docker Compose keywords [`deploy`](../../compose/compose-file/compose-file-v3.md#deploy) and [`replicas`](../../compose/compose-file/compose-file-v3.md#replicas).

### Other databases

Since we did not run a cluster of CockroachDB instances, you might be wondering whether we could have used a non-distributed database engine. The answer is 'yes', and if we were to pick a more traditional SQL database, such as [PostgreSQL](https://www.postgresql.org/), the process described in this chapter would have been very similar.

## Next steps

In this module, we set up a containerised development environment with our application and the database engine running in different containers. We also wrote a Docker Compose file which links the two containers together and provides for easy starting up and tearing down of the development environment.

In the next module, we’ll take a look at one possible approach to running functional tests in Docker. See:

[Run your tests](run-tests.md){: .button .outline-btn}

## Feedback

Help us improve this topic by providing your feedback. Let us know what you think by creating an issue in the [Docker Docs](https://github.com/docker/docker.github.io/issues/new?title=[Golang%20docs%20feedback]){:target="_blank" rel="noopener" class="_"} GitHub repository. Alternatively, [create a PR](https://github.com/docker/docker.github.io/pulls){:target="_blank" rel="noopener" class="_"} to suggest updates.
