Develop in Docker: Node.js, Express, & PostgreSQL on Heroku

This post builds on the excellent tutorial from Tania Rascia: Create and Deploy a Node.js, Express, & PostgreSQL REST API.

I want to build a little web app that can store some data! …without installing anything new on my computer. Here’s how to configure VSCode Remote Containers to do that.

Installed on my machine:

  • Docker
  • VSCode
  • VSCode Remote Containers Extension
  • git (but I don’t talk about git operations here)

The database will be handled for you

Skip Tania’s section on setting up PostgreSQL

Skip to writing the code

Follow Tania’s section on creating an Express API.

Set up containers in VSCode

Open the directory in VSCode.

Enter the command (Ctrl-shift-P for the command prompt) “Remote Containers: Add Development Container Configuration Configuration Files.” I type “add dev” until this comes up.

Choose “Node.js 12 & PostgreSQL” for the starting point. (You may have to choose “Show all definitions…” before it gives this to you.) I search for “node postgres” and this comes up.

Now you have a .devcontainer directory with three relevant files: docker-compose.yml, Dockerfile, and devcontainer.json.

VSCode reads devcontainer.json to figure out what to do. That references docker-compose.yml, which references the Dockerfile.

Set up the Postgres user

In docker-compose.yml, two services are defined. The second one is db, and that uses the standard postgres Docker image. It defines a username, password, and database to create.

Change those environment variables to reference the ones in .env as Tanya set it up:

  db:
    image: postgres
    restart: unless-stopped
    ports: 
      - ${DB_PORT}:${DB_PORT}
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_DB: ${DB_DATABASE}

When docker-compose reads this file, it reads .env first, and makes those variables available here.

Make it easy to connect to that user

The first service in docker-compose.yml is web, and that’s where your app lives. It’s also where your VSCode terminal will open (as defined in devcontainer.json when it says "service": "web").

I want to connect from the terminal using psql, and I can make the connection parameters available in an environment variable, building them out of the same definitions. To the web service definition, I add an environment section and define CONNECTION_STRING:

services:
  web:
    # ... stuff that is already there ...    
    environment: 
      CONNECTION_STRING: "postgresql://${DB_USER}:${DB_PASSWORD}@db:${DB_PORT}/${DB_DATABASE}"

This will let me run psql $CONNECTION_STRING in the terminal to get a database prompt.

But wait! That doesn’t work yet!

Install psql in the container

The web container doesn’t have psql in it. We can fix that.

in docker_compose.yml, the web service is defined with (among other things)

    build: 
      context: .
      dockerfile: Dockerfile

This means it’s using the Dockerfile that’s right here. Open that and add to the bottom:


# psql
RUN apt-get update && apt-get install -y postgresql-client

This fetches package definitions and then installs the package with psql in it (but not the whole PostgreSQL database).

Initialize the schema

Whenever the container comes up, it’s time to run that init.sql script.

Now, the script as Tania wrote it isn’t idempotent (it’ll give an error if the table exists already), so I added to the top of init.sql:

DROP TABLE IF EXISTS books;

Do that for all the tables you create. That way you’ll start with a clean database every time.

Every time you rebuild these containers, the db container creates itself an empty Docker volume to start with. You can have VSCode populate that after startup. Add to devcontainer.json:

	 "postCreateCommand": "psql -f init.sql $CONNECTION_STRING"

Note: if you want your development data to persist (not recommended), then instead run init.sql manually, and in docker-compose, add a named volume and mount it to /var/lib/postgresql/data in the db container.

In case you want to deploy

You’ll want the Heroku client in your container, too. Add this to the Dockerfile:


# heroku
RUN curl https://cli-assets.heroku.com/install-ubuntu.sh | sh

For authentication with Heroku, consider following Avdi’s instructions to store credentials on your own computer and mount them into the container.

Start it up!

In VSCode, run the command (Ctrl-shift-P) “Remote Containers: Rebuild and Reopen in Container”.

When the little “Starting with Dev Container” notice pops up in the bottom right, click it so you can watch it work. It runs docker-compose up, then logs in to the web container and installs VSCode server stuff.

If it fails, let it reopen locally, and it should show you the log. You can modify the container definitions and try again.

Once it completes, open a terminal. You can npm install and npm start there.

The next time you start VSCode in this directory, it’ll offer to Restart in Container. Go with that.

Clean up

When you exit VSCode, it stops the containers, but it does not remove them. This makes startup a lot faster next time.

If you’re done with the project or want to clean up your docker containers, you may want to remove the containers and the database’s volume.

Run docker ps -a to see all the containers, including stopped one. You’ll see one named <your project>_devcontainer_web_1 and a similar container for db.

Go to your project root and

docker-compose -f .devcontainer/docker-compose.yml --project-name <your project>_devcontainer down -v

This removes both relevant containers and the volume. (If you want to keep your data around, omit the -v.)

The end

Okay! Now you have a database, a backend, and a carefully defined and contained development environment.

Don’t miss the part of Tania’s post where she sets the app up for production.

Corrections? DM on Twitter or email jessitron@jessitron.com.

If this was useful, you can support Tania and me on Patreon 🙂

Discover more from Jessitron

Subscribe now to keep reading and get access to the full archive.

Continue reading