Search code examples
postgresqldockergodocker-composeredis

How to run Golang backend with PostgreSQL DB and Redis using Docker?


I have a project with Golang backend, PostgreSQL DB and Redis.

I need to init PostgreSQL DB with this SQL:

ALTER USER postgres WITH password '123';

CREATE TABLE users
  (
     username VARCHAR (200) PRIMARY KEY,
     pass     VARCHAR (50)
  );

My Dockerfile is:

# syntax=docker/dockerfile:1
FROM postgres:latest AS go2
ENV POSTGRES_USER postgres
ENV POSTGRES_PASSWORD ''
ENV POSTGRES_DB postgres
EXPOSE 5432
ADD table.sql /docker-entrypoint-initdb.d/
FROM redis:latest AS go3
FROM golang:latest AS go
WORKDIR /app
COPY . .
RUN go build -o bin
ENTRYPOINT [ "/app/bin" ] 
EXPOSE 8000

I build it with:

docker build . -t mycontainer

And run it with:

docker run -t mycontainer --network=bridge

But I get the following error:

[error] failed to initialize database, got error failed to connect to `host=172.17.0.2 user=postgres database=postgres`: dial error (dial tcp 172.17.0.2:5432: connect: connection refused)

My problems are:

  1. I can't connect to PostgreSQL DB
  2. I can't connect to Golang backend

I tried to run the container with this command:

docker run -t mycontainer --network=host

But the problems remain.


Update:

I created a docker-compose.yaml file:

services:
  db:
    image: 'postgres:15.7'
    restart: always
    network_mode: bridge
    volumes:
      - './db/table.sql:/docker-entrypoint-initdb.d/init.sql'
    ports:
      - '5433:5432'
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 123
  redis:
    image: 'redis:7.0.12'
    network_mode: bridge
    ports:
      - '6380:6379'
  backend:
    build: ./backend
    network_mode: bridge
    ports:
      - '8000:8089'
    depends_on:
      - db

My PostgreSQL DB and Redis work good and I can connect to them.

But when I run my backend container it throws this error:

[error] failed to initialize database, got error failed to connect to `host=127.0.0.1 user=postgres database=postgres`: dial error (dial tcp 127.0.0.1:5433: connect: connection refused)

Solution

  • In your Dockerfile you combine PostgreSQL, Redis and your Go app.

    But according to Docker's docs it's suggested to split them up:

    It's best practice to separate areas of concern by using one service per container.

    Thus, let's simplify your Dockerfile down to the Go app alone:

    FROM golang
    WORKDIR /app
    COPY ./app .
    RUN go build -o bin
    ENTRYPOINT [ "/app/bin" ]
    

    Fun fact, the EXPOSE instruction used by you does not expose anything:

    The EXPOSE doesn't publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container.

    Since you are the person who runs the container, it's unnecessary. I'd use it only if I publish the Dockerfile to a registry so that others could download and use it. That's why I removed it.

    Optional: The following slightly improved version should allow faster rebuilds:

    FROM golang
    WORKDIR /app
    COPY go.mod go.sum ./ # ddd
    RUN go mod download
    COPY *.go ./
    RUN go build -o bin
    CMD [ "/app/bin" ]
    

    Now let's talk about the error you experience.

    Each service is in a separate container, but you are trying to connect from backend to db like if they are running in a single container. Think about them as running on separate machines. This way it should be obvious why you can't access db on localhost of backend.

    You defined network_mode: bridge for all containers. Thus they all will be attached to the default bridge network. It means they can access each other by their IPs. But it's tricky to get IPs. Thankfully, there is an easier way. Just define a custom bridge network:

    networks:
      my_network:
    

    Here my_network is a user defined network and it defaults to driver: bridge. As opposed to the default bridge network, user defined bridge networks allow automatic service discovery:

    On user-defined networks containers can not only communicate by IPs, but can also resolve a container name to an IP. This is called automatic service discovery.

    It means, in the code of your backend service you will be able to use db:5432 to access your PostgreSQL DB. The db here will be automatically replaced by IP of the db service.

    But to access your services from the host use ports attribute to map host's port to container's port. Your backend service maps 8000:8089. It means, the backend service is accessible on the host as localhost:8000, but from other services as backend:8089.

    This is a complete minimal example of compose file you need:

    networks:
      my_network:
    services:
      db:
        image: postgres
        volumes:
          - ./db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
        networks:
          - my_network
        ports:
          - 5433:5432
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: 123
        healthcheck:
          test: ["CMD-SHELL", "pg_isready"]
          interval: 1s
          retries: 100
      redis:
        image: redis
        networks:
          - my_network
        ports:
          - 6380:6379
      backend:
        depends_on:
          db:
            condition: service_healthy
          redis:
            condition: service_started
        build: ./backend
        networks:
          - my_network
        ports:
          - 8000:8089
    

    Here, the ./postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d maps data from ./postgres/docker-entrypoint-initdb.d on the host to docker-entrypoint-initdb.d within the db container. Any SQL there will init PostgreSQL DB.

    The backend.depends_on together with db.healthcheck allow to make sure the backend starts when db is healthy (i.e. PostgreSQL DB is ready to accept connections).

    Finally, a complete working example is here.