skip to Main Content

Problem

I would like to request to API wordpress server(localhost:8000/wp/graphql) from frontend app by fetch function. But, the following error appeared.

Server Error
FetchError: request to http://localhost:8000/wp/graphql failed, reason: connect ECONNREFUSED 127.0.0.1:8000

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
<unknown> (FetchError: request to
http://localhost:8000/wp/graphql%20failed,%20reason:%20connect%20ECONNREFUSED%20127.0.0.1 (8000)

Question

How does the frontend app connect to API server on docker when developing locally?

Please give me some advice.
Thank you in advance.


Infos

The following server was hosted correctly(wordpress)

localhost:8000

endpoint(/wp/graphql) is also working correctly ( from curl or browser )

The following server was hosted correctly(Next.js)

localhost:3100

Directory strategy

  • Dockerfile.frontend
  • docker-compose.yaml
  • frontend(Next.js)
  • api(worpress)

My docker-compose.yaml

version: "3"

services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: wordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
  
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports: 
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress 
      WORDPRESS_DB_PASSWORD: wordpress 
      WORDPRESS_DB_NAME: wordpress 
    volumes:
      - ./api:/var/www/html
    working_dir: "/var/www/html/wp"

  frontend:
    build:
      context: .
      dockerfile: Dockerfile.frontend
    volumes:
      - ./frontend:/usr/src/app
    restart: always
    command: sh -c "cd app && yarn dev"
    ports:
      - "3100:3000"

volumes:
  db_data: {}


My codes which is requesting to API(Next.js)

export async function getStaticProps({ params }) {

  const data = await fetch('http://localhost:8000/wp/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `
        {
            posts {
              edges {
                node {
                  date
                  title
                  slug
                }
              }
            }
        }
    `,
  }),
})

  const json = await data.json()

  return {
    props: {
      posts: json.data
    }
  }
}


Additional Infos

①I tried host.docker.internal. The following eror appeared

Server Error FetchError: invalid json response body at host.docker.internal:8000/wp/graphql reason: Unexpected token < in JSON at position 0

②Referencing container name directly

This shows the same error as the original error

③IP address

This also doesn’t work. Maybe the problem is that wordpress endpoint (/wp/graphql) doesn’t work by the IP addresses (127.0.0.1 and my network IP)

I’m thinking that the problem may be in communicating between containers… Thank you anyway.

5

Answers


  1. instead of localhost, maybe you can try using host.docker.internal

    so basically

    export async function getStaticProps({ params }) {
    
      const data = await fetch('http://host.docker.internal:8000/wp/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: `
            {
                posts {
                  edges {
                    node {
                      date
                      title
                      slug
                    }
                  }
                }
            }
        `,
      }),
    })
    
      const json = await data.json()
    
      return {
        props: {
          posts: json.data
        }
      }
    }
    

    Alternatively you can reference the container by name directly so

    http://wordpress:8000 should work too.

    Lastly, you can reference it using ip address

    Login or Signup to reply.
  2. Containers can inter-communicate using container names (e.g. wordpress, php, mysql etc.) and original ports (80, 3000).

    Port mappings are used for external access – in this case: docker internal wordpress:80 into [docker] host localhost:8000.

    You can check this – log into frontend container and run (or run ‘externally’) wget http://wordpress/index.php. Response should look like:

    Connecting to php (172.16.238.10:80)
    saving to 'index.php'
    

    In my case (routing mode, internal networks) you can see the container IP and port – notice, original 80.

    Login or Signup to reply.
  3. I think that Unexpected token < in JSON at position 0 you get when using host.docker.internal. is because wordpress is returning the home page html as a response to your next app. You could verify this by inspecting the response. The same thing should happen when you use wordpress container name in the api uri – ex. http://wordpress/graphql

    This is because wordpress is unable to resolve the handler for the /graphql endpoint because of what is set as the WordPress Address (URL) (settings/general or wp_options wordpress db table)

    If you change the WordPress Address (URL) to http://wordpress (your service name) it should work, but this isn’t ideal because the wordpress URL shouldn’t depend on the service name.

    What I did is alter the next service to use the host network mode in order to see the network interface of the docker host:

     frontend:
        network_mode: "host"
        ...
    

    It should now be able hit the wordpress container normally without being part of the internal bridge network.

    Login or Signup to reply.
  4. TL;DR
    Use whatever you want in place of "example".

    1. Set up WordPress with a site URL and site address in wp-admin to http://example.com

    http:// must be included

    1. Edit Windows c:/windows/system32/dirvers/etc/hosts file to add

      127.0.0.1 example.com

    2. Make some changes to your docker-compose file, by putting WordPress and nextjs in a network and giving WordPress a hostname.

    If you need any clarification everything is listed out below. Enjoy!


    Problem:
    You have to make the fetch request to the same domain that WP thinks it is. This is written in the WP database, in the options table, row 1: siteurl. I think the problem that is occurring is that making a request to any domain other than the one shown here will trigger a url rewrite in WP(probably from .htaccess rule, apache config, or WP api add_rewrite_rule) changing the domain from whatever your fetch request is directed at to whatever is written in the database. I’m not certain if this totally scraps your original fetch request, or if it changes the origin header and since the request origin header and response origin don’t match, node-fetch throws an error (maybe someone can clarify that for me).

    I’ll write down all the steps I took below, not necessarily in the order I executed them, but in the order that I would try next time. There are some steps that may be unnecessary so I’ll put those at the end and someone can respond with an updated list that doesn’t involve extra steps.

    0. Flush rewrite rules:

    You can do a quick search of how to do this, but I believe you go to wp-admin > settings > permalinks and click update. This is just to make sure that the /graphql endpoint is working properly.

    1. Set siteurl and home DB values:

    option a. You can go to your wp-admin > settings > general and change wordpress address and site address to something like http://example.com. The name you choose here can be completely arbitrary, but choose a url you never plan to visit in your browser, as we will be changing windows hosts file later, so don’t choose google.com or something like that. Anyway, make sure you include http:// as that will make it automatically use port 80.
    option b. If you’re using the WP container there may be environment variables you can add to your docker-compose file that will do this for you at build time, but you’ll need to check their documentation on docker hub or github.
    option c. Alternatively you could set up a phpMyAdmin container on your db network to manually change the values, which is what I did.

    Your WP container probably won’t work now. That’s ok.

    2. Edit your PC hosts file

    If you want to be able to access your WP from its new domain name, you’ll need to tell Windows, or whatever OS, that http://example.com has a local IP and it shouldn’t try to fetch that from a DNS server.
    The process to do this on Windows is: Go to windows search bar, type "notepad", right click on the notepad, run as administrator. Then go to file > open > c:/windows/system32/drivers/etc, change file type from text documents to all files and select the hosts file.
    Once that file is open you will probably see a list of ip’s and hostnames. Notice that host.docker.internal is also listed here. XD
    All you do is type an ip address followed by example.com(the same you used for the previous step in WP, nix the http://). Here are some options as to how you could go about this.

    Use Loopback IP:

    #in hosts file 
    127.0.0.1 example.com
    

    You can look up what loopback ips are currently in use on your local machine by opening CMD and typing

    netstat -a -n -p TCP
    

    They’ll probably all be 127.0.0.1 on various ports. I believe you can use any 127.0.0.0/8 ip for this, meaning the zeros can be any number between 0-255, but I also won’t guarantee that.
    If 127.0.0.1:80 is listed there already, perhaps try 127.12.34.156 or something(random numbers I just made up). We will need port 80 on the selected ip to be available for this to work.
    OR Use Local Network Address:
    If you’re connected to a router, your machine will publish open ports on your machine there to some 192.168.x.x address. You’ll have to do more research on how that’s set up, but it should be whichever 192.168 local address is listed with the 0.0.0.0 foreign address in the netstat output.

    Sidenote, this is cool because if your phone is on the same Wi-Fi
    network you can go to that 192.168.x.x:exposed-container-port and view
    your apps on mobile while developing.

    You can add this address to your hosts file like so, if it’s available.

    #in hosts file
    192.168.x.x example.com
    

    Great. Now that that’s done you have to restart your computer. Is there a way to reinitialize the hosts file without restart? I don’t know but maybe someone else can comment.

    The Reason this is a necessary step:
    As mentioned in "the problem" section, WP does weird stuff to your urls. It needs to be requested from the browser as the name that it believes it’s called(maybe not always in vanilla WP, but could just be a theme/plugin issue).

    Now you can start up your WP container with docker compose WITH ONE CHANGE:

    #in docker-compose.yml
    services:
      wordpress:
        ports:
          - 80:80
    

    You need to expose port 80 of the WP container.

    In a hosts file you cannot add ports to the ip addresses

    # don't do it
    127.0.0.1:8000 example.com       #it won't work, don't do it
    # it doesn't work
    

    We need to expose this port because we’re going to use the http:// protocol to access our WP site. There is a way apparently that you can map ports to these hosts using netsh, but you’ll have to learn about that elsewhere. That will be required if you need multiple containers to expose port 80. For our use case, we only have one container exposing port 80 so it’s fine.

    So you’ve done the following:

    1. Change WP siteurl and homeurl
    2. Add to hosts file
    3. Change docker compose to expose WP container to port 80

    You can now run your WP container to make sure everything is working. Once it’s up, try visiting http://example.com or whatever you named your wp. Your WP should be completely functional. If not you might need to flush rewrite rules again.

    3. Network your containers

    #docker-compose.yml
    version: 3.8
    
    services:
      #...
      #db and what not
      # db is on backend network and does not expose ports
      wordpress:
        hostname: example.com
        networks:
          frontend:
          backend:
      nextjs:
        networks:
          frontend:
    
    networks:
      frontend:
      backend:
    

    Ok, so what did we add here? By default, your docker containers will be added to the "default [bridge] network". This means that they are in their own private subnet which has its own ip address and gateway ip. The thing about this network is that you can make internal http requests to your other containers by their service name. The service names I used in the above yml file are wordpress and nextjs. So I could make a server side fetch request from the nextjs application directly to ‘http://wordpress’ and it would find that container(and http defaults to port 80, otherwise you can specify the port as well).

    // wordpress doesn't like this request for some reason
    const data = await fetch('http://wordpress/graphql', request);
    

    The problem with that is when we make such a request, the ‘wordpress’ container pulls some funny business with the rewrites or request headers, I’m not sure which, and we can’t get an appropriate response in the nextjs container.

    So what we’ve done here is put our applications in some different networks. The backend network is for WP and the DB to communicate and frontend network is for Next and WP to communicate.

    One very important line here is:

    hostname: example.com
    

    When we run docker-compose up, docker will add the following to the container’s /etc/hosts file

    172.0.0.x example.com example
    

    If you want to check this, spin up you containers, open CMD and type

    docker ps
    

    and look at either the CONTAINER ID or NAMES column, then type

    docker exec -it CONTAINER_ID_HERE bash
    or
    docker exec -it CONTAINER_NAME_HERE bash
    

    and replace the id or name with the id or name of your wordpress container. Also you might have to replace the ‘bash’ part with ‘sh’ depending on your linux distro.
    What you should see in your cli is

    root@example:/var/www/html
    

    Now type one by one

    cd /etc
    cat hosts
    exit
    

    The cat command will print the hosts file contents into the cli. You should see in the list:

    172.0.0.x example.com example
    

    Where x is corresponds to your container’s ip on the docker bridge networks to which it is assigned. Since I have two networks in the above compose.yml file, mine will show two ip’s with the hostname designation– one for each network to which the container is attached; in this case "frontend" and "backend".

    What’s the next step?

    That’s it. You’re done.

    You can now make a server-side node-fetch request like so:

    // inside of some server-side function i.e. in /pages/api/graphqltest.js    
    //...
    
    const data = await fetch('http://example.com/graphql', request)
    
    // handle data...
    

    A note about DNS resolution in networks.

    Let me try and explain what’s going on with these hosts files as well as I understand it. If I make any mistake, someone can notify me in the comments and I’ll do my best to correct it here.

    When node makes a server-side request, how it resolves the domain name specified is going to bubble outward from the container.

    Upon request of http://example.com, the server will look first in its hosts file to see if there is a mention of example.com or example. If it’s there then it will resolve to whatever IP is listed. in our case there is no example.com listed in the hosts file of the nextjs container. When we set it in the wordpress container it modifies the hosts file for the wordpress container only. Well, it also defines it for the networks we set up on that container, frontend and backend.

    Since the nextsjs app doesn’t have this hostname on file, the request next goes to the network. The packet will hit the network’s internet gateway. If there is a container in the network with the requested hostname(example.com) in its hosts file, the gateway will send that packet to the container. This is the case in our example.

    In order to prove it, try the following. In CMD type:

    docker exec -it nextjs sh
    

    Here, nextjs should be replaced with the name of your container which you can see using ‘docker ps’ command. Also the ‘sh’ option puts us in the command shell for the node-alpine image, but if you use another image you might need to change this to ‘bash’

    Once in the shell you can type:

    apk add tcptraceroute
    

    And install the traceroute package. Next do:

    tcptraceroute example.com
    

    The output should look like this:

    Selected device eth0, address 172.x.0.2, port xxxxx for outgoing packets
    Tracing the path to example.com (172.x.0.3) on TCP port 80 (http), 30 hops max
     1  172.x.0.3 [open]  0.099 ms  0.050 ms  0.097 ms
    

    Let me translate:

    –Selected device eth0, address 172.x.0.2, port xxxxx for outgoing packets

    The address 172.x.0.2 we see here is the privat IPV4 address of the nextjs app in the frontend network.

    –Tracing the path to example.com (172.x.0.3) on TCP port 80 (http), 30 hops max

    The address 172.x.0.3 is the private IPV4 address of the wordpress container inside the frontend network

    –1 172.x.0.3 [open] 0.099 ms 0.050 ms 0.097 ms

    Took one hop, found the host at 172.x.0.3, it’s open, took only a fraction of a second.

    Don’t forget to exit the shell

    exit
    

    Conclusion

    I know this is a long-winded answer but it’s really just a lot of explanation for a few simple steps:

    1. Change wordpress siteurl
    2. Add to system hosts file
    3. Make a few changes to dockerfile

    Here is a complete docker-compose.yml for the copy-pasters:

    # docker-compose.yml
    version: '3.8'
    
    services:
    
      db:
        image: mariadb:10.3
        restart: always
        volumes:
          - ./db/mysqldata:/var/lib/mysql
        environment:
          MYSQL_ROOT_PASSWORD: badpassword
          MYSQL_DATABASE: WP_BADNAME
          MYSQL_USER: badusername
          MYSQL_PASSWORD: badusernamespassword
        networks:
            - backend
    
      phpmyadmin:
        image: phpmyadmin
        restart: always
        ports:
          - 8080:80
        environment:
          - PMA_ARBITRARY=1
        networks:
          - backend
    
      wordpress:
        image: wordpress
        restart: always
        hostname: example.com
        volumes:
          # You might want to copy the files 
          # into the container in a dockerfile
          # for better performance. Alternatively,
          # You might set up your installation using 'environment' vars.
          # I use a mix of both. As executed here, changes to these files
          # will persist, but performance will suffer
          - ./app/wp/wp-config.php:/var/www/html/wp-config.php
          - ./app/wp/wp-content:/var/www/html/wp-content
          - ./app/wp/.htaccess:/var/www/html/.htaccess
        ports:
          - 80:80
        networks:
          backend:
          frontend:
    
    
      nextjs:
        build:
          context: ./app/frontend
          dockerfile: Dockerfile.dev
        restart: always
        volumes:
          - ./app/frontend/:/app
          - /app/node_modules
          - /app/.next
        ports:
          - 3000:3000
        networks:
          - frontend
    
    networks:
        frontend:
        backend:
    

    And my Dockerfile.dev in the nextjs directory look like this:

    FROM node:current-alpine
    WORKDIR /app
    COPY package.json .
    RUN npm install
    COPY . .
    
    Expose 3000
    CMD ["npm", "run", "dev"]
    

    and to run it, because next uses ‘build’

    docker-compose up --build
    
    Login or Signup to reply.
  5. Stuck with the same problem and just solved with network aliases feature.

    My docker-compose.yml simplified example:

    version: '3.9'
    services:
      web:
        build: .
        image: my_image
        container_name: web
        ports:
          - 3000:3000
        networks:
          - frontend
      nginx:
        depends_on:
          - cms
          - web
        image: nginx:1.20-alpine
        container_name: nginx
        ports:
          - 80:80
        networks:
          frontend:
            aliases:
              - nginx
              # Now you can request nginx with http://wp.site.local/graphql from frontend container! ;)
              # nginx proxied wp.site.local to cms container
              - wp.site.local
    
      cms:
        depends_on:
          - db
        image: wordpress:5.8.2-fpm-alpine
        container_name: cms
        networks:
          - frontend
          - backend
    
      db:
        image: mysql:8.0
        container_name: db
        networks:
          - backend
    
    

    If you need details about nginx.conf please let me know

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search