diff --git a/dockerfiles/docker-compose-search.yml b/dockerfiles/docker-compose-search.yml index 68810d9..9523027 100644 --- a/dockerfiles/docker-compose-search.yml +++ b/dockerfiles/docker-compose-search.yml @@ -27,8 +27,6 @@ services: # Disable HTTPS on development. - xpack.security.transport.ssl.enabled=false - 'ES_JAVA_OPTS=-Xms128m -Xmx128m' - ports: - - "9200:9200" links: - web - celery diff --git a/dockerfiles/docker-compose.yml b/dockerfiles/docker-compose.yml index 0f603c8..5752b85 100644 --- a/dockerfiles/docker-compose.yml +++ b/dockerfiles/docker-compose.yml @@ -20,7 +20,7 @@ services: stop_grace_period: 1s image: nginx:1.22 ports: - - "80:80" + - "${RTDDEV_PORT_NGINX:-80}:80" links: - web - proxito @@ -30,6 +30,12 @@ services: readthedocs: volumes: - ${PWD}/dockerfiles/nginx:/etc/nginx/templates + # Each repository symlinks the templates in this directory from its own templates, + # mounted at the path the relative symlink resolves to inside the container. + - ${PWD}/common/dockerfiles/nginx:/etc/common/dockerfiles/nginx:ro + # Required on Linux by `side-by-side.conf.template`; Docker Desktop resolves it already. + extra_hosts: + - "host.docker.internal:host-gateway" # Disable logs for NGINX by default because they are too noisy and we have # better logs in our application code logging: @@ -69,8 +75,6 @@ services: - ${PWD}/${RTDDEV_PATH_ADDONS:-../addons}/webpack.config.js:/usr/src/app/checkouts/addons/webpack.config.js tty: true working_dir: /usr/src/app/checkouts/addons/ - ports: - - "8000:8000" command: [ "npm", "run", @@ -186,8 +190,6 @@ services: context: ${PWD}/${RTDDEV_PATH_EXT_THEME:-../ext-theme} dockerfile: ${PWD}/common/dockerfiles/Dockerfile.webpack working_dir: /usr/src/app/checkouts/ext-theme - ports: - - "10001:10001" volumes: - ${PWD}/${RTDDEV_PATH_EXT_THEME:-../ext-theme}/src:/usr/src/app/checkouts/ext-theme/src - ${PWD}/${RTDDEV_PATH_EXT_THEME:-../ext-theme}/readthedocsext:/usr/src/app/checkouts/ext-theme/readthedocsext @@ -445,9 +447,6 @@ services: storage: image: rustfs/rustfs:1.0.0-alpha.83 - ports: - - "9000:9000" - - "9001:9001" environment: - RUSTFS_ADDRESS=:9000 - RUSTFS_ACCESS_KEY=admin diff --git a/dockerfiles/nginx/services.conf.template b/dockerfiles/nginx/services.conf.template new file mode 100644 index 0000000..6c60867 --- /dev/null +++ b/dockerfiles/nginx/services.conf.template @@ -0,0 +1,97 @@ +# Development services, served by hostname instead of their own host ports, +# so both environments can run side by side (see `side-by-side.conf.template`). +# +# Upstreams are resolved per request (`resolver` + variable), +# since these containers may start after NGINX, or not at all (`--no-search`). + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# ext-theme webpack development server, including its live reload websocket. +server { + listen 80; + server_name assets.${NGINX_WEB_SERVER_NAME}; + access_log off; + resolver 127.0.0.11 valid=10s; + + location / { + set $upstream http://webpack:10001; + proxy_pass $upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + } +} + +# Addons development server. +server { + listen 80; + server_name addons.${NGINX_WEB_SERVER_NAME}; + access_log off; + resolver 127.0.0.11 valid=10s; + + location / { + set $upstream http://addons:8000; + proxy_pass $upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + } +} + +# S3 API, used by presigned upload URLs. +server { + listen 80; + server_name storage.${NGINX_WEB_SERVER_NAME}; + access_log off; + resolver 127.0.0.11 valid=10s; + client_max_body_size 0; + proxy_request_buffering off; + + location / { + set $upstream http://storage:9000; + proxy_pass $upstream; + # S3 clients sign the Host header. + proxy_set_header Host $http_host; + } +} + +# S3 web console. +server { + listen 80; + server_name storage-console.${NGINX_WEB_SERVER_NAME}; + access_log off; + resolver 127.0.0.11 valid=10s; + + location = / { + return 302 /rustfs/console/; + } + + # The console calls the S3 and admin APIs on its own origin, signing the Host header. + location / { + set $upstream http://storage:9001; + proxy_pass $upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + } +} + +# Elasticsearch. +server { + listen 80; + server_name search.${NGINX_WEB_SERVER_NAME}; + access_log off; + resolver 127.0.0.11 valid=10s; + + location / { + set $upstream http://search:9200; + proxy_pass $upstream; + proxy_set_header Host $http_host; + } +} diff --git a/dockerfiles/nginx/side-by-side.conf.template b/dockerfiles/nginx/side-by-side.conf.template new file mode 100644 index 0000000..de5aaec --- /dev/null +++ b/dockerfiles/nginx/side-by-side.conf.template @@ -0,0 +1,21 @@ +# Forward the other environment's domains (community <-> business) to its NGINX, +# so both can run side by side: whichever one owns port 80 forwards to the other, +# which publishes its NGINX on another host port (`RTDDEV_PORT_NGINX`). +# Returns a 502 for those domains when the other environment isn't running. +server { + listen 80; + server_name ${NGINX_SIDE_BY_SIDE_SERVER_NAME}; + access_log off; + client_max_body_size 0; + + location / { + proxy_pass http://host.docker.internal:${NGINX_SIDE_BY_SIDE_PORT}; + proxy_set_header Host $http_host; + # Websockets, like webpack's live reload. + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 300s; + } +} diff --git a/dockerfiles/tasks.py b/dockerfiles/tasks.py index a94f93a..194bfd3 100644 --- a/dockerfiles/tasks.py +++ b/dockerfiles/tasks.py @@ -11,6 +11,48 @@ DOCKER_COMPOSE_OVERRIDE = "docker-compose.override.yml" DOCKER_COMPOSE_COMMAND = f"docker compose --project-directory=. -f {DOCKER_COMPOSE} -f {DOCKER_COMPOSE_OVERRIDE} -f {DOCKER_COMPOSE_SEARCH}" +# Host port for NGINX when the other environment already owns port 80. +# The other environment forwards our domains here (see `nginx/side-by-side.conf.template`). +NGINX_SIDE_BY_SIDE_PORTS = { + "community": "10080", + "commercial": "10081", +} + + +def compose_project_name(): + if os.environ.get("COMPOSE_PROJECT_NAME"): + return os.environ["COMPOSE_PROJECT_NAME"] + with open(".env") as f: + for line in f: + name, _, value = line.strip().partition("=") + if name == "COMPOSE_PROJECT_NAME": + return value + return None + + +def nginx_port(c): + """ + Host port for NGINX: 80, unless another container already publishes it. + + Run community and business at the same time without any configuration: + the second one to start gets its side-by-side port. + ``RTDDEV_PORT_NGINX`` overrides it. + """ + if os.environ.get("RTDDEV_PORT_NGINX"): + return os.environ["RTDDEV_PORT_NGINX"] + + project = compose_project_name() + side_by_side_port = NGINX_SIDE_BY_SIDE_PORTS.get(project) + if not side_by_side_port: + return "80" + + result = c.run("docker ps --filter publish=80 --format '{{.Names}}'", hide=True, warn=True) + for owner in result.stdout.split(): + if owner != f"{project}-nginx-1": + print(f"Port 80 is used by {owner}, publishing NGINX on port {side_by_side_port}.") + return side_by_side_port + return "80" + @task( help={ @@ -50,7 +92,7 @@ def build(c, cache=False): ) def compose(c, command): """Pass the command to docker compose directly.""" - c.run(f"{DOCKER_COMPOSE_COMMAND} {command}", pty=True) + c.run(f"RTDDEV_PORT_NGINX={nginx_port(c)} {DOCKER_COMPOSE_COMMAND} {command}", pty=True) @task( @@ -99,6 +141,7 @@ def up( cmd.append("INIT=t" if init else "INIT=") cmd.append("DOCKER_NO_RELOAD=t" if not reload else "DOCKER_NO_RELOAD=") cmd.append(f"RTD_LOGGING_LEVEL={log_level}") + cmd.append(f"RTDDEV_PORT_NGINX={nginx_port(c)}") cmd.append("docker compose") cmd.append("--project-directory=.")