Files
proxy/Dockerfile
T
wmantly 94ad6143cc Dockerize the proxy (all-in-one image) + Docker docs
All-in-one Dockerfile bundling OpenResty + the Node mgmt app + Redis in one
container, mirroring the bare-metal ops/install.sh layout:
- Dockerfile (openresty/openresty:1.31.1.1-2-bookworm-fat base; dumb-init PID 1;
  luarocks install lua-resty-auto-ssl/luasocket/lua-resty-ipmatcher; node 22.x;
  npm ci --omit=dev; OpenResty confs + lua copied into place).
- docker-entrypoint.sh: fallback cert, sed-parameterize RESOLVER/REAL_IP_FROM,
  start bundled redis + node app, exec openresty foreground.
- docker-compose.yml (standalone), .dockerignore, DEPLOYMENT.md.
- nodejs/routes/render.js: /health endpoint for healthchecks.
- nodejs/models/user_ldap.js: tlsOptions forwarded to ldapts Client so the
  proxy can bind ldaps:// with a self-signed cert (app_ldap__tlsOptions__*).
- nodejs/package.json: bump @simpleworkjs/conf to ^1.1.0 (app_* env overrides).
- docs/docker.md + index.md: Docker deployment guide + fronting an SSO Manager.
- ops/proxy.service: add WorkingDirectory=/var/www/proxy/nodejs (bare-metal
  cwd fix so relative conf/ paths resolve).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-11 17:02:52 -04:00

130 lines
7.5 KiB
Docker

# Theta42 Proxy — All-in-One Dockerfile
# OpenResty (front) + Node management app + Redis in a single container, mirroring
# the bare-metal ops/install.sh layout. Intended for self-contained single-node
# deployments (home labs / small businesses). Each process is supervised by
# dumb-init (PID 1); redis + the node app run in the background and OpenResty
# runs in the foreground as the primary process.
#
# Base image: the official openresty/openresty "-fat" variant bundles luarocks
# preconfigured for OpenResty's luajit, so `luarocks install` places rocks into
# /usr/local/openresty/lualib (which is on OpenResty's package.path) — no manual
# --lua-dir/--tree wrangling needed.
FROM openresty/openresty:1.31.1.1-2-bookworm-fat
# ── Tooling needed before adding apt repos ──────────────────────────────────
# The -fat base image lacks gnupg, which the NodeSource keyring setup needs.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# ── Node.js 22.x (NodeSource) ────────────────────────────────────────────────
# The management app + its native deps (bcrypt) need Node. Matches
# ops/install.sh NODE_MAJOR=22.
RUN install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list
# ── System packages ──────────────────────────────────────────────────────────
# build-essential g++ make python3 : native addon build for bcrypt
# libpam0g-dev : native build for linux-sys-user
# redis-server : bundled Redis (the app + lua-resty-auto-ssl
# + targetinfo.lua all reach 127.0.0.1:6379)
# dumb-init : PID 1 zombie reaping + signal forwarding
# openssl lsb-release wget : tooling
# nodejs : Node 22.x runtime
# luarocks ships in the -fat base image (configured for OpenResty's luajit).
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential g++ make python3 \
libpam0g-dev \
redis-server \
dumb-init \
openssl lsb-release wget \
nodejs \
&& rm -rf /var/lib/apt/lists/*
# ── Lua modules ──────────────────────────────────────────────────────────────
# lua-resty-auto-ssl : Let's Encrypt automation (certs stored in the bundled redis)
# luasocket : socket helpers used by auto-ssl
# lua-resty-ipmatcher: CIDR matching for per-host IP allow/deny (hostfeatures.lua)
# resty.limit.req is bundled with OpenResty, so no rock is needed for it.
RUN luarocks install lua-resty-auto-ssl \
&& luarocks install luasocket \
&& luarocks install lua-resty-ipmatcher
# ── Node app ─────────────────────────────────────────────────────────────────
WORKDIR /app
# Install production deps first (layer cache: only rebuilds when package*.json
# changes). .dockerignore excludes nodejs/node_modules.
COPY nodejs/package*.json ./
RUN npm ci --omit=dev
# App source (mirror sso-manager's layout — flattened into /app).
COPY nodejs/app.js ./
COPY nodejs/bin ./bin
COPY nodejs/conf ./conf
COPY nodejs/controller ./controller
COPY nodejs/middleware ./middleware
COPY nodejs/migrations ./migrations
COPY nodejs/models ./models
COPY nodejs/routes ./routes
COPY nodejs/services ./services
COPY nodejs/utils ./utils
COPY nodejs/views ./views
COPY nodejs/public ./public
# ── OpenResty config (mirrors ops/install.sh symlink targets) ─────────────────
# The default OpenResty config lives at /usr/local/openresty/nginx/conf/nginx.conf
# (the prefix conf dir); relative `include` directives resolve there. We place:
# nginx.conf -> prefix conf dir (overwrites the stock config)
# autossl.conf -> prefix conf dir (included by proxy.conf)
# proxy.conf -> prefix conf dir/sites-enabled/000-proxy (included by nginx.conf)
# *.lua -> /usr/local/openresty/lualib (on OpenResty's package.path)
# The entrypoint sed-substitutes the env-specific real_ip/resolver values into
# the copied files at runtime, so the committed confs (which carry the bare-metal
# home-LAN values) are left untouched for bare-metal use.
RUN install -d /usr/local/openresty/nginx/conf/sites-enabled
COPY ops/nginx_conf/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY ops/nginx_conf/autossl.conf /usr/local/openresty/nginx/conf/autossl.conf
COPY ops/nginx_conf/proxy.conf /usr/local/openresty/nginx/conf/sites-enabled/000-proxy
COPY ops/nginx_conf/targetinfo.lua /usr/local/openresty/lualib/targetinfo.lua
COPY ops/nginx_conf/hostfeatures.lua /usr/local/openresty/lualib/hostfeatures.lua
# ── Runtime dirs ─────────────────────────────────────────────────────────────
# nginx.conf writes access/error logs to /var/log/nginx and uses a response
# cache at /var/cache/nginx/proxy. OpenResty workers run as `nobody` (the
# compiled-in default — nginx.conf leaves `user` commented), so the cache dir
# must be owned by nobody:nogroup for proxy_cache_path to write to it. The node
# app creates /var/run/proxy_lookup.socket and chmods it 777 so the nobody
# workers can connect (see utils/unix_socket_json.js).
RUN install -d -m 0755 -o nobody -g nogroup /var/cache/nginx/proxy \
&& install -d /var/log/nginx /var/run /etc/ssl
# ── Entrypoint ───────────────────────────────────────────────────────────────
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# 80/443/4443 : public proxy listeners (autossl.conf)
# 3000 : node management API + web UI (internal; the OpenResty front
# proxies /api/* and the UI. Expose it for first-run access /
# healthcheck — bind to localhost only in production via compose.)
EXPOSE 80 443 4443 3000
# Healthcheck: the node app's /health (added in routes/render.js). Confirms the
# management process is up and routing; OpenResty liveness is implied because it
# proxies /api/auth -> the node app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://localhost:3000/health || exit 1
# dumb-init reaps zombies and forwards SIGTERM to the OpenResty process the
# entrypoint execs into, so `docker stop` shuts down cleanly instead of hitting
# the 10s kill timeout.
ENTRYPOINT ["dumb-init", "/usr/local/bin/docker-entrypoint.sh"]
# The entrypoint starts redis + the node app in the background, then execs
# `openresty -g 'daemon off;'` in the foreground as the primary process.
CMD ["openresty", "-g", "daemon off;"]