Search code examples
dockeraxiosvuejs3cypresslocal

Use axios in dev environment inside docker network AND from docker host


I'm building a web application, that's supposed to run on different docker containers:

version: "3.8"
services:
  backend:
    build: ./backend
    container_name: app-be
    ports:
      - 8000:8000
    volumes:
      - ./backend:/app

  frontend:
    build: ./frontend
    container_name: app-fe
    ports:
      - 5173:5173
    volumes:
      - ./frontend:/app

  test:
    build: ./test
    container_name: app-e2e
    command: npx cypress run -b firefox
    environment:
      - CYPRESS_baseUrl=http://frontend:5173
    tty: true
    volumes:
      - ./test:/app

The frontend is a vuejs app, and I am trying to access the backend via axios; when starting up everything, the app also gets tested by cypress. But I also want to see my changes locally; which is creating my problem:

In order to access the backend from the host, axios would need to use http://localhost:8000 as the address for the backend, but then of course the Cypress tests would fail; for those axios would need to access http://backend:8000 which again is unknown to the host. However I want both cases covered.

export default {
  data() {
    return { msg: '' };
  },
  methods: {
    getMessage() {
      axios.get('http://backend:8000/') // <-- change to localhost when running in browser
           .then((res) => { this.msg = res.data; })
           .catch((error) => { console.error(error);});
    },
  },
  created() {
    this.getMessage();
  },
};

Coming from backend development, I would usually solve something like this with different environment files that get sourced according to the environment - but in this case axios runs as JavaScript on the client, so this cannot be solved on the server side, as it is the same server used. One could change this, of course - have two frontend instances running - one for the cypress tests and another for local development, and start them up with different environment variables, but that feels somewhat dirty.

Another workaround is adding the line 127.0.0.1 backend to the systems /etc/hosts, but I don't feel good with this solution either; for me, one main benefit of using docker is to enable others to just download the project and run it without the need of installing anything on their local machine. Needing root rights to change an essential system config somehow defeats that idea.

So my question: Is there a better way of solving this problem that I am not seeing? Or are these really the only two options available?


Solution

  • So I found a better solution that I am happy with, thanks to the comment by David Maze, but it involved some more work.

    After adding the nginx proxy as explained, i.e.

    • adding to docker-compose:
    proxy:
        build: ./proxy
        container_name: app-prx
        ports:
            - 80:80
    
    • introducing new Dockerfile in ./proxy/:
    FROM nginx:1.25.2-alpine
    COPY default.conf /etc/nginx/conf.d
    
    • introducing new config file for nginx in ./proxy/default.conf:
    
        server {
            listen 80;
            server_name localhost;
            location / {
                proxy_pass http://frontend:5173/;
            }
            location /api/ {
                proxy_pass http://backend:8000/;
            }
        }
    
    

    and then fixing Cypress to visit http://proxy/ and fixing the frontend calls to the backend from http://backend:8000/ to /api/, the project would actually show the correct result in the local browser. However, my cypress tests where failing with a strange message:

    An invalid or illegal string was specified

    Turns out, that when looking into the console locally you get a more helpful message:

    
        Firefox can't establish a connection to the server at ws://localhost/.
        Uncaught (in promise) DOMException: An invalid or illegal string was specified
            setupWebSocet client.ts: 77
            fallback      client.ts: 42
        (...)
    
    

    I don't know why this is happening, but for some reason when adding an nginx, vite needs to have a websocket specified in the vite.config.js:

    export default defineConfig({
        plugins: [
            vue(),
        ],
        (...)
        server: {
            hmr: {
                port: 5173
                host: "localhost",
                protocol: "ws",
            },
        },
    })
    

    NB: It is important to add all three components - I first left out the port, which led to the same error as before (and therefore took me even longer to figure this out). Depending on how you start your frontend, this might lead to a new error:

    
        Port 5173 is in use, trying another one.
        error when starting dev server:
        Error [ERR_SERVER_ALREADY_LISTEN]: Listen method has been called more than once without closing.
    
    

    In this case make sure, that you are explicitly starting the frontend with a port specified. In my case, I am doing this at the end of my Dockerfile for my frontend:

    
        CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
    
    

    For me, this is a far superior setup compared to the other two I presented above for 4 reasons:

    1. It solves my initial problem, i.e. it's runnable both inside docker and from the host, without needing to change the host.
    2. It also simplifies all the routes in the frontend (before I created an instance of axios as helper to set the baseURL: 'http://backend:8000' and export it to all my components - no need for that anymore).
    3. You'd probably want an nginx anyways as soon as you go into production.
    4. Given 3, you're not spawning "useless" docker containers that are just copies of other containers with different environment settings just to satisfy client-side Browser scripts.