Assume that you have two docker services/containers. One must be ready to accept connections from the other one. As you have just noticed, I didn't say "one must be running" because there is different between being "ready to accept connections" and "running". The depends_on key of docker compose doesn't always solve the problem because it just checks if the container is running. This is often the case if a service depends on MySQL, RabbitMQ, Elasticsearch like services. They take time to accept connections. To solve such issue, we can use an additional script. This script would periodically ping these services until they are ready to accept connections before actually running the main service.


In this example we have a Go and MySQL service. Go depends on MySQL. MySQL is slow in accepting connections. We will use our script in Go to force it to wait for MySQL to accept connections first. Once MySQL says it is ready then we will bring up the Go service.


Structure


.
├── cmd
│   └── app
│   └── main.go
├── docker
│   └── dev
│   ├── docker-compose.yaml
│   └── go
│   ├── Dockerfile
│   └── init.sh
├── go.mod
└── go.sum

Files


main.go


package main

import (
"database/sql"
"log"

_ "github.com/go-sql-driver/mysql"
)

func main() {
log.Println("Staring the app ...")

db, err := sql.Open("mysql", "user:pass@tcp(app-db:3306)/db")
if err != nil {
log.Fatalln(err)
}
defer db.Close()

if err := db.Ping(); err != nil {
log.Fatalln(err)
}

log.Println("Connected to the db ...")

select {}
}

Dockerfile


#
# STAGE 1: prepare
#
FROM golang:1.13.1-alpine3.10 as prepare

WORKDIR /source

COPY go.mod .
COPY go.sum .

RUN go mod download

#
# STAGE 2: build
#
FROM prepare AS build

COPY . .

RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/app -v cmd/app/main.go

#
# STAGE 3: run
#
FROM alpine:3.10 as run

COPY --from=build /source/bin/app /app
COPY --from=build /source/docker/dev/go/init.sh /init.sh

RUN chmod +x /init.sh

ENTRYPOINT ["/init.sh"]

init.sh


You can use exactly the same script for other services or just add other services here.


#!/bin/sh

set -eu

echo "Checking DB connection ..."

i=0
until [ $i -ge 10 ]
do
nc -z app-db 3306 && break

i=$(( i + 1 ))

echo "$i: Waiting for DB 1 second ..."
sleep 1
done

if [ $i -eq 10 ]
then
echo "DB connection refused, terminating ..."
exit 1
fi

echo "DB is up ..."

/app

docker-compose.yaml


version: "3.4"

services:

app-go:
build:
context: "../.."
dockerfile: "docker/dev/go/Dockerfile"
ports:
- "8001:8000"

app-db:
image: "mysql:5.7.24"
ports:
- "3001:3306"
environment:
MYSQL_ROOT_PASSWORD: "root"
MYSQL_DATABASE: "db"

Test


Failed to connect in 10 seconds


I actually changed the 1 second to 0.001 (1ms) in the script in order to simulate termination here.


Recreating dev_app-go_1 ... done
Starting dev_app-db_1 ... done
Attaching to dev_app-db_1, dev_app-go_1
app-db_1 | 2020-04-17T19:31:26.156962Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated.
app-db_1 | 2020-04-17T19:31:26.158227Z 0 [Note] mysqld (mysqld 5.7.24) starting as process 1 ...
app-db_1 | 2020-04-17T19:31:26.161203Z 0 [Note] InnoDB: PUNCH HOLE support available
app-db_1 | 2020-04-17T19:31:26.161454Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
app-go_1 | Checking DB connection ...
app-go_1 | 1: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.161563Z 0 [Note] InnoDB: Uses event mutexes
app-go_1 | 2: Waiting for DB 1 second ...
app-go_1 | 3: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.161853Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used
app-go_1 | 4: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.162130Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11
app-db_1 | 2020-04-17T19:31:26.162368Z 0 [Note] InnoDB: Using Linux native AIO
app-go_1 | 5: Waiting for DB 1 second ...
app-go_1 | 6: Waiting for DB 1 second ...
app-go_1 | 7: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.162986Z 0 [Note] InnoDB: Number of pools: 1
app-go_1 | 8: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.163323Z 0 [Note] InnoDB: Using CPU crc32 instructions
app-go_1 | 9: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.164968Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M
app-db_1 | 2020-04-17T19:31:26.172335Z 0 [Note] InnoDB: Completed initialization of buffer pool
app-go_1 | 10: Waiting for DB 1 second ...
app-go_1 | DB connection refused, terminating ...
dev_app-go_1 exited with code 1

Successfully connected in 3 seconds


Recreating dev_app-go_1 ... done
Starting dev_app-db_1 ... done
Attaching to dev_app-db_1, dev_app-go_1
app-db_1 | 2020-04-17T19:31:26.156962Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated.
app-db_1 | 2020-04-17T19:31:26.158227Z 0 [Note] mysqld (mysqld 5.7.24) starting as process 1 ...
app-db_1 | 2020-04-17T19:31:26.161203Z 0 [Note] InnoDB: PUNCH HOLE support available
app-db_1 | 2020-04-17T19:31:26.161454Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
app-go_1 | Checking DB connection ...
app-go_1 | 1: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:31:26.161563Z 0 [Note] InnoDB: Uses event mutexes
app-go_1 | 2: Waiting for DB 1 second ...
app-go_1 | 3: Waiting for DB 1 second ...
app-db_1 | 2020-04-17T19:45:21.324028Z 0 [Note] mysqld: ready for connections.
app-db_1 | Version: '5.7.24' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
app-go_1 | DB is up ...
app-db_1 | 2020-04-17T19:45:21.663343Z 2 [Note] Got an error reading communication packets
app-go_1 | 2020/04/17 19:45:21 Staring the app ...
app-go_1 | 2020/04/17 19:45:21 Connected to the db ...