How not expose my backend I'm studying Docker and how to properly publish my containers coding a very simple web application. It has only two containers, backend and frontend. I've published the source code here: https://github.com/sinayra/hello-private-world
If I run locally each node application, its works &quot;fine&quot; (everything is public, but this is not what I want in the end of the day). Then, I created Dockerfile for each container, to try to simulate a real application. For the frontend, I exposed the port 3000 to access from the host. However, I don't want to expose the port 3333 for the host, and, because of that, my front can't fetch the data from the back.
Here is my index.html from the frontend.
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Hello Public World&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Message from backend:&lt;/h1&gt;
    &lt;p id=&quot;backend&quot;&gt;&lt;/p&gt;

    &lt;script&gt;
      const back = document.getElementById(&quot;backend&quot;);
      const url = &quot;http://localhost:3333/&quot;; //I tried backend:3333, but without success

      fetch(url)
        .then((resp) =&gt; resp.json()) // Transform the data into json
        .then(function (data) {
          back.innerHTML = data.message;
        });
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;

Its Dockerfile:
# pull official base image
FROM node:lts

# set working directory
WORKDIR /usr/src/app

# install app dependencies
COPY package.json ./
RUN npm install

# add app
COPY . ./

EXPOSE 3000

# start app
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]

My index.js from backend:
const express = require(&quot;express&quot;);
const app = express();
const cors = require(&quot;cors&quot;);
const port = 3333;

const allowlist = [&quot;http://localhost:3000&quot;];
const corsOptions = {
  origin: allowlist,
  methods: &quot;GET&quot;,
  preflightContinue: false,
  optionsSuccessStatus: 204,
};
app.use(cors(corsOptions));

app.use(express.json());
app.get(&quot;/&quot;, (req, res) =&gt; {
  console.log(&quot;GET /&quot;);
  res.json({ message: &quot;Hello Private World!&quot; });
});

app.listen(port, () =&gt;
  console.log(`Example app listening at http://localhost:${port}`)
);

Its Dockerfile:
# pull official base image
FROM node:lts

# set working directory
WORKDIR /usr/src/app

# install app dependencies
COPY package.json ./
RUN npm install

# add app
COPY . ./

#EXPOSE 3333

# start app
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]

And the Docker compose file
version: &quot;3.8&quot;
services:

  backend:
    image: helloworld-private
    build:
      context: ./backend
      dockerfile: Dockerfile
    working_dir: /usr/src/app
    container_name: &quot;express_container&quot;
    volumes:
      - ./backend/:/usr/src/app
      - /usr/src/app/node_modules
    networks: 
      - webappnetwork
    expose:
      - &quot;3333&quot;

  web:
    image: helloworld-public
    build:
      context: ./web
      dockerfile: Dockerfile
    working_dir: /usr/src/app
    container_name: &quot;liveserver_container&quot;
    links:
      - backend
    volumes:
      - ./web/:/usr/src/app
      - /usr/src/app/node_modules
    environment:
      - PORT=3000
    networks: 
      - webappnetwork
    ports: 
      - &quot;3000:3000&quot;

networks:
  webappnetwork:
    driver: bridge

Is there any abstraction layer that I'm missing?
The layer that you're missing is nginx proxy. See my toy Mafia Game project here how to do it:

Shell deployment project (uses either Docker compose or k8s) - https://github.com/taleodor/mafia-deployment
Vue.js frontend - https://github.com/taleodor/mafia-vue
Express.js backend - https://github.com/taleodor/mafia-express
(Optional) detailed write-up about CI/CD and how it's built from DevOps prospective - https://itnext.io/building-kubernetes-cicd-pipeline-with-github-actions-argocd-and-reliza-hub-e7120b9be870

The missing piece in your case is nginx proxy that you use in your UI.
So what you need to do is to define some location for your api - i.e. /api. Then your proxy it via nginx - see my nginx configuration for the project above here - https://github.com/taleodor/mafia-vue/tree/master/nginx. At the same time it's highly recommended for performance to deploy static resources on the same nginx.
Use Dockerfile to define environment variables and defaults, i.e. in my project - https://github.com/taleodor/mafia-vue/blob/master/Dockerfile
Finally, you can update configuration via environment variables in your docker-compose yaml or k8s definitions.