:nxdomain error in creating docker containers of phoenix

Hello. I’m new to Docker and am trying to create docker containers of phoenix. I use docker-compose.yml for creating 2 containers, application one and postgres one.


This is my docker-compose.yml

version: '3'

networks:
  backend:
    driver: bridge

services:
  db:
    build: ./dockerfiles/db
    volumes:
      - ./volumes/db/data:/var/lib/postgresql/0.1.0
    ports:
      - 5432:5432
    environment:
      - POSTGRES_DB=milk
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    restart: always
    tty: true
    networks:
      - backend

  app:
    build:
      dockerfile: ./dockerfiles/app/Dockerfile
      context: .
    command: "bin/milk start"
    environment:
      - DATABASE_URL=ecto://postgres:postgres@db/milk
      - SECRET_KEY_BASE=KL6jdYa8VzDIg1TjqQcGZsaGSnd43grQYWrl95dYjn/SJB7KABj7N4BWgaCxSF81
    ports:
      - 4000:4000
    tty: true
    networks:
      - backend

and these are Dockerfiles.

  • dockerfiles/app/Dockerfile
FROM elixir:1.9.4-alpine AS build

RUN apk add --no-cache build-base npm git python

WORKDIR /app

RUN mix local.hex --force && \
    mix local.rebar --force

ENV MIX_ENV=prod

COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile

COPY priv priv
COPY lib lib
COPY rel rel
RUN mix compile
RUN mix ecto.create
RUN mix ecto.migrate
RUN mix release

FROM alpine:3.9 AS app
RUN apk add --no-cache openssl ncurses-libs

WORKDIR /app

RUN chown nobody:nobody /app

USER nobody:nobody

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/milk ./

ENV HOME=/app

CMD ["bin/milk", "start"]
  • dockerfiles/db/Dockerfile
FROM postgres:13.0-alpine

CMD ["postgres"]

EXPOSE 5432

I have changed the hostname localhost to db, and changed the database config in config/ directory.


  • config/prod.exs
use Mix.Config

# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :milk, MilkWeb.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "localhost", port: 4000],
  #cache_static_manifest: "priv/static/cache_manifest.json",
  server: true,
  root: ".",
  version: Application.spec(:milk, :vsn)

# Do not print debug messages in production
config :logger, level: :info

config :milk, Milk.Repo,
  username: "postgres",
  password: "postgres",
  database: "milk",
  hostname: "db",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
#     config :milk, MilkWeb.Endpoint,
#       ...
#       url: [host: "example.com", port: 443],
#       https: [
#         :inet6,
#         port: 443,
#         cipher_suite: :strong,
#         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
#         certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
#       ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https:
#
#     config :milk, MilkWeb.Endpoint,
#       force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.

# Finally import the config/prod.secret.exs which loads secrets
# and configuration from environment variables.
# import_config "prod.secret.exs"
  • config/config.exs
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

config :milk, ecto_repos: [Milk.Repo]

# Configures the endpoint
config :milk, MilkWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "EBeu358sy8Bi+OLxOAee44nMrRCj/iwRKq5NUls46hBMwnqVhNOhl7qcw0d1REjZ",
  render_errors: [view: MilkWeb.ErrorView, accepts: ~w(html json)]

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

config :milk, Milk.UserManager.Guardian,
  issuer: "milk",
  secret_key: "ucwM9beYUEgWdkHoZ5kXflOMW8wZSEVwheR3PuUVROQrl3uymZL/qtRbHs+V3BN4",
  serializer: Milk.UserManager.GuardianSerializer,
  ttl: {24, :hour}

  config :milk, Milk.UserManager.Pipeline,
  module: Milk.UserManager.Guardian,
  error_handler: Milk.UserManager.ErrorHandler
  • config/release.exs
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
import Config

database_url =
  System.get_env("DATABASE_URL") ||
    raise """
    environment variable DATABASE_URL is missing.
    For example: ecto://USER:PASS@HOST/DATABASE
    """

config :milk, Milk.Repo,
  adapter: Ecto.Adapters.Postgres,
  #ssl: true,
  url: database_url,
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
  database: "milk"

secret_key_base =
  System.get_env("SECRET_KEY_BASE") ||
    raise """
    environment variable SECRET_KEY_BASE is missing.
    You can generate one by calling: mix phx.gen.secret
    """

