How I setup golang on docker and deploy it to Heroku
Tags: go
Published: 2020-01-06 00:52:25

Hello dear reader,

This post came as a result of my recent pain point which was figuring out how to set up golang on docker and deploying the backend service to Heroku.

Sometimes last year, I came across golang and I have been playing with it ever since. I came from a Node.Js background and found the features of golang intriguing. 

I have so far overcome certain obstacles such as structuring the project and now I am a big fan of domain-driven development. 

Always in love with docker, it was my initial go-to for setting up my project since it gives me `build in one place, run on any platform` solution. 

Firstly, how I set-up for development.

```

# Start from golang base image
FROM golang:alpine as builder

# Add Maintainer info
LABEL maintainer="Fakorede Boluwatife"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git

# Set the current working directory inside the container 
WORKDIR /app

# Copy go mod and sum files 
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed 
RUN go mod download 

# Copy the source from the current directory to the working Directory inside the container 
COPY . .

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd

# Start a new stage from scratch
FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the Pre-built binary file from the previous stage.
COPY --from=builder /app/main .
COPY --from=builder /app/.env .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/pkg ./pkg
COPY --from=builder /app/migrations ./migrations
# COPY --from=builder /app/tls ./tls
     

# Expose port to the outside world
EXPOSE 4000

#Command to run the executable
CMD [ "./main" ]

```

The above code is my default `Dockerfile.dev` and it is a multi-stage build docker file and it is well commented to understand each step. I name it `Dockerfile.dev` simply because I need to use the `Dockerfile` itself for production with just a tiny change.


For the second stage of the build, you can easily remove (or comment out) the file you don't have in your initial working directory to prevent a build fail.

Now, I am also working with PostgreSQL and I needed a way to use it alongside my golang application in a docker container.

Docker-compose came to my rescue.

I would also love to set up automatic Migrations once I run my docker-Compose file.

My docker-compose file looked like this.

```
version: '3'
services:
  postgres_database:
    environment:
      - POSTGRES_USER=${DATABASE_USER}  
      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
      - POSTGRES_DB=${DATABASE_NAME}
      - DATABASE_HOST=${DATABASE_HOST}
    build: ./migrations/postgres
    ports:
      - 5432:5432
  app:
    container_name: test-app
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - api:/usr/src/app/
    ports:
      - "4000:4000"
    depends_on:
      - postgres_database

```

If you notice, my build for `postgres_database` service is using a `Dockerfile` in a migration folder.

Image below:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/bc02fa00xljsoz9drbzk.png)

Okay, let me explain.

The folder also contains a tables folder that holds some SQL commands for each table. 

The tables command are then included in a `deploy_schemas.sql` file. 

Image below:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/lkfc1vvfwop1m2u1rixz.png)

We are not done yet.

The `Dockerfile` (used by our docker-compose) actually holds certain command as shown below:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/zr1sunr97c2or8kfhvtn.png)

It simply gets the latest Postgres from docker hub, adds the table SQLs to `docker-entrypoint-initdb.d/tables` then adds the `deploy_schemas.sql` file to `docker-entrypoint-initdb.d`.

P.S: 

```The `docker-entrypoint-initdb.d` will only run the first time when it is not set, hence you have to run a docker-compose down command then `docker-compose up --b` to create the latest table migrations```



Also, don't forget to set the proper environment variables alongside `DATABASE_USER`,  `DATABASE_PASSWORD`, `DATABASE_NAME` and `DATABASE_HOST`.

After all that, simply run the `docker-compose up --b` and you are set for development.

Let's talk about the production deployment on Heroku:


I couldn't find a suitable way to copy the production environment variables for production usage and it became quite difficult for me to figure out how to do this.

On Heroku, I simply set the environment variables there, before deploying and I simply copied and paste my `Dockerfile.dev` into a new `Dockerfile` but removing the .env file to prevent the test or development environment variables from running in production.

This way, my `Dockerfile` looks like:

```

# Start from golang base image
FROM golang:alpine as builder

# Add Maintainer info
LABEL maintainer="Fakorede Boluwatife"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git

# Set the current working directory inside the container 
WORKDIR /app

# Copy go mod and sum files 
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed 
RUN go mod download 

# Copy the source from the current directory to the working Directory inside the container 
COPY . .

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd

# Start a new stage from scratch
FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the Pre-built binary file from the previous stage.
COPY --from=builder /app/main .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/pkg ./pkg
COPY --from=builder /app/migrations ./migrations
# COPY --from=builder /app/tls ./tls
     

# Expose port to the outside world
EXPOSE 4000

#Command to run the executable
CMD [ "./main" ]

```

Exactly my Dockerfile.dev without the .env been copied on the second-build stage.


`MAKE SURE YOU USE CMD instead of ENTRYPOINT` to run the executable as this is mandatory for your application to run on Heroku (I just saved you hours of google search).

Also, make sure your port in main.go is using a `":" + $PORT`. This is set automatically by Heroku and you need not worry about it. In your `development environment` though, don't forget to set the $PORT variable in your .env file.

You need to set up a project on Heroku (You can also use Heroku create to do this).

I didn't use docker to migrate my database on Heroku because, what if I bring down my application on Heroku, I am going to lose all the data in my database and that is way too risky. 

Hence, my database is separate from my Dockerfile in production.

You must not forget to set up all the environment variables on Heroku including the database credentials (can be found on the add-ons section of your database then click on Heroku Postgres (if you are using it) and add then settings). 

The beauty of Heroku with docker is the ease of deployment.

With 4 simple commands, your application is up and running in production.

At the directory of your application,

```

1. `Heroku login` (you need to login into the Heroku-cli) 

PS: If you don't have the heroku-cli installed, you need to install following:

`https://devcenter.heroku.com/articles/heroku-cli`

2. `heroku container:login` This allows you to login into the heroku container.

3. `heroku container:push web -a <name-of-your-heroku-project>`

On the success of step 3,

4. `heroku container:release web -a <name-of-your-heroku-project>`.

That is all!

Your project is running on Heroku perfectly.

Incase you experience any bug, run:

`heroku logs --tail -a <name-of-your-heroku-project>`

 This helps you to identify the issue and gives you the first step to debugging.

```

I know there are many ways to improve this process and I will write another article on the improvements. Your suggestions are welcome.

Thank you for reading.