Help connecting OpenProject and Nextcloud in Docker (Traefik reverse proxy + DNS issue?)

Hi,

I'm working on a local Docker setup (Windows + Docker Desktop) and trying to integrate Nextcloud and OpenProject, both behind Traefik (v3.4) as reverse proxy. I'm not a DevOps expert, so I'm hoping for some guidance.

Setup:
Traefik (v3.4) as a reverse proxy
Nextcloud (official image)
OpenProject (v16.1-slim)

All services share a Docker network: traefik-frontend.

What I want to do is simple: make Nextcloud talk to OpenProject via the OpenProject integration app. In Nextcloud’s OpenProject integration app, I entered:

http://openproject.docker.localhost/

That works from my browser (host machine), but from Nextcloud I get:
“There is no valid OpenProject instance at this URL”, and the logs show a 400 Bad Request.

What I tested:
Inside Nextcloud container:
curl openproject.docker.localhost → returns Nextcloud login page (!)
curl traefik-reverse-proxy-1 -H "Host: openproject.docker.localhost" → returns a 302 redirect to OpenProject (expected)

I added this in OpenProject’s env:
OPENPROJECT_HOST__NAME=openproject.docker.localhost

My Traefik config has correct labels, and everything routes fine from the host.

I tried adding aliases (in Traefik or in Nextcloud), and it works when I alias openproject.docker.localhost to Traefik in Nextcloud’s network config — but that feels like a workaround.

What I’d really like is a solution that works in dev but could also adapt to a production (VPS) setup later

Any tips or best practices? I’m fine with modifying docker-compose.override.yml or adding config in Traefik if needed, but I’d prefer not to rely on hardcoded IPs or temporary hacks unless absolutely necessary.

Thanks in advance for any help

You can't use localhost to connect between containers, as that is just localhost within the container itself, not on the host machine.

If you use Docker, you would usually connect via their compose service name or their container name.

Thank you very much for your answer ! yeah I figured localhost probably wasn’t the right approach between containers, but I’m still trying to understand how things are supposed to work properly in this context.

I’m not trying to reach the OpenProject container directly, the request is meant to go through Traefik, using openproject.docker.localhost so that Traefik can route based on the Host header. That part works from my host machine, but inside the Nextcloud container, the same URL seems to resolve back to itself (I get the Nextcloud login page = 302 instead of OpenProject).

I might be misunderstanding how DNS resolution works inside Docker in this case. I thought putting both containers on the same network and using a domain name that Traefik is watching would be enough, but clearly something’s not lining up.

If there’s a more standard or reliable way to handle this kind of internal routing through Traefik, I’d really appreciate any clarification. I can share files if helpful.

Again, you can't really use any localhost when dealing with containers.

When using URLs, they need to

  1. Be resolved to the right target IP
  2. Be matched by the Traefik router rule

Within Docker containers, you can just use something like openproject if the serice or container has that name.

But this does not work when the services interact with each other via the PC browser (linking to each other), there the resolving of Docker names will not work. But from the browser localhostworks, if the container publishes the port.

I think I got it, thanks for the clarification.

Just to be sure:
Could the issue be that I’m using a domain like openproject.docker.localhost?

I didn’t use plain 'localhost', but I now understand that anything ending in '.localhost' might still resolve to loopback (127.0.0.1), even inside containers — which would explain why the request loops back to the calling container instead of reaching Traefik.

If that’s the case, switching to something like 'openproject.local' or 'openproject.internal' should fix it, right?

Correct.

But for openproject.local to work, you need to create a resolver with the target IP, via local DNS service (router?) or hosts file.

Thanks again for your help.

I'm running a defreitas/dns-proxy-server container as a local DNS inside Docker. It’s on the same external Docker network (traefik-frontend) as Traefik, Nextcloud, and OpenProject. I mounted the Docker socket as expected, and enabled container name registration (MG_REGISTER_CONTAINER_NAMES=1). The container starts fine and logs show that it registers container hostnames correctly.

On the host side (Windows), I didn’t change any system DNS settings — I only added entries to the C:\Windows\System32\drivers\etc\hosts file like:

127.0.0.1 openproject.lab.test
127.0.0.1 nextcloud.lab.test
etc...

That allows me to access the apps in the browser through Traefik, and that part works.

But I wanted to try to test all together and now the problem: Traefik fails to resolve openproject.lab.test internally, which breaks my setup. Here’s the exact error in Traefik’s logs:

Health check failed. error="HTTP request failed: Get \"http://openproject.lab.test/login?...\": dial tcp: lookup openproject.lab.test on 127.0.0.11:53: no such host"

So it looks like Traefik is still using Docker’s default DNS (127.0.0.11) and not the dns-proxy-server container.

I assumed that just having everything on the same Docker network would allow Traefik to resolve the domains via the proxy, but I guess not?

Do I need to manually set a dns: entry in the Traefik docker-compose.yml?

Do you have any idea ?

I’m not very advanced with DNS or Docker internals, so I might be missing something simple. Just trying to let Nextcloud communicate with OpenProject using clean .lab.test hostnames, routed through Traefik.

BR

Nextcloud communicate with OpenProject

How should that work? From container to container, from container through Traefik container to container? Or via browser through Traefik container to container?

Maybe we start again an you share your full Traefik static and dynamic config, and Docker compose file(s).

Thanks for following up. I probably jumped over some key details. Here's a clearer explanation of the setup and where I'm stuck.

What I want to do

I'm working on a local Docker setup (Windows + Docker Desktop) and trying to connect Nextcloud and OpenProject via the official integration app in Nextcloud.

The idea is to enter http://openproject.lab.test in the OpenProject integration settings in Nextcloud, and have it route correctly through Traefik, which handles all services via reverse proxy.

So the intended request path is:

Nextcloud (container)Traefik (container)OpenProject (container)

Both apps are on the same external Docker network (traefik-frontend) along with Traefik.

Before introducing DNS proxy

Before trying any DNS solution, I had this setup:

  • From my browser (host), openproject.lab.test worked fine thanks to Windows hosts file:
127.0.0.1 openproject.lab.test
127.0.0.1 nextcloud.lab.test
  • Inside Nextcloud, trying to connect to openproject.lab.test would fail during the integration process with a:
400 Bad Request — “There is no valid OpenProject instance at this URL”
  • But the real issue seemed to be that Nextcloud was resolving the domain to itself, not OpenProject. I confirmed this by running:
curl openproject.lab.test

From within the Nextcloud container — it returned the Nextcloud login page, not OpenProject.

  • So I assumed the problem was DNS resolution inside Docker: all containers were using Docker's internal resolver and couldn't resolve custom FQDNs.

After introducing defreitas/dns-proxy-server

I tried adding dns-proxy-server as a DNS container, hoping to solve the internal resolution between containers. The proxy runs fine, and detects containers with MG_REGISTER_CONTAINER_NAMES=1.

But since then, everything broke, including access Openproject from my browser. Even Traefik now throws:

Health check failed. error="HTTP request failed: Get "http://openproject.lab.test/login?...\": dial tcp: lookup openproject.lab.test on 127.0.0.11:53: no such host"

Files

Let me know if helpful :

  • traefik.yml (static config)
global:
  checkNewVersion: false
  sendAnonymousUsage: false
log:
  level: DEBUG
api:
  dashboard: true
  insecure: true
entryPoints:
  web:
    address: :80
  websecure:
    address: :443
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    network: traefik-frontend
    exposedByDefault: false
  • Docker Compose files for

Traefik

services:
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.4
    # Enables the web UI and tells Traefik to listen to docker
    #command: --api.insecure=true --providers.docker
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
      - "443:443"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config/traefik.yml:/etc/traefik/traefik.yml:ro
    networks:
      - traefik-frontend
    restart: unless-stopped

networks:
  traefik-frontend:
    external: true

Nextcloud

volumes:
  nextcloud:
  db:

services:
  ############################
  #   D A T A B A S E
  ############################
  db:
    image: mariadb:10.6
    restart: unless-stopped
    networks:
      - nextcloud-internal
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 1m
      start_interval: 10s
      interval: 1m
      timeout: 5s
      retries: 3
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    volumes:
      - "db:/var/lib/mysql"
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}

  ############################
  #   A P P L I C A T I O N
  ############################
  app:
    image: nextcloud
    restart: unless-stopped
    networks:
      - traefik-frontend
      - nextcloud-internal
    depends_on:
      db:
        condition: service_healthy
        restart: true
    labels:
      # 1️  Active Traefik pour ce conteneur
      - "traefik.enable=true"

      # 2️  ROUTER – règle d’hôte + point d’entrée
      - "traefik.http.routers.nextcloud.rule=Host(`${NEXTCLOUD_DOMAIN}`)"
      - "traefik.http.routers.nextcloud.entrypoints=web"

      # 3️  MIDDLEWARE retry (5 tentatives max si le conteneur répond 502/504)
      - "traefik.http.routers.nextcloud.middlewares=retry"
      - "traefik.http.middlewares.retry.retry.attempts=5"

      # 4️  SERVICE – port exposé par Apache dans l’image Nextcloud
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"

      # 5️  HEALTH-CHECK – Traefik teste / toutes les 20 s
      - "traefik.http.services.nextcloud.loadbalancer.healthcheck.path=/"
      - "traefik.http.services.nextcloud.loadbalancer.healthcheck.hostname=${NEXTCLOUD_DOMAIN}"
      - "traefik.http.services.nextcloud.loadbalancer.healthcheck.interval=20s"
      - "traefik.http.services.nextcloud.loadbalancer.healthcheck.timeout=5s"
    #ports:
    #  - 8080:80
    volumes:
      - "nextcloud:/var/www/html" # code + config (volume Docker)
      - "${NEXTCLOUD_DATA_PATH}:/var/www/html/data" # fichiers utilisateurs
    environment:
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_HOST=db

networks:
  traefik-frontend:
    external: true
  nextcloud-internal:
    internal: true

OpenProject

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/openproject:${TAG:-16-slim}
x-op-app: &app
  <<: [*image, *restart_policy]
  environment:
    OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}"
    OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}"
    OPENPROJECT_HSTS: "${OPENPROJECT_HSTS:-true}"
    RAILS_CACHE_STORE: "memcache"
    OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
    OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB}?pool=20&encoding=unicode&reconnect=true"
    RAILS_MIN_THREADS: ${RAILS_MIN_THREADS:-4}
    RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-16}
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    IMAP_ENABLED: "${IMAP_ENABLED:-false}"
  volumes:
    - "${OPDATA:-opdata}:/var/openproject/assets"

services:
  db:
    image: postgres:13
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "${PGDATA:-pgdata}:/var/lib/postgresql/data"
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-p4ssw0rd}
      POSTGRES_DB: openproject
    networks:
      - backend

  cache:
    image: memcached
    <<: *restart_policy
    networks:
      - backend

  proxy:
    build:
      context: ./proxy
      args:
        APP_HOST: web
    image: openproject/proxy
    <<: *restart_policy
    ports:
      - "${PORT:-8080}:80"
    depends_on:
      - web
    networks:
      - frontend

  web:
    <<: *app
    command: "./docker/prod/web"
    networks:
      - frontend
      - backend
    depends_on:
      - db
      - cache
      - seeder
    labels:
      - autoheal=true
    healthcheck:
      test:
        [
          "CMD",
          "curl",
          "-f",
          "http://localhost:8080${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}/health_checks/default",
        ]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s

  autoheal:
    image: willfarrell/autoheal:1.2.0
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    environment:
      AUTOHEAL_CONTAINER_LABEL: autoheal
      AUTOHEAL_START_PERIOD: 600
      AUTOHEAL_INTERVAL: 30

  worker:
    <<: *app
    command: "./docker/prod/worker"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  cron:
    <<: *app
    command: "./docker/prod/cron"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  seeder:
    <<: *app
    command: "./docker/prod/seeder"
    restart: on-failure
    networks:
      - backend

Openproject (Override)

services:
  proxy:
    profiles: ["disabled"] # Désactive le service proxy d'OpenProject

  web:
    networks:
      - traefik-frontend
      - backend
    labels:
      # Activation Traefik
      - "traefik.enable=true"
      # Route basée sur la variable .env
      - "traefik.http.routers.openproject.rule=Host(`${OPENPROJECT_HOST__NAME}`)"
      - "traefik.http.routers.openproject.entrypoints=web"
      - "traefik.http.routers.openproject.middlewares=retry"
      - "traefik.http.middlewares.retry.retry.attempts=5"
      # Backend → conteneur OpenProject expose le port 8080
      - "traefik.http.services.openproject.loadbalancer.server.port=8080"
      # Health-check Traefik
      - "traefik.http.services.openproject.loadbalancer.healthcheck.path=/"
      - "traefik.http.services.openproject.loadbalancer.healthcheck.hostname=${OPENPROJECT_HOST__NAME}"
      - "traefik.http.services.openproject.loadbalancer.healthcheck.interval=20s"
      - "traefik.http.services.openproject.loadbalancer.healthcheck.timeout=5s"

  db:
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_DB: ${POSTGRES_DB:-openproject}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-p4ssw0rd}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      start_period: 1m
      start_interval: 10s
      interval: 1m
      timeout: 5s
      retries: 3

networks:
  traefik-frontend:
    external: true
  backend:
    internal: true

DNS proxy

services:
  dps:
    image: defreitas/dns-proxy-server # identique au docker run
    hostname: "${DPS_HOSTNAME:-dns.mageddo}"
    container_name: "${CONTAINER_NAME:-dns}" # nom du conteneur
    restart: unless-stopped
    environment:
      - MG_REGISTER_CONTAINER_NAMES=${MG_REGISTER_CONTAINER_NAMES:-1} # par défaut, oui
    networks:
      traefik-frontend: # réseau Traefik pour exposer le service
        aliases:
          - "dns"
          - "${DPS_HOSTNAME:-dns.mageddo}"
    # Les deux montages repris tels quels du docker run original
    volumes:
      - "${DOCKER_SOCK}:/var/run/docker.sock" # observe les conteneurs
      - "${RESOLV_CONF}:/etc/resolv.conf" # relaie les DNS actuels

networks:
  traefik-frontend:
    external: true

Hi Bluepuma,

Do you have any idea ?

BR