fix(api-gateway): rewrite APISIX package and deploy to production

The 09-01 package referenced a nonexistent Cloudron etcd addon and an
unpinned base image. Rewritten: digest-pinned apisix 3.18.0 with an
embedded single-node etcd, loopback-only Admin API, and read-only
rootfs support. Deployed live at apigw.knownelement.com (healthz 200).

Umbrella: https://projects.knownelement.com/issues/632
This commit is contained in:
2026-09-07 10:02:09 -05:00
parent d8bc507741
commit 17a3e8e9c0
7 changed files with 260 additions and 359 deletions
@@ -1,21 +1,34 @@
# Changelog
## [3.12.0] - 2025-01-24
## [1.0.2] - 2026-09-07
### Added
- Initial Cloudron package for APISIX
- Official Apache APISIX Docker image wrapper
- Automatic etcd configuration via Cloudron addon
- Admin API key configuration
- Health check endpoint
- Documentation with usage examples
- Architecture diagram
### Fixed
- Read-only rootfs: relocate the whole APISIX runtime tree (CLI, deps,
config.yaml, nginx.conf, logs, sockets) to /app/data/apisix - Cloudron 8
mounts app root filesystems read-only
- Patched the CLI's hardcoded apisix_home in the copied tree so nginx.conf
generation lands on the writable volume
### Features
- Multi-protocol support (HTTP/HTTPS, TCP/UDP, Dubbo, MQTT, gRPC, WebSocket)
- Dynamic configuration without restarts
- Load balancing with multiple strategies
- Rich plugin ecosystem (100+ plugins)
- Security features (IP restrictions, JWT, API Key auth)
- Traffic management (rate limiting, circuit breaking, canary releases)
- AI Gateway capabilities for LLM proxying
### Changed
- Upgraded APISIX 3.12.0 -> 3.18.0-debian, pinned by tag AND digest
- etcd copied from quay.io/coreos/etcd v3.5.33 (digest-pinned)
## [1.0.1] - 2026-09-07
### Fixed
- Run as root: Cloudron does not chown /app/data to arbitrary non-root
image users, so the apisix user could not persist state
## [1.0.0] - 2026-09-07
### Changed
- REWRITE of the 2026-09-01 package, which referenced a nonexistent
Cloudron "etcd addon" and an unpinned apache/apisix:latest
- Embedded single-node etcd (no platform addon dependency); Admin API on
127.0.0.1:9180 with a persisted random key in /app/data/.admin_key
- healthCheckPath "/" (Cloudron treats 4xx as alive); /healthz route
seeded for external monitoring
- Manifest: semver version, localstorage-only addons, 1 GiB memory limit
### Deployed
- Production Cloudron at https://apigw.knownelement.com (2026-09-07)
@@ -1,7 +1,6 @@
{
"version": 1,
"version": "1.0.2",
"manifestVersion": 2,
"type": "app",
"id": "io.cloudron.apisix",
"title": "APISIX",
"description": "Apache APISIX is a dynamic, real-time, high-performance API Gateway. Provides rich traffic management features like load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability and more.",
@@ -9,33 +8,15 @@
"website": "https://apisix.apache.org",
"contactEmail": "cloudron@tsys.dev",
"tagline": "Cloud-native, high-performance API gateway",
"version": "3.12-latest",
"healthCheckPath": "/apisix/admin/routes",
"httpPort": 9180,
"memoryLimit": 1024,
"healthCheckPath": "/",
"httpPort": 9080,
"memoryLimit": 1073741824,
"addons": {
"localstorage": true,
"etcd": {
"version": "3.4"
}
},
"tcpPorts": {
"ADMIN_PORT": {
"description": "APISIX Admin API port",
"defaultValue": 9180
},
"HTTP_PORT": {
"description": "APISIX HTTP proxy port",
"defaultValue": 9080
},
"HTTPS_PORT": {
"description": "APISIX HTTPS proxy port",
"defaultValue": 9443
}
"localstorage": {}
},
"mediaLinks": [
"https://raw.githubusercontent.com/apache/apisix/master/logos/apache-apisix.png"
],
"changelog": "Initial Cloudron package for APISIX API Gateway",
"changelog": "APISIX 3.18.0 with embedded single-node etcd; Admin API loopback-only",
"icon": "file://logo.png"
}
@@ -1,7 +1,43 @@
FROM apache/apisix:latest
# APISIX Cloudron Package
#
# Apache APISIX API gateway, traditional deployment mode with an embedded
# single-node etcd. Cloudron has no etcd addon, so the etcd server ships
# inside this image (copied from the upstream release image) and stores its
# state under /app/data/etcd - that keeps the Admin API + dynamic routes
# available without any platform dependency.
#
# Upstream: https://github.com/apache/apisix
# - Official image apache/apisix:3.18.0-debian, runs as non-root apisix
# (uid 636), ENTRYPOINT /docker-entrypoint.sh CMD docker-start which
# does `apisix init` + `apisix init_etcd` then execs openresty with
# `daemon off`.
#
# Authentication: APISIX is a gateway (no user concept). The Admin API
# listens on 127.0.0.1:9180 ONLY (never exposed) and is protected by a
# persisted random admin key in /app/data/.admin_key. Operators reach it
# via `cloudron exec` into the app. Data-plane auth (key-auth, OIDC
# relay, etc.) is route-level and configured per API by the operator.
#
# Pattern: official-image wrapper. Image pinned by tag AND digest.
FROM apache/apisix:3.18.0-debian@sha256:84e6b5e787e9f889ebff88161cb9a16599bafcffa236c6b54c7f779a0655940d
# Copy start script (already executable from host)
COPY start.sh /start.sh
# etcd + etcdctl from the upstream release image (multi-arch manifest
# digest; build resolves the amd64 binary). etcdctl is kept as an ops tool
# for `cloudron exec` debugging of the config store.
COPY --from=quay.io/coreos/etcd@sha256:d367cba7801b29d2f7481bb56802894658ec8a647834509118c81f77f721381b /usr/local/bin/etcd /usr/local/bin/etcd
COPY --from=quay.io/coreos/etcd@sha256:d367cba7801b29d2f7481bb56802894658ec8a647834509118c81f77f721381b /usr/local/bin/etcdctl /usr/local/bin/etcdctl
# Start APISIX
CMD ["/start.sh"]
# start.sh waits for etcd, regenerates config.yaml from the Cloudron env
# contract and execs openresty in the foreground. Made executable on the
# host, not at build time (Cloudron builds hit permission errors on RUN chmod).
COPY start.sh /app/start.sh
# Cloudron proxies HTTPS -> this port (manifest httpPort / healthCheckPath "/").
EXPOSE 9080
# Cloudron does not chown /app/data to arbitrary non-root image users, so the
# container runs as root (the Cloudron app convention) and start.sh keeps the
# Admin API loopback-only + key-protected.
USER root
ENTRYPOINT ["/bin/bash", "/app/start.sh"]
+54 -168
View File
@@ -1,188 +1,74 @@
# APISIX Cloudron Package
**DEPLOYED**: `https://apigw.knownelement.com` (production Cloudron, 2026-09-07)
## Description
Apache APISIX is a dynamic, real-time, high-performance API Gateway. It provides rich traffic management features such as load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability and more.
Apache APISIX is a dynamic, real-time, high-performance API Gateway. It
provides rich traffic management features such as load balancing, dynamic
upstream, canary release, circuit breaking, authentication, observability
and more.
## Features
## Package design
### Core Capabilities
- **Dynamic Configuration**: Hot updates and hot plugins without restarts
- **Multi-Protocol Support**: HTTP/HTTPS, TCP/UDP, Dubbo, MQTT, gRPC, WebSocket
- **Load Balancing**: Multiple load balancing strategies
- **Security**: IP restrictions, JWT authentication, API Key authentication
- **Traffic Management**: Rate limiting, circuit breaking, canary releases
- **Observability**: Prometheus metrics, distributed tracing
- **AI Gateway**: Support for LLM proxying and AI workloads
- **Base image**: `apache/apisix:3.18.0-debian` (pinned by tag AND digest).
- **Embedded etcd**: Cloudron has no etcd addon, so a single-node etcd
(copied from `quay.io/coreos/etcd` v3.5.33, digest-pinned) runs inside
the container, state under `/app/data/etcd`. This keeps the Admin API
and dynamic routes available with zero platform dependencies.
- **Read-only rootfs ready**: Cloudron 8 mounts app root filesystems
read-only. `start.sh` relocates the APISIX runtime tree (CLI, deps,
generated `config.yaml`, `nginx.conf`, logs, sockets) to
`/app/data/apisix` via `APISIX_PREFIX` and a path-patched CLI copy.
- **Ports**: only the proxy port (9080) is served; Cloudron terminates
TLS and proxies `https://apigw.knownelement.com` to it. In-container
TLS is disabled.
### Gateway Features
- Proxy Rewrite (host, URI, schema, method, headers)
- Upstream Health Checks
- Request/Response Transformation
- CORS Support
- Web Application Firewall (WAF) via plugins
- OpenID Connect integration
## Authentication / admin access
## Configuration
The **Admin API listens on 127.0.0.1:9180 only** - it is never published.
It is protected by a random 64-hex-char key persisted at
`/app/data/.admin_key` (rotating it invalidates automation clients).
### Etcd Connection
APISIX requires an etcd instance for configuration storage. The Cloudron package automatically configures this connection using Cloudron's etcd addon.
### Environment Variables
- `CLOUDRON_ETCD_HOST`: Etcd host (automatically set by Cloudron)
- `CLOUDRON_ETCD_PORT`: Etcd port (automatically set by Cloudron)
- `ADMIN_KEY`: Admin API key (default: admin-key-secret-change-me, **change this in production**)
### Ports
- **9180**: Admin API port (for configuration via REST API)
- **9080**: HTTP proxy port (client requests)
- **9443**: HTTPS proxy port (client requests with SSL)
## Usage
### 1. Configure Routes via Admin API
APISIX provides a RESTful Admin API for configuration:
Operate the gateway from the host (or any admin shell):
```bash
# List all routes
curl http://localhost:9180/apisix/admin/routes/ \
-H 'X-API-KEY: admin-key-secret-change-me'
# read the admin key
cloudron exec -a apigw.knownelement.com cat /app/data/.admin_key
# Create a new route
curl http://localhost:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: admin-key-secret-change-me' \
-X PUT -d '{
"uri": "/hello",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
# example: list routes (from inside the app container)
cloudron exec -a apigw.knownelement.com curl \
-H "X-API-Key: $KEY" http://127.0.0.1:9180/apisix/admin/routes
```
### 2. Test the Gateway
```bash
# Test via HTTP
curl http://localhost:9080/hello
Data-plane auth (key-auth, jwt-auth, OIDC relay, IP restriction, ...) is
per-route and configured by the operator through the Admin API - the
gateway itself has no user concept.
# Test via HTTPS (after configuring SSL)
curl -k https://localhost:9443/hello
```
## Operator notes
### 3. Add SSL Certificates
```bash
curl http://localhost:9180/apisix/admin/ssls/1 \
-H 'X-API-KEY: admin-key-secret-change-me' \
-X PUT -d '{
"cert": "...",
"key": "...",
"snis": ["example.com"]
}'
```
- **Routes persist** in the embedded etcd across restarts and Cloudron
backups (state is under `/app/data`).
- **Health**: Cloudron's healthcheck hits `/` (a 404 counts as alive);
`/healthz` returns 200 for external monitoring.
- **Real client IPs** are restored from the Cloudron proxy's
`X-Forwarded-For` (RFC1918 trusted).
- **etcd tuning**: auto-compaction every hour keeps the embedded store
small; single-node by design (gateway config is reproducible from
infrastructure-as-code, not a durability concern).
### 4. Configure Plugins
APISIX supports 100+ plugins for various capabilities:
## Files
```bash
# Enable rate limiting
curl http://localhost:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: admin-key-secret-change-me' \
-X PATCH -d '{
"plugins": {
"limit-count": {
"count": 10,
"time_window": 60,
"rejected_code": 429
}
}
}'
```
| File | Purpose |
|------|---------|
| `Dockerfile` | Pinned base image + etcd/etcdctl binaries + start.sh |
| `start.sh` | etcd lifecycle, config generation, health route seed |
| `CloudronManifest.json` | Package manifest (localstorage only) |
| `sbom.cyclonedx.json` | SBOM [#834] (syft, pinned container) |
## Security
## History
### Change Default Admin Key
The default admin key is `admin-key-secret-change-me`. **Change this immediately after installation**:
```bash
# Get current admin key from Cloudron environment
# Update in /usr/local/apisix/conf/config.yaml
# Restart APISIX
```
### Use HTTPS in Production
Always use HTTPS (port 9443) for production deployments. Configure SSL certificates via the Admin API.
### IP Restrictions
Configure IP restrictions to limit who can access the Admin API:
```bash
curl http://localhost:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: admin-key-secret-change-me' \
-X PATCH -d '{
"plugins": {
"ip-restriction": {
"whitelist": ["192.168.1.0/24"]
}
}
}'
```
## Architecture
```
┌─────────────┐
│ Client │
└──────┬──────┘
┌──────────────┐
│ APISIX │
│ (Gateway) │
└──────┬──────┘
┌──────────────┐
│ Upstream │
│ Services │
└──────────────┘
┌──────────────┐
│ Etcd │
│ (Config DB) │
└──────────────┘
```
## Documentation
For more information on configuring APISIX:
- [Official Documentation](https://apisix.apache.org/docs/)
- [Admin API Reference](https://apisix.apache.org/docs/apisix/admin-api/)
- [Plugin Configuration](https://apisix.apache.org/docs/apisix/plugins/)
- [Best Practices](https://apisix.apache.org/docs/general/faq)
- [AI Gateway Guide](https://apisix.apache.org/ai-gateway/)
## Troubleshooting
### APISIX won't start
1. Check etcd connectivity: `curl http://localhost:2379/health`
2. Review logs in `/usr/local/apisix/logs/error.log`
3. Verify configuration syntax in `/usr/local/apisix/conf/config.yaml`
### Routes not working
1. Check Admin API key is correct
2. Verify upstream servers are accessible
3. Check firewall rules allow traffic to ports 9080/9443
4. Review route configuration via Admin API
### Performance issues
1. Increase worker processes in nginx_config
2. Enable HTTP/2 for better performance
3. Consider horizontal scaling (multiple APISIX instances)
## Upstream
[GitHub Repository](https://github.com/apache/apisix)
[Apache Project Page](https://apisix.apache.org/)
[Official Docker Images](https://hub.docker.com/r/apache/apisix)
The original package (2026-09-01) used a nonexistent Cloudron "etcd
addon" and an unpinned `apache/apisix:latest` - it could never have
installed. Rewritten 2026-09-07 (embedded etcd, digest pins, read-only
rootfs support) and deployed to production; see JOURNAL.md §20.
@@ -1,95 +0,0 @@
# APISIX Configuration Sample
# This file is automatically generated by Cloudron package
# Located at: /usr/local/apisix/conf/config.yaml
deployment:
role: traditional
role_traditional:
config_provider: etcd
admin:
# Admin API port
port: 9180
# Allow admin access from all IPs
# Restrict this in production to specific IPs
allow_admin:
- 0.0.0.0/0
# Admin API key (CHANGE THIS IN PRODUCTION)
admin_key:
- admin-key-secret-change-me
# Admin API version
admin_api_version: v3
etcd:
# Etcd hosts (automatically configured by Cloudron)
host:
- 127.0.0.1
port: 2379
prefix: "/apisix"
timeout: 30
apisix:
# SSL configuration
ssl:
ssl_trusted_certificate: /etc/ssl/certs/ca-certificates.crt
ssl_protocols: "TLSv1.2 TLSv1.3"
# Main HTTP proxy port
node_listen: 9080
# Disable IPv6
enable_ipv6: false
# Enable CORS for admin API
enable_admin_cors: true
# Enable HTTP/2
enable_http2: true
# Nginx configuration
nginx_config:
# Error log file
error_log: "logs/error.log"
error_log_level: "warn"
# Worker processes
worker_processes: auto
# Maximum open files
worker_rlimit_nofile: 20480
# Event worker processes
event_worker_processes: 2
# Worker shutdown timeout
worker_shutdown_timeout: 240s
# Additional configuration options:
#
# Plugin configuration (default plugins enabled)
# plugins:
# - real-ip
# - proxy-rewrite
# - limit-req
# - limit-conn
# - prometheus
# - node-status
# - jwt-auth
# - key-auth
# - basic-auth
# - ip-restriction
# - cors
# - proxy-cache
# - limit-count
# - request-id
# - fault-injection
# - consumer
#
# Stream plugins (for TCP/UDP):
# stream_plugins:
# - mqtt-proxy
# - ip-restriction
# - limit-conn
File diff suppressed because one or more lines are too long
+123 -44
View File
@@ -1,69 +1,148 @@
#!/bin/bash
set -euo pipefail
set -e
# APISIX runtime setup for Cloudron:
# 1. persist the Admin API key (rotating it would break automation clients)
# 2. relocate the APISIX runtime tree to /app/data - Cloudron mounts the
# container rootfs READ-ONLY (writable: /app/data, /tmp, /run), so the
# generated config.yaml, nginx.conf, logs and sockets must live there
# 3. start the embedded single-node etcd (state under /app/data/etcd,
# survives restarts and backups)
# 4. regenerate config.yaml on EVERY start, then apisix init + init_etcd
# 5. seed the /healthz route once (cosmetic - Cloudron accepts 4xx)
# 6. exec openresty in the foreground with the /app/data prefix
#
# The Admin API listens on 127.0.0.1:9180 ONLY and is never published;
# reach it via `cloudron exec` with the key in /app/data/.admin_key.
# Cloudron etcd connection
ETCD_HOST=${CLOUDRON_ETCD_HOST:-127.0.0.1}
ETCD_PORT=${CLOUDRON_ETCD_PORT:-2379}
DATA_DIR="/app/data"
ETCD_DIR="${DATA_DIR}/etcd"
ADMIN_KEY_FILE="${DATA_DIR}/.admin_key"
ETCD_ENDPOINT="http://127.0.0.1:2379"
echo "Etcd host: $ETCD_HOST"
echo "Etcd port: $ETCD_PORT"
PROXY_PORT="${CLOUDRON_APP_PORT:-9080}"
# Wait for etcd to be ready
echo "Waiting for etcd to be ready..."
MAX_WAIT=30
WAIT_TIME=0
while ! curl -f "http://${ETCD_HOST}:${ETCD_PORT}/health" 2>/dev/null; do
if [ $WAIT_TIME -ge $MAX_WAIT ]; then
echo "Timeout waiting for etcd"
# writable runtime tree (container rootfs is read-only on Cloudron).
# The apisix CLI derives its install dir from the script path and writes
# nginx.conf there, so the lua CLI tree + deps are copied to /app/data and
# invoked from the copy (~62 MB, once per start).
export APISIX_PREFIX="${DATA_DIR}/apisix"
CONF="${APISIX_PREFIX}/conf/config.yaml"
mkdir -p "${APISIX_PREFIX}/conf" "${APISIX_PREFIX}/logs"
# re-copy on every start so upgrades never run a stale tree
rm -rf "${APISIX_PREFIX}/apisix" "${APISIX_PREFIX}/deps"
cp -a /usr/local/apisix/apisix "${APISIX_PREFIX}/"
cp -a /usr/local/apisix/deps "${APISIX_PREFIX}/"
# the CLI hardcodes apisix_home = /usr/local/apisix (read-only); repoint the
# copy at the writable prefix so nginx.conf and deps resolve under /app/data
grep -rl '/usr/local/apisix' "${APISIX_PREFIX}/apisix" \
| xargs sed -i "s|/usr/local/apisix|${APISIX_PREFIX}|g"
# the CLI looks for its config templates under $APISIX_PREFIX/conf
cp -a /usr/local/apisix/conf/. "${APISIX_PREFIX}/conf/"
APISIX_CLI="/usr/local/openresty/luajit/bin/luajit ${APISIX_PREFIX}/apisix/cli/apisix.lua"
# --- 1. persistent Admin API key ---------------------------------------------
if [[ ! -s "${ADMIN_KEY_FILE}" ]]; then
( umask 077; head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${ADMIN_KEY_FILE}" )
echo "Generated new Admin API key at ${ADMIN_KEY_FILE}"
fi
ADMIN_KEY="$(cat "${ADMIN_KEY_FILE}")"
# --- 2. embedded etcd ----------------------------------------------------------
mkdir -p "${ETCD_DIR}"
# --initial-cluster is ignored on restarts from an existing data-dir.
etcd \
--name default \
--data-dir "${ETCD_DIR}" \
--listen-client-urls "http://127.0.0.1:2379" \
--advertise-client-urls "http://127.0.0.1:2379" \
--listen-peer-urls "http://127.0.0.1:2380" \
--initial-advertise-peer-urls "http://127.0.0.1:2380" \
--initial-cluster "default=http://127.0.0.1:2380" \
--auto-compaction-retention=1h &
echo "Waiting for embedded etcd at ${ETCD_ENDPOINT} ..."
for i in $(seq 1 30); do
if etcdctl --endpoints="${ETCD_ENDPOINT}" endpoint health >/dev/null 2>&1; then
echo "etcd is up"
break
fi
if [[ "$i" -eq 30 ]]; then
echo "etcd failed to become healthy" >&2
exit 1
fi
echo "Etcd is unavailable - sleeping ($WAIT_TIME/$MAX_WAIT)"
sleep 2
WAIT_TIME=$((WAIT_TIME+2))
sleep 1
done
echo "Etcd is ready!"
# --- 3. generated config (rewritten on every start) -----------------------------
cat > "${CONF}" <<EOF
# Generated by start.sh on every boot - manual edits will be lost.
# Data-plane routes live in the embedded etcd (persisted under
# /app/data/etcd) and are managed through the Admin API.
# Create APISIX configuration file
cat > /usr/local/apisix/conf/config.yaml << 'EOF'
deployment:
role: traditional
role_traditional:
config_provider: etcd
admin:
port: 9180
allow_admin:
- 0.0.0.0/0
admin_key:
- ${ADMIN_KEY:-admin-key-secret-change-me}
admin_api_version: v3
etcd:
host:
- ${ETCD_HOST}
port: ${ETCD_PORT}
prefix: "/apisix"
- "${ETCD_ENDPOINT}"
prefix: /apisix
timeout: 30
admin:
admin_listen:
ip: 127.0.0.1
port: 9180
admin_key:
- name: admin
key: ${ADMIN_KEY}
role: admin
apisix:
ssl:
ssl_trusted_certificate: /etc/ssl/certs/ca-certificates.crt
ssl_protocols: "TLSv1.2 TLSv1.3"
node_listen: 9080
node_listen:
- ${PROXY_PORT}
enable_ipv6: false
enable_admin_cors: true
enable_http2: true
ssl:
# TLS terminates at the Cloudron platform proxy
enable: false
nginx_config:
error_log: "logs/error.log"
error_log_level: "warn"
error_log_level: warn
worker_processes: auto
worker_rlimit_nofile: 20480
event_worker_processes: 2
worker_shutdown_timeout: 240s
http:
# restore real client IPs from the Cloudron proxy's X-Forwarded-For
real_ip_header: X-Forwarded-For
real_ip_from:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
EOF
echo "APISIX configuration created at /usr/local/apisix/conf/config.yaml"
cat /usr/local/apisix/conf/config.yaml
# --- 4. init + schema bootstrap -------------------------------------------------
${APISIX_CLI} init
${APISIX_CLI} init_etcd
# Start APISIX
# apisix init rewrites config.yaml (deployment timestamp). nginx runs workers
# as nobody (no `user` directive in the generated nginx.conf), so the config
# must stay world-readable within the container - acceptable because the
# Admin API itself listens on 127.0.0.1 only.
chmod 644 "${CONF}"
echo "Generated APISIX config at ${CONF} (proxy port ${PROXY_PORT})"
# --- 5. seed the health route once ----------------------------------------------
# Cloudron's healthcheck accepts 4xx, so it is not required for the platform -
# it gives https://<app-domain>/healthz a friendly answer instead of a bare 404.
if [[ -z "$(etcdctl --endpoints="${ETCD_ENDPOINT}" get /apisix/routes/healthz --print-value-only 2>/dev/null)" ]]; then
# ngx.say is unavailable in the rewrite phase - a bare ngx.exit(200) is the
# error-proof direct response (200, empty body).
etcdctl --endpoints="${ETCD_ENDPOINT}" put /apisix/routes/healthz \
'{"uri":"/healthz","id":"healthz","priority":100,"plugins":{"serverless-pre-function":{"phase":"rewrite","functions":["return function() ngx.exit(200) end"]}}}' > /dev/null \
|| echo "WARNING: seeding /healthz failed (cosmetic only, continuing)" >&2
echo "Seeded /healthz route"
fi
# --- 6. run (foreground) ----------------------------------------------------------
echo "Starting APISIX ..."
exec /usr/bin/apisix start
rm -f "${APISIX_PREFIX}/conf/config_listen.sock" "${APISIX_PREFIX}/logs/worker_events.sock"
exec /usr/local/openresty/bin/openresty -p "${APISIX_PREFIX}" -g 'daemon off;'