Commit Graph
32 Commits
Author SHA1 Message Date
jknapp fe89f093bf Merge pull request 'ci: pin base images by digest resolved directly from Docker Hub' (#6) from ci/base-image-digest-pin into main
Cloud Node Container / Build-and-Push (20) (push) Successful in 5m2s
Cloud Node Container / Build-and-Push (22) (push) Successful in 5m28s
Cloud Node Container / Build-and-Push (18) (push) Successful in 6m15s
Cloud Node Container / Build-and-Push (24) (push) Successful in 4m57s
Cloud Node Container / Build-and-Push (26) (push) Successful in 5m50s
2026-10-22 13:59:36 +00:00
shadowdaoandClaude Opus 5.5 6b015141dc ci: pin base images by digest resolved directly from Docker Hub
With the pkg-cache BuildKit mirror on, `FROM almalinux/10-base` was resolved
by tag through the cache, so a compromised cache could substitute the image.

Each job using the mirror now looks up the base tag's digest DIRECTLY at
registry-1.docker.io (.gitea/scripts/resolve-base-image.sh, HTTPS, HEAD on
the manifest, regex-validated) and builds FROM image@sha256:... via the
BASE_IMAGE build-arg, so the mirror can only serve content-addressed blobs.
If the lookup fails, the mirror is NOT used for that job and the tag is pulled
straight from Docker Hub. The digest is resolved every run (no manual bumps);
local builds default to the tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:58:18 -07:00
jknapp 329136d78a Merge pull request 'fix(ci): build through the internal package cache; keep bundled npm on Node 18/20' (#5) from fix/npm-eol-lines into main
Cloud Node Container / Build-and-Push (18) (push) Successful in 3m8s
Cloud Node Container / Build-and-Push (24) (push) Successful in 5m18s
Cloud Node Container / Build-and-Push (20) (push) Successful in 5m55s
Cloud Node Container / Build-and-Push (26) (push) Successful in 5m18s
Cloud Node Container / Build-and-Push (22) (push) Successful in 5m48s
2026-10-22 13:52:10 +00:00
shadowdaoandClaude Opus 5.5 0edd0d3c53 fix(ci): build through the internal package cache; keep bundled npm on Node 18/20
- The first Node 24/26 run timed out on every job: public AlmaLinux mirrors
  served the runners at 1-150 kB/s. Use the CT130 package cache the same way
  cloud-apache-container does (same pkg-cache-repos.sh and pinned CA), with
  the upstream repos as fallback when the probe fails. epel-release is now
  GPG-verified before install instead of fetched unverified by URL.
- npm 12 requires Node >=22.22.2; on 18/20 keep the npm Node ships with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:52:07 -07:00
jknapp f1e92ea1b4 Merge pull request 'feat: Node.js 24 and 26 images; refuse builds below the Oct 2026 security floor' (#4) from feat/node24-26-security into main
Cloud Node Container / Build-and-Push (18) (push) Failing after 1m39s
Cloud Node Container / Build-and-Push (26) (push) Failing after 41m11s
Cloud Node Container / Build-and-Push (24) (push) Failing after 41m27s
Cloud Node Container / Build-and-Push (22) (push) Failing after 41m42s
Cloud Node Container / Build-and-Push (20) (push) Failing after 41m59s
2026-10-22 12:50:51 +00:00
shadowdaoandClaude Opus 5.5 ae6acdd009 feat: Node.js 24 and 26 images; refuse builds below the Oct 2026 security floor
- One install-node.sh <major> replaces install-node{18,20,22}.sh. It fails the
  build if NodeSource hands back a version below the newest security release
  for the line (22.23.4 / 24.22.1 / 26.12.1: CVE-2026-111497 Brotli OOB write
  and 9 others), so a lagging mirror can't ship a vulnerable image silently.
  18 and 20 are EOL: they still build, with a warning.
- CI matrix adds 24 and 26; fail-fast off so one line can't cancel the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:50:49 -07:00
jknapp a1364d7d52 Merge pull request 'fix(entrypoint): no recursive chmod on the customer home; root never follows customer symlinks' (#3) from fix/no-recursive-chmod-on-start into main
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m49s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m42s
Cloud Node Container / Build-and-Push (22) (push) Successful in 5m34s
2026-10-21 00:01:31 +00:00
shadowdaoandClaude Opus 5.5 4064d9d6aa fix(entrypoint): unlink planted symlinks as the customer, then fail closed
Root rm -f in drop_symlink could be redirected by a parent-directory swap to
delete arbitrary files as root. drop_symlink and the logs/ symlink sweep now
unlink AS THE CUSTOMER (can never delete anything the customer couldn't), and
drop_symlink still refuses to start if the path is a symlink afterwards; the
pre-daemon logs/ gate re-checks and fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:06:41 -07:00
shadowdaoandClaude Opus 5.5 d400d5eb39 fix(entrypoint): fail closed on planted symlinks before nginx starts
drop_symlink unlinks as root (never follows the target) and refuses to start if
the path is still a symlink. Immediately before nginx (which opens its logs
under /home/$user/logs as root) starts, logs/, logs/nginx and logs/nodejs must be
real directories and nothing under logs/ may be other than a regular file or
directory, else the entrypoint exits 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:04:21 -07:00
shadowdaoandClaude Opus 5.5 a84f05d41b fix(entrypoint): unlink planted symlinks as the customer, never as root
Same race-safety as cac: drop_symlink and the logs/ symlink sweep run as the
customer, so a parent dir swapped for a symlink between check and rm cannot
redirect a root unlink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:52:01 -07:00
shadowdaoandClaude Opus 5.5 b9c5a8d617 fix(entrypoint): root never follows customer symlinks under /home/$user
/home/$user is the customer's bind mount; they can plant symlinks in it. The
entrypoint ran as root and created/copied/chowned through customer paths (the
example-app copy, ecosystem.config.js generation + non-recursive chown, which
dereferences). Now: refuse to start if /home/$user is not a real dir; logs/ must
be a real dir and symlinks under it are removed and logged (nginx opens those
files as root); app/logs dirs, the example copy and ecosystem.config.js are
created AS THE CUSTOMER (setpriv, runuser fallback); the o+x directory pass runs
as the customer. chown -R is unchanged (GNU default -P never follows symlinks).
app/ itself may stay a symlink (release-dir deploys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:49:18 -07:00
shadowdaoandClaude Opus 5.5 3ad03be015 fix(entrypoint): stop chmod -R 755 on the customer home every start
entrypoint.sh ran `chmod -R 755 /home/$user` on every container start.
/home/$user is the customer's bind-mounted directory, so every file (.env and
other app secrets included) became world-readable and executable, and any
tighter permissions the customer set were reverted on each restart. It dates
from the initial implementation (2989cd5) with no stated reason; the same
pattern was removed from cloud-apache-container.

The legitimate need: nginx workers run as `nginx` (configs/nginx.conf) and
serve /static/ via `alias /home/$user/app/public/`, so nginx must traverse
directories. Keep the recursive chown; replace the recursive chmod with
chmod 755 on /home/$user itself and o+x on directories lacking it. Regular
files are never touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:34:38 -07:00
jknapp d2ae99cd37 Merge pull request 'Upgrade to AlmaLinux 10 and fix WebSocket proxying' (#2) from upgrade-almalinux-10 into main
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m44s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m27s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m27s
2026-08-03 16:07:23 +00:00
shadowdaoandClaude Opus 5 80b03c884e Use dumb-init --single-child for tini signal parity
Code review caught that dumb-init is not a drop-in replacement for tini as
invoked. tini without -g forwards a signal only to its direct child;
dumb-init without --single-child calls setsid() and forwards to the entire
process group. Verified against dumb-init 1.2.5 with a parent that catches
SIGTERM and does not forward it: under the default the grandchild is still
signalled, under --single-child it is not.

Without the flag, `docker stop` delivered SIGTERM to the tenant's node
process directly and simultaneously with pm2, bypassing pm2's kill_timeout
shutdown sequencing. Since the wedged-container fix in #1 introduced tini
specifically for its signal forwarding, the previous commit's claim that
the two are equivalent did not hold.

Also corrects MEMORY-GUIDE.md, which still described header buffers as
"reduced" after they were raised from 2 1k to the nginx default of 4 8k.
The nginx memory budget is unchanged: these buffers are allocated per
request only when a request needs them, not preallocated per connection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:06:10 -07:00
shadowdaoandClaude Opus 5 6651872729 Exclude .claude/ from the Docker build context
Keeps local Claude Code settings out of the build context alongside the
other editor directories. No effect on the image itself -- the Dockerfile
only copies scripts/, configs/ and examples/ -- but the directory was
being sent to the daemon on every build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 07:56:11 -07:00
shadowdaoandClaude Opus 5 e417a908f2 Upgrade to AlmaLinux 10 and fix WebSocket proxying
Base image moves from AlmaLinux 9 to 10, which forces two related changes:
EPEL repo URL bumps to the el10 release RPM, and tini is replaced with
dumb-init as PID 1 since tini is not packaged in EPEL 10. Both provide the
signal forwarding and zombie reaping the wedged-container fix relies on.
Nginx 1.26 in AlmaLinux 10 deprecates the `listen ... http2` parameter, so
the server block now uses the separate `http2 on;` directive.

Also fixes three issues that affected WebSocket apps (socket.io, ws):

- `Connection: upgrade` was hardcoded on every proxied request, including
  ordinary HTTP. Now driven by a `map $http_upgrade $connection_upgrade`
  so only genuine upgrade requests carry it.
- No explicit proxy read/send timeout meant idle WebSockets were cut at
  nginx's 60s default. Set to 600s, which clears any sane heartbeat without
  pinning connection slots (worker_connections is 512, two per client).
- `large_client_header_buffers 2 1k` returned 400 for any single header
  line over 1KB, which session cookies and bearer tokens routinely exceed.
  Raised to the nginx default of 4 8k; buffers are allocated on demand, so
  this only costs memory for requests that need it.

Verified by rendering the generated config and running it under nginx with
a Node backend: WebSocket handshakes return 101 and reach the upstream
'upgrade' event, polling requests arrive with `Connection: close`, and a
3KB cookie returns 200 where the old buffer setting returned 400.

README gains a WebSocket Support section covering the PM2 cluster-mode
trap, the concurrency ceiling, and reconnects on memory restarts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 07:54:32 -07:00
jknapp bdea2939c7 Merge pull request 'Fix wedged-container outage: TCP healthcheck + tini-managed PID 1' (#1) from fix/wedged-container-outage into main
Cloud Node Container / Build-and-Push (18) (push) Successful in 5m0s
Cloud Node Container / Build-and-Push (20) (push) Successful in 2m49s
Cloud Node Container / Build-and-Push (22) (push) Successful in 2m39s
Reviewed-on: #1
2026-05-05 14:30:12 +00:00
shadowdaoandClaude Opus 4.7 b431a66a7b Fix wedged-container outage: TCP healthcheck + tini-managed PID 1
A 10s postgresql restart took down transcribe.shadowdao.com-01 for ~17h
because pm2 gave up after 5 fast retries, the entrypoint's trailing
tail -f kept PID 1 alive, and the healthcheck (wget --spider on nginx
port 80) succeeded on the 301-to-https redirect regardless of whether
Node was alive.

Three coordinated fixes to the cnoc image:

- HEALTHCHECK: replace the redirect-passing wget probe with TCP-level
  checks on 127.0.0.1:3000 (Node) and :80 (nginx). Tenant-agnostic, no
  /ping dependency — catches the exact incident scenario (port 3000
  closed when pm2 exits).
- entrypoint.sh: exec pm2 via tini so it becomes PID 1. When pm2
  exhausts max_restarts and exits, the container exits and the
  unless-stopped restart policy brings it back. Logs are tailed in the
  background with -F (logrotate-safe).
- Dockerfile: install tini from EPEL for proper signal forwarding and
  zombie reaping of nginx/crond children that reparent to PID 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 06:59:52 -07:00
shadowdaoandClaude Opus 4.6 821db4873e Fix healthcheck to validate full stack through Nginx
Cloud Node Container / Build-and-Push (18) (push) Successful in 2m15s
Cloud Node Container / Build-and-Push (20) (push) Successful in 4m12s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m58s
Changed healthcheck from hitting Node.js directly on port 3000/ping to
going through Nginx on port 80, ensuring the entire stack is validated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:57:10 -07:00
shadowdao 566cdb5276 fix generate-ecosystem-config.sh
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m58s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m56s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m55s
2025-07-31 12:08:07 -07:00
shadowdaoandClaude 2029c8821e Add comprehensive troubleshooting section and clarify user permissions
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m48s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m48s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m51s
- Clarified that uid/user must match host system values
- Added note that PM2 runs as specified user for security
- Created troubleshooting section with common issues:
  - 502 Bad Gateway errors
  - Permission errors
  - App not starting
- Provided specific commands for debugging each issue

This documentation helps users understand and resolve common
container deployment issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 10:24:42 -07:00
shadowdaoandClaude a9dd0573ca Fix PM2 UID type error by using login shell and explicit undefined
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m47s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m52s
Cloud Node Container / Build-and-Push (22) (push) Successful in 2m0s
- Use su with login shell (-) to ensure clean environment
- Explicitly set uid/gid to undefined in ecosystem config
- This prevents PM2 from trying to parse string UID as integer

The error occurred because PM2 was receiving '1002' as a string
instead of an integer. By using a login shell and explicitly
setting uid/gid to undefined, PM2 won't try to switch users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 10:02:17 -07:00
shadowdaoandClaude aeb934d93e Document PM2 configuration and remove unnecessary ready signal
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m47s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m51s
Cloud Node Container / Build-and-Push (22) (push) Has been cancelled
- Added PM2 configuration notes to README
- Documented that wait_ready is false by default (no signals needed)
- Provided advanced usage instructions for users who need wait_ready
- Removed unnecessary process.send('ready') from simple-website example

This ensures users understand the default PM2 behavior and know how
to customize it if needed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 09:55:54 -07:00
shadowdaoandClaude 68ef106277 Fix PM2 cluster mode and user permission errors
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m52s
Cloud Node Container / Build-and-Push (22) (push) Has been cancelled
Cloud Node Container / Build-and-Push (20) (push) Has been cancelled
- Force PM2 to use fork mode instead of cluster mode
- Disable wait_ready to avoid startup issues
- Add PM2 ready signal to simple-website server
- Add PM2 status check after startup
- Set NODE_ENV=production for PM2 startup

The cluster mode was causing the UID 1002 error. Fork mode runs
the process directly as the specified user without additional
permission complications.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 09:53:01 -07:00
shadowdaoandClaude 205c1c4d68 Fix PM2 user permission error (UID 1002 not found)
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m58s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m51s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m53s
- Add proper error handling for user creation with detailed logging
- Verify user exists before starting PM2
- Set HOME environment variable when running PM2
- Run PM2 with --no-daemon flag and proper user context
- Add ownership fix for generated ecosystem.config.js

This should resolve the "User identifier does not exist: 1002" error
by ensuring the user is properly created and PM2 runs in the correct context.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 09:22:43 -07:00
shadowdaoandClaude 88d0d4e64b Fix Dockerfile build error - use dnf instead of yum for groupinstall
Cloud Node Container / Build-and-Push (18) (push) Successful in 1m51s
Cloud Node Container / Build-and-Push (20) (push) Successful in 1m52s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m50s
Changed 'yum groupinstall' to 'dnf group install -y' to match AlmaLinux 9
package manager and ensure non-interactive installation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 09:03:33 -07:00
shadowdaoandClaude 9f0aa4b8b1 Implement auto-generation of ecosystem.config.js and improve container setup
Cloud Node Container / Build-and-Push (18) (push) Failing after 38s
Cloud Node Container / Build-and-Push (20) (push) Failing after 33s
Cloud Node Container / Build-and-Push (22) (push) Failing after 33s
- Add automatic ecosystem.config.js generation from package.json
- Create app directory automatically if missing
- Copy simple-website example when app directory is empty
- Remove redundant default app files from configs/
- Add HAProxy support with proper real IP forwarding
- Configure nginx to trust proxy headers from private networks
- Simplify entrypoint logic - always use /home/$user/app

This makes the container more user-friendly by eliminating the need for
manual PM2 configuration and ensuring the server always has a working app.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 09:01:08 -07:00
shadowdaoandClaude f8edb2b407 Change container name from cnc to cnoc to avoid nginx container conflict
Cloud Node Container / Build-and-Push (18) (push) Successful in 51s
Cloud Node Container / Build-and-Push (20) (push) Successful in 51s
Cloud Node Container / Build-and-Push (22) (push) Successful in 53s
- Update Gitea pipeline to build cnoc:node18, cnoc:node20, cnoc:node22
- Update local-dev.sh to use cnoc container from registry
- Update documentation references from CNC to CNOC
- Update default app package name to cnoc-default-app
- Avoid conflict with existing nginx container (cnc)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 16:17:16 -07:00
shadowdaoandClaude e9d4d11177 Fix package conflict by removing curl dependency
Cloud Node Container / Build-and-Push (18) (push) Successful in 49s
Cloud Node Container / Build-and-Push (20) (push) Successful in 54s
Cloud Node Container / Build-and-Push (22) (push) Successful in 1m12s
- Remove curl from package installation to avoid curl/curl-minimal conflict
- Replace curl with wget in health check and Node.js installation scripts
- wget is already installed and provides same functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 16:05:37 -07:00
shadowdaoandClaude 0b99f5f011 Fix Dockerfile build error - remove non-existent default.conf
Cloud Node Container / Build-and-Push (18) (push) Failing after 26s
Cloud Node Container / Build-and-Push (20) (push) Failing after 23s
Cloud Node Container / Build-and-Push (22) (push) Failing after 25s
The nginx default.conf is generated dynamically by create-nginx-config.sh
script at runtime, not copied during build.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 16:03:18 -07:00
shadowdaoandClaude 2989cd590a Complete Node.js container implementation with multi-version support
Cloud Node Container / Build-and-Push (18) (push) Failing after 10s
Cloud Node Container / Build-and-Push (20) (push) Failing after 8s
Cloud Node Container / Build-and-Push (22) (push) Failing after 8s
- Add Dockerfile with AlmaLinux 9 base, Nginx reverse proxy, and PM2
- Support Node.js versions 18, 20, 22 with automated installation
- Implement memory-optimized configuration (256MB minimum, 512MB recommended)
- Add Memcached session storage for development environments
- Create comprehensive documentation (README, USER-GUIDE, MEMORY-GUIDE, CLAUDE.md)
- Include example applications (simple website and REST API)
- Add Gitea CI/CD pipeline for automated multi-version builds
- Provide local development script with helper utilities
- Implement health monitoring, log rotation, and backup systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 16:00:46 -07:00
jknapp 36285fa102 Initial commit 2025-07-21 22:35:23 +00:00