config :milk, MilkWeb.Endpoint,
  http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
  secret_key_base: secret_key_base

# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
#     config :milk, MilkWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.

With those files, I got an error when I attempt running docker-compose build.

Step 13/23 : RUN mix ecto.create
 ---> Running in 760fc7c73107

14:40:27.994 [error] GenServer #PID<0.271.0> terminating
** (DBConnection.ConnectionError) tcp connect (db:5432): non-existing domain - :nxdomain
    (db_connection) lib/db_connection/connection.ex:87: DBConnection.Connection.connect/2
    (connection) lib/connection.ex:622: Connection.enter_connect/5
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: nil
** (Mix) The database for Milk.Repo couldn't be created: killed
ERROR: Service 'app' failed to build : The command '/bin/sh -c mix ecto.create' returned a non-zero code: 1

Concerning docker-compose, I think the host (db:5432) is correct. But my terminal shows an error.
I don’t know where to fix at all. Help me!

Try adding depends_on to app service in your docker-compose.yml for the db service. The problem might be that you’re trying to access the database before it is even started and when you use depends on you might not need a bridge network to talk to db service.

Some of the examples in this thread might help you understand them in detail:

1 Like

Thank you for the prompt answer!

Well, I have added depends_on in app section. But It does not work yet.

  app:
    build:
      dockerfile: ./dockerfiles/app/Dockerfile
      context: .
    command: "bin/milk start"
    environment:
      - DATABASE_URL=ecto://postgres:postgres@db/milk
      - SECRET_KEY_BASE=KL6jdYa8VzDIg1TjqQcGZsaGSnd43grQYWrl95dYjn/SJB7KABj7N4BWgaCxSF81
    ports:
      - 4000:4000
    tty: true
    networks:
      - backend
    depends_on:
      - db
** (DBConnection.ConnectionError) tcp connect (db:5432): non-existing domain - :nxdomain
    (db_connection) lib/db_connection/connection.ex:87: DBConnection.Connection.connect/2
    (connection) lib/connection.ex:622: Connection.enter_connect/5
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: nil
** (Mix) The database for Milk.Repo couldn't be created: killed
ERROR: Service 'app' failed to build : The command '/bin/sh -c mix ecto.create' returned a non-zero code: 1

It still shows same error. Please give me more ideas! :cry:

Does it work if you explicitly start db and wait for it to start before starting the app container?

1 Like

The issue is happening at build time when you do RUN mix ecto.create. At that point Docker has no idea about your database. Networking or depends_on won’t help with that.

Normally you would create your database at runtime after everything is built and running. The same goes for running migrations.

3 Likes

I’m checking it out. I can only see the result of docker-compose build for now.

Building db
Step 1/3 : FROM postgres:13.0-alpine
 ---> f9dc9f9f4f4d
Step 2/3 : CMD ["postgres"]
 ---> Using cache
 ---> 78f381ce5637
Step 3/3 : EXPOSE 5432
 ---> Using cache
 ---> 386fe8ef73f7

Successfully built 386fe8ef73f7
Successfully tagged milk_db:latest
Building app
Step 1/23 : FROM elixir:1.9.4-alpine AS build
 ---> 1715dc516645
Step 2/23 : RUN apk add --no-cache build-base npm git python
 ---> Using cache
 ---> 31b34ab728fc
Step 3/23 : WORKDIR /app
 ---> Using cache
 ---> 27d383fd7c35
Step 4/23 : RUN mix local.hex --force &&     mix local.rebar --force
 ---> Using cache
 ---> 5ce0454743ae
Step 5/23 : ENV MIX_ENV=prod
 ---> Using cache
 ---> 9e362524279b
Step 6/23 : COPY mix.exs mix.lock ./
 ---> Using cache
 ---> cb3ac4b775cc
Step 7/23 : COPY config config
 ---> Using cache
 ---> b115766bf6c9
Step 8/23 : RUN mix do deps.get, deps.compile
 ---> Using cache
 ---> d048c0782ca2
Step 9/23 : COPY priv priv
 ---> Using cache
 ---> d2f387232cde
Step 10/23 : COPY lib lib
 ---> Using cache
 ---> 692036868ed3
Step 11/23 : COPY rel rel
 ---> Using cache
 ---> 856a55ef96c2
Step 12/23 : RUN mix compile
 ---> Using cache
 ---> 3e7888ed0e02
Step 13/23 : RUN mix ecto.create
 ---> Running in 8fd739bf4e4c

17:56:59.248 [error] GenServer #PID<0.271.0> terminating
** (DBConnection.ConnectionError) tcp connect (db:5432): non-existing domain - :nxdomain
    (db_connection) lib/db_connection/connection.ex:87: DBConnection.Connection.connect/2
    (connection) lib/connection.ex:622: Connection.enter_connect/5
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: nil
** (Mix) The database for Milk.Repo couldn't be created: killed

Please tell me if I’m doing something weird :cry:

Sorry for my lack of knowledge, do I have to build db container before building application?
So should I stop using docker-compose.yml and build postgres container and phoenix application container one by one? :cry:

Step 1 is to build your image with nothing about it being related to your database. Building should do nothing except provide an image that will get run in the future. From a “here’s how Docker works” standpoint it doesn’t really make sense for an image to connect to another running container at build time.

So in this case remove the database creation and migration instructions out of your Dockerfile and you’ll be 1 step closer to be going good to go. You can continue using Docker Compose.

The takeaway there is you need to decouple building and running a container. Running a container is when you can introduce the idea of running networked containers together or performing actions on other containers that may or may not be running (such as creating your database).

If you want to run things automatically when a container starts (sort of like how running PostgreSQL in Docker will automatically create a database if one doesn’t exist based on those environment variables you set) you can use another Docker feature called entrypoint scripts. These happen at runtime when your container starts.

3 Likes

Usually, we build the image and then use docker-compose run container one-off cmd for migrations and any other functions or nickjanetakis pointed out use the entrypoints to wait for the initialization.

1 Like

Thank you.

I’m trying this entrypoint.sh.

#!/bin/bash

DB_USER=${DATABASE_USER:-postgres}

while ! pg_isread -q -h $DATABASE_HOST -p 5432 -U $DB_USER
do
  echo "$(date) - waiting for database to start"
  sleep 2
done

bin="/app/bin/milk"
#eval "$bin eval \"Milk.Release.migrate\""
MIX_ENV=prod
mix ecto.create
mix ecto.migrate
echo "migrated"

# start the elixir application
exec "$bin" "start"

And make the code call entrypoint.sh in Dockerfile.

FROM elixir:1.9.4-alpine AS build

RUN apk add --no-cache build-base npm git python

WORKDIR /app

RUN mix local.hex --force && \
    mix local.rebar --force

ENV MIX_ENV=prod

COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile

COPY priv priv
COPY lib lib
COPY rel rel
RUN mix compile
#RUN mix ecto.create
#RUN mix ecto.migrate
RUN mix release

FROM alpine:3.9 AS app
RUN apk add --no-cache openssl ncurses-libs

WORKDIR /app

RUN chown nobody:nobody /app

USER nobody:nobody

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/milk ./

ENV HOME=/app

COPY entrypoint.sh .

CMD ["bash", "./entrypoint.sh"]

And then, I ran docker-compose build and docker-compose up -d.
The result of docker-compose ps -a is

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
916d50165153        milk_app            "bin/milk start"         13 minutes ago      Up 13 minutes       0.0.0.0:4000->4000/tcp   milk_app_1
9748614e38af        milk_db             "docker-entrypoint.s…"   16 minutes ago      Up 16 minutes       0.0.0.0:5432->5432/tcp   milk_db_1

I think COMMAND of milk_app_1 should be “bash entrypoint.sh”.
What is wrong?

I tried to write entrypoint.sh, but it does not work. What do I have to fix?

Do change the CMD to ENTRYPOINT.for making it entrypoint

Also, you would need postgresql client installed in your app container to use the below commands.

Also, FYI, there is NO MIX in releases. So you cannot use mix for managing your db tasks. Instead use the custom migration script from this link. This is the official link for managing elixir based application releases.

https://hexdocs.pm/phoenix/releases.html#ecto-migrations-and-custom-commands

I have listed a thread already that has most of the examples to achieve what you’re looking for.

1 Like

https://hexdocs.pm/phoenix/releases.html#content

This is the full content for managing releases.

1 Like

I’m checking whether it works though, it seems working properly!
Thank you very much :smile: