A small preview or single-host production box can run two systemd colors and switch the Gateway port through an OpenResty named upstream. The frontend still uses deploy-frontend.sh and the current symlink. Environment files, seeds, and CORS stay in 1Panel Linux development preview. This page only covers the reverse-proxy wiring that usually fails and the release commands.
Why the 1Panel reverse-proxy site fails
The official OpenResty app (1Panel app store 1.31.x) is laid out like this:
Host ${WEBSITE_DIR}/conf.d/*.conf → container /usr/local/openresty/nginx/conf/conf.d/*.conf → included from http {} in nginx.conf
Host ${WEBSITE_DIR}/sites/<alias>/proxy/*.conf → included from the site server {} (locations / proxy_pass only)| Mistake | Result |
|---|---|
Write /opt/1panel/apps/openresty/openresty/conf/upstreams/*.conf | That directory is not included; port edits never switch traffic |
docker exec 1panel-openresty nginx -s reload | The real name is almost always 1Panel-openresty-<id> |
Paste upstream {} into custom config | upstream directive is not allowed here |
Reverse proxy target http://127.0.0.1:8082 | Traffic stays on blue; stopping blue yields 502 |
Bridge network + Kestrel bound to 127.0.0.1 | Container loopback is not the host; 502 |
Intended path:
Browser → OpenResty location proxy_pass http://bitzorcas_gateway_preview; ↑ ${WEBSITE_DIR}/conf.d/00-bitzorcas-preview-upstream.conf (http context; the script only changes the server port) ↓ 127.0.0.1:8082 blue Gateway or 127.0.0.1:9082 green Gateway ↓ same-color API 8080/9080 same-color JobHost 8081/9081Ports and directories
| Color | API | JobHost | Gateway | Directory |
|---|---|---|---|---|
| Blue | 8080 | 8081 | 8082 | /www/apps/<env>-blue |
| Green | 9080 | 9081 | 9082 | /www/apps/<env>-green |
| Current | — | — | — | symlink /www/apps/<env> |
Unit names are bitzorcas-{api,jobhost,gateway}-<env>-{blue,green}.
An existing deploy.sh directory at /www/apps/<env> is renamed to <env>-blue on the first blue-green run. If binaries live under /srv/sites/bitzorcas, export BITZORCAS_APP_ROOT.
Both JobHosts may run during the drain window. With a connection string, Quartz uses AdoJobStore plus clustering, so one job still runs once. Green Gateway downstream addresses are overridden by systemd Environment= to port 9080 so the 8080 keys in the env file cannot send green traffic back to blue API.
One-time wiring
Install the scripts and lib/common.sh under /www/scripts/:
# ① Workstation: upload the scripts and lib together or source will fail.scp scripts/deploy/deploy-blue-green.sh scripts/deploy/deploy-rollback.sh \ scripts/deploy/setup-1panel-openresty.sh deploy@server:/tmp/scp -r scripts/deploy/lib deploy@server:/tmp/deploy-lib
# ② Server: install the trio plus common.sh, then write the http-level upstream.sudo install -m 0755 /tmp/setup-1panel-openresty.sh /www/scripts/sudo install -m 0755 /tmp/deploy-blue-green.sh /www/scripts/sudo install -m 0755 /tmp/deploy-rollback.sh /www/scripts/sudo mkdir -p /www/scripts/libsudo install -m 0644 /tmp/deploy-lib/common.sh /www/scripts/lib/common.sh
# ③ Wiring only; systemd is left alone. Unwired locations fail with panel steps.sudo /www/scripts/setup-1panel-openresty.sh previewThe setup script detects the container, ${WEBSITE_DIR}, and the network mode, writes the http-level upstream, then runs nginx -t and reloads. If locations still pin 127.0.0.1:8082, it fails and prints the panel steps. That failure is the acceptance check.
Complete copy-paste configuration
Prefer setup-1panel-openresty.sh: it writes the block below and then reloads. You can write the files by hand when the script is missing, but an upstream {} block is legal only in an http-level file. Do not paste it into site custom config.
http-level upstream
write_openresty_upstream in scripts/deploy/lib/common.sh writes this exact file for preview on blue port 8082:
| Condition | Host path |
|---|---|
| Default | ${WEBSITE_DIR}/conf.d/00-bitzorcas-preview-upstream.conf |
Site alias set and sites/<alias>/upstream/ exists | ${WEBSITE_DIR}/sites/<alias>/upstream/bitzorcas_gateway_preview.conf |
${WEBSITE_DIR} is the host path mounted as /www in the OpenResty container. On 1Panel it is often /opt/1panel/www. Do not write to /opt/1panel/apps/openresty/openresty/conf/upstreams/.
# Managed by BitzOrcas blue-green. This file must stay in http context.# Do not paste the upstream block into a 1Panel website server / 自定义配置.upstream bitzorcas_gateway_preview { server 127.0.0.1:8082; keepalive 32;}When the blue-green script switches to green it changes only the port. The name stays the same:
upstream bitzorcas_gateway_preview { server 127.0.0.1:9082; keepalive 32;}If the environment is not preview, rename the file to 00-bitzorcas-<env>-upstream.conf and the block to bitzorcas_gateway_<env>, and keep proxy_pass in the locations in sync.
If the file lives under sites/<alias>/upstream/, the site main file ${WEBSITE_DIR}/conf.d/<alias>.conf must start with an http-level include /www/sites/<alias>/upstream/*.conf; (container path). The setup script adds this line. Do not put that include inside server {}.
Without the setup script, write the blue block to the default path:
# Write the preview blue upstream in the http-level conf.d file, never in site custom config.# Inspect the /www mount first, then change WEBSITE_DIR. On 1Panel it is often /opt/1panel/www.OPENRESTY="$(sudo docker ps --format '{{.Names}}' | grep -Ei openresty | head -n1)"sudo docker inspect "$OPENRESTY" --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'WEBSITE_DIR=/opt/1panel/wwwsudo mkdir -p "${WEBSITE_DIR}/conf.d"sudo tee "${WEBSITE_DIR}/conf.d/00-bitzorcas-preview-upstream.conf" >/dev/null <<'EOF'# Managed by BitzOrcas blue-green. This file must stay in http context.# Do not paste the upstream block into a 1Panel website server / 自定义配置.upstream bitzorcas_gateway_preview { server 127.0.0.1:8082; keepalive 32;}EOFsudo docker exec "$OPENRESTY" nginx -tsudo docker exec "$OPENRESTY" nginx -s reloadDo not edit the port by hand on a normal cutover; let deploy-blue-green.sh / deploy-rollback.sh do it.
include chain
The official image already includes conf.d from http . Do not edit the OpenResty app nginx.conf to add a custom upstreams/ directory; a panel upgrade or container recreate drops that change.
http { # conf.d is the host ${WEBSITE_DIR}/conf.d mount, not conf/upstreams/ under the app dir. include /usr/local/openresty/nginx/conf/conf.d/*.conf;}Every *.conf under host conf.d is therefore in the http context: it may hold an upstream {} or a full server {}. The 1Panel site main file lives here too.
Host ${WEBSITE_DIR}/conf.d/*.conf → container /usr/local/openresty/nginx/conf/conf.d/*.conf → included from http {} in nginx.conf ├─ 00-bitzorcas-preview-upstream.conf http: upstream {} only └─ <site-alias>.conf http: optional upstream include, then server {}
Host ${WEBSITE_DIR}/sites/<alias>/upstream/*.conf → container /www/sites/<alias>/upstream/*.conf → must be included from the site main file before server {} (http) → setup uses this path when BITZORCAS_OPENRESTY_SITE is set and the directory exists
Host ${WEBSITE_DIR}/sites/<alias>/proxy/*.conf → container /www/sites/<alias>/proxy/*.conf → included only from server {} (locations / proxy_pass)Prefer conf.d/00-bitzorcas-preview-upstream.conf. The site main file does not need another include. Add one line at the top of the site file only when the upstream lives under sites/<alias>/upstream/. The next block is a 1Panel-style include example. Keep panel-generated include lines; do not delete rewrite, ssl, waf, or similar files that the panel already created.
# Host: ${WEBSITE_DIR}/conf.d/app.example.com.conf# Loaded from http {} in nginx.conf. The upstream include must stay above server {}.# Skip this line when conf.d/00-bitzorcas-preview-upstream.conf already defines the block.include /www/sites/app.example.com/upstream/*.conf;
server { listen 80; listen 443 ssl http2; server_name app.example.com;
# Keep the certificate paths from the 1Panel site config file. ssl_certificate /www/sites/app.example.com/ssl/fullchain.pem; ssl_certificate_key /www/sites/app.example.com/ssl/privkey.pem;
root /www/sites/bitzorcas-preview/current; index index.html;
access_log /www/sites/app.example.com/log/access.log; error_log /www/sites/app.example.com/log/error.log;
# These includes are inside server {}. Locations and headers only; never upstream {}. include /www/sites/app.example.com/proxy/*.conf; include /www/sites/app.example.com/rewrite/*.conf; include /www/sites/app.example.com/ssl/*.conf; include /www/sites/app.example.com/redirect/*.conf; include /www/sites/app.example.com/auth/*.conf; include /www/sites/app.example.com/error_page/*.conf; include /www/sites/app.example.com/waf/*.conf;}When the panel reverse-proxy UI owns the backends, each path becomes a file under sites/<alias>/proxy/*.conf. After you change the target to http://bitzorcas_gateway_preview, those files must say proxy_pass http://bitzorcas_gateway_preview; and must not pin 127.0.0.1:8082. If you do not use the panel reverse-proxy UI, put the full server {} from the next section into the site main file, or paste the location-only block into custom config.
Full server (site main file)
The next two files are complete site main files you can compare or paste. They do not contain upstream {}; that stays in 00-bitzorcas-preview-upstream.conf. Change root, server_name, and certificate paths to the values the container can see. Do not listen 8081 (JobHost).
Domain plus HTTPS:
# Host: ${WEBSITE_DIR}/conf.d/app.example.com.conf# This file is in the http context but should only declare server {}. No upstream {} here.
server { listen 80; listen 443 ssl http2; server_name app.example.com;
ssl_certificate /www/sites/app.example.com/ssl/fullchain.pem; ssl_certificate_key /www/sites/app.example.com/ssl/privkey.pem;
root /www/sites/bitzorcas-preview/current; index index.html;
access_log /www/sites/app.example.com/log/access.log; error_log /www/sites/app.example.com/log/error.log;
if ($scheme = http) { return 301 https://$host$request_uri; }
location ^~ /.well-known/acme-challenge { allow all; root /usr/share/nginx/html; }
location /api/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /hubs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; proxy_read_timeout 3600s; proxy_buffering off; }
location /health/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /openapi/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /scalar/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /docs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location / { try_files $uri $uri/ /index.html =404; }}IP-only (common home-broadband entry on 8088):
# Host: ${WEBSITE_DIR}/conf.d/<PUBLIC_IP>.conf# Listen on 8088 when public port 80 is taken. Do not listen on 8081.# CORS and Frontend__BaseUrl must include the port, for example http://10.10.10.10:8088.
server { listen 8088; server_name <PUBLIC_IP>;
root /www/sites/bitzorcas-preview/current; index index.html;
access_log /www/sites/bitzorcas-preview/log/access.log; error_log /www/sites/bitzorcas-preview/log/error.log;
location /api/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /hubs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; proxy_read_timeout 3600s; proxy_buffering off; }
location /health/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /openapi/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /scalar/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location /docs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; }
location / { try_files $uri $uri/ /index.html =404; }}server-level locations
When you only edit 1Panel custom config and do not replace the site main file, paste the next block. It matches deploy/1panel/openresty-app.locations-blue-green.conf. The http-level upstream must already exist. Change root to the frontend current path visible inside the container.
# Paste into 1Panel site custom config or the site server {} block.# Write the http-level upstream first. Rename bitzorcas_gateway_<env> when not preview.root /www/sites/bitzorcas-preview/current;index index.html;
location /api/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;}
location /hubs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; proxy_read_timeout 3600s; proxy_buffering off;}
location /health/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;}
location /openapi/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;}
location /scalar/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;}
location /docs/ { proxy_pass http://bitzorcas_gateway_preview; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;}
location / { try_files $uri $uri/ /index.html =404;}Panel steps
- Create a static site. Point
rootat the frontendcurrentpath inside the container. - Do not create a reverse-proxy site whose only backend is port
8082. - Either change every reverse-proxy target to
http://bitzorcas_gateway_preview(no IP, no port), paste the fullserver {}into the site main file, or paste the location-only block into custom config. Do not pasteupstream {}with it. /api/,/hubs/,/health/,/openapi/,/scalar/, and/docs/must all use the named upstream.- Forward
$http_hostandX-Forwarded-Port $server_port.$hostalone drops a non-default port such as8088and login origin checks return 403. - OpenResty must use host networking. If
docker inspect <container> --format '{{.HostConfig.NetworkMode}}'is nothost, change it in the app editor and recreate the container.
Run setup again until it prints that OpenResty is wired.
Release and rollback
# ① Pack the same way as an in-place release; incremental packs still need the server manifest.VERSION="$(date +%Y%m%d-%H%M)"./scripts/deploy/pack.sh preview "$VERSION"scp ../publish/<branch>/bitzorcas-preview-${VERSION}.zip deploy@server:/www/releases/
# ② Install on the idle color, then rewrite the upstream only after health checks pass.sudo BITZORCAS_DEPLOY_DB_MODE=schema-only \ /www/scripts/deploy-blue-green.sh \ /www/releases/bitzorcas-preview-${VERSION}.zip \ preview
# ③ Confirm the live color. Rollback switches processes only, not the database.sudo /www/scripts/deploy-blue-green.sh --status previewsudo /www/scripts/deploy-rollback.sh previewA failed health check or an unwired OpenResty leaves the upstream untouched. Only one previous color is kept. A destructive schema change cannot be undone by switching colors.
Optionally set BITZORCAS_PUBLIC_HEALTH_URL=https://app.example.com/health/ready so reload is followed by a public probe.
Acceptance
# ① Inspect the live color, then probe the local Gateway and the public entry.sudo /www/scripts/deploy-blue-green.sh --status previewcurl -fsS http://127.0.0.1:8082/health/readycurl -fsS <PUBLIC_BASE>/health/ready
# ② nginx -T must show both the upstream block and the named proxy_pass.sudo docker exec "$(sudo docker ps --format '{{.Names}}' | grep -Ei openresty)" nginx -T \ | grep -E 'upstream bitzorcas_gateway_|proxy_pass http://bitzorcas_gateway_'nginx -T must show both the upstream block and proxy_pass http://bitzorcas_gateway_<env>.
Failures
| Symptom | Fix |
|---|---|
upstream directive is not allowed here | Remove upstream {} from site custom config; let setup write conf.d/00-*.conf |
| Setup says locations do not use the name | Reverse proxy still points at 127.0.0.1:8082 |
Container 1panel-openresty not found | docker ps | grep -i openresty, or set BITZORCAS_OPENRESTY_CONTAINER |
| 502 after switching to green | Traffic was switched while locations were still pinned; rollback, then fix locations |
| Green Gateway still calls 8080 | Use the current deploy-blue-green.sh (units override ports with Environment=) |
| 502 on a bridge network | Switch OpenResty to host networking |
Frontend rewrite cycle /index.html | The container cannot see current; mount the site directory as in the preview guide |