DEV Community

Boluwatife Fakorede
Boluwatife Fakorede

Posted on

How I setup golang on docker and deploy it to Heroku

#go

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" ]

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Image below:

Alt Text

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

We are not done yet.

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

Alt Text

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" ]

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (3)

Collapse
 
kissgyorgy profile image
Kiss, György

Go build will download way more than what's necessary, so the final binary will be way bigger. go build ./... should be used.

Collapse
 
wati_fe profile image
Boluwatife Fakorede

Hello György,

I believe what you raised was addressed with the './cmd' as this specifies the directory to build.

Collapse
 
kissgyorgy profile image
Kiss, György

I mean I think you don't need the "RUN go mod download" line at all.