Use docker para tudo. Não só banco de dados.

docker-compose.yaml

  #
  #
  # API
  #
  #
  backend:
    container_name: backend
    build:
      context: .
      dockerfile: ./backend/Dockerfile.development
    restart: unless-stopped
    ports:
      - 3300:3300
    networks:
      - qrdapio
    depends_on:
      - postgres
      - redis
    volumes:
      - ./backend:/home/node/app

e o arquivo dockerfile.development referenciado no docker compose

FROM node:20.11.1

WORKDIR  /home/node/app

USER node

# Subir o docker sem executar. 
# A execução será feita separadamente.
CMD [ "tail", "-f", "/dev/null" ]

Destaco que o código acima eu só uso pra desenvolvimento. Abaixo segue o dockerfile de produção

#  Cria a imagem para testar se está tudo ok
#  docker build -t qrdapio/backend:1.0 .
# 
# 

FROM node:20.11.1 as build

WORKDIR  /home/node/app
# USER node

COPY package*.json ./
RUN npm install --force
COPY . .

ENV HOST 0.0.0.0
ENV PORT 8080

RUN npm run build

# 

FROM node:20.11.1
WORKDIR  /home/node/app
COPY package*.json ./



RUN npm install --production
COPY --from=build /home/node/app/dist ./dist

# Executa com a função interna para poder ter acesso
# à versão definida em package.json
CMD [ "npm", "run", "start:prod" ]
# CMD [ "node", "./dist/src/main.js" ]

Para desenvolver, observe que o docker compose fica em uma pasta e o dockerfile fica em uma subpasta chamada backend. O dockerfile de desenvolvimento apenas inicia o container. você precisa acessar o container usando docker compose exec backend bash e lá dentro você faz o npm run start:dev.

Então, resumidamente, você sempre irá trabalhar dentro do container. A pasta de trabalho está compartilhada com o docker através da criação do volume lá dentro do docker compose.

Em produção, você usa o dockerfile.

E é só isso. Seguindo essa lógica, o seu dockercompose consegue subir seu ambiente inteiro, inclusive frontend, site etc.