Skip to content
bitzorcas
中EN

Guide

Single-host blue-green on 1Panel OpenResty

Switch a single-host BitzOrcas pair through a named OpenResty upstream so 1Panel reverse-proxy sites do not pin port 8082 or reject upstream blocks.

Last updated

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)
MistakeResult
Write /opt/1panel/apps/openresty/openresty/conf/upstreams/*.confThat directory is not included; port edits never switch traffic
docker exec 1panel-openresty nginx -s reloadThe real name is almost always 1Panel-openresty-<id>
Paste upstream {} into custom configupstream directive is not allowed here
Reverse proxy target http://127.0.0.1:8082Traffic stays on blue; stopping blue yields 502
Bridge network + Kestrel bound to 127.0.0.1Container 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/9081

Ports and directories

ColorAPIJobHostGatewayDirectory
Blue808080818082/www/apps/<env>-blue
Green908090819082/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/:

Terminal window
# ① 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/lib
sudo 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 preview

The 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:

ConditionHost 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/.

00-bitzorcas-preview-upstream.conf
# 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:

green changes only the server port
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:

Terminal window
# 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/www
sudo 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;
}
EOF
sudo docker exec "$OPENRESTY" nginx -t
sudo docker exec "$OPENRESTY" nginx -s reload

Do 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 fragment in nginx.conf (shipped by the image; do not edit)
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.

<site-alias>.conf (include style)
# 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:

app.example.com.conf
# 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):

<PUBLIC_IP>.conf
# 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.

openresty-app.locations-blue-green.conf
# 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

  1. Create a static site. Point root at the frontend current path inside the container.
  2. Do not create a reverse-proxy site whose only backend is port 8082.
  3. Either change every reverse-proxy target to http://bitzorcas_gateway_preview (no IP, no port), paste the full server {} into the site main file, or paste the location-only block into custom config. Do not paste upstream {} with it.
  4. /api/, /hubs/, /health/, /openapi/, /scalar/, and /docs/ must all use the named upstream.
  5. Forward $http_host and X-Forwarded-Port $server_port. $host alone drops a non-default port such as 8088 and login origin checks return 403.
  6. OpenResty must use host networking. If docker inspect <container> --format '{{.HostConfig.NetworkMode}}' is not host, change it in the app editor and recreate the container.

Run setup again until it prints that OpenResty is wired.

Release and rollback

Terminal window
# ① 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 preview
sudo /www/scripts/deploy-rollback.sh preview

A 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

Terminal window
# ① Inspect the live color, then probe the local Gateway and the public entry.
sudo /www/scripts/deploy-blue-green.sh --status preview
curl -fsS http://127.0.0.1:8082/health/ready
curl -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

SymptomFix
upstream directive is not allowed hereRemove upstream {} from site custom config; let setup write conf.d/00-*.conf
Setup says locations do not use the nameReverse proxy still points at 127.0.0.1:8082
Container 1panel-openresty not founddocker ps | grep -i openresty, or set BITZORCAS_OPENRESTY_CONTAINER
502 after switching to greenTraffic was switched while locations were still pinned; rollback, then fix locations
Green Gateway still calls 8080Use the current deploy-blue-green.sh (units override ports with Environment=)
502 on a bridge networkSwitch OpenResty to host networking
Frontend rewrite cycle /index.htmlThe container cannot see current; mount the site directory as in the preview guide

See also

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%