Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e2e723
Add Thruster HTTP/2 proxy and Shakapacker early hints support
justin808 Nov 20, 2025
d64e758
Add Chrome DevTools Protocol scripts for verifying early hints
justin808 Nov 20, 2025
74cfc83
Fix Docker build by adding SECRET_KEY_BASE to react_on_rails:locale task
justin808 Nov 20, 2025
aed736d
Update react_on_rails to 16.2.0.beta.11
justin808 Nov 20, 2025
4698881
Update react-on-rails npm package to 16.2.0-beta.11
justin808 Nov 20, 2025
61c9469
Fix Rails 8.1+ deployment by adding SECRET_KEY_BASE to release script
justin808 Nov 20, 2025
014a974
Add SECRET_KEY_BASE to Control Plane GVC template
justin808 Nov 20, 2025
5fadd89
Enable Early Hints debug mode to verify if Rails sends HTTP 103
justin808 Nov 21, 2025
fe5bfc6
Update react_on_rails to 16.2.0.beta.12
justin808 Nov 21, 2025
54a7956
Update react_on_rails to use master branch from GitHub
justin808 Nov 21, 2025
e29a702
Improve development setup and webpack configuration
justin808 Nov 22, 2025
3f12f78
Update react_on_rails to use master branch from GitHub
justin808 Nov 23, 2025
63ce41f
Update react_on_rails and fix headless Chrome for tests
justin808 Nov 25, 2025
5730ac5
Update react_on_rails and fix headless Chrome for tests
justin808 Nov 25, 2025
f28ff59
Fix RuboCop line length violation in react_on_rails.rb
justin808 Nov 25, 2025
3c4dcd9
Improve Early Hints tooling and documentation
justin808 Dec 18, 2025
447cc56
Replace hardcoded PR review app URLs with placeholder
justin808 Dec 18, 2025
1995bca
lock files
justin808 Dec 18, 2025
0f66fce
Fix shakapacker gem/npm version mismatch
justin808 Dec 22, 2025
69fb64c
Add PropTypes to Footer icon components
justin808 Dec 22, 2025
1474111
Move Rails environment require before precompile tasks
justin808 Dec 25, 2025
8a93eef
Simplify build orchestration for rescript and locale generation
justin808 Dec 25, 2025
b25dca9
Add .context directory to .gitignore
justin808 Dec 25, 2025
32ea00f
Clean up configuration and remove duplicate code
justin808 Dec 25, 2025
22c9c04
Update react_on_rails to 16.2.0.rc1 and shakapacker to 9.5.0
justin808 Jan 15, 2026
154aa64
Address CodeRabbit review feedback
justin808 Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .controlplane/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.4.3
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
ARG RUBY_VERSION=3.4.6
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base

# Current commit hash environment variable
ARG GIT_COMMIT
Expand Down Expand Up @@ -32,7 +32,7 @@ ENV RAILS_ENV="production" \


# Throw-away build stage to reduce size of final image
FROM base as build
FROM base AS build

# Install application gems
COPY Gemfile Gemfile.lock ./
Expand Down Expand Up @@ -60,23 +60,27 @@ COPY --from=build /app /app

RUN chmod +x /app/.controlplane/*.sh

# Set environment variables for asset compilation
ENV RAILS_ENV=production \
NODE_ENV=production \
SECRET_KEY_BASE=NOT_USED_NON_BLANK
# compiling assets requires any value for ENV of SECRET_KEY_BASE
NODE_ENV=production

# These files hardly ever change
RUN bin/rails react_on_rails:locale
# SECRET_KEY_BASE is required for Rails initialization but is not persisted in the image
RUN SECRET_KEY_BASE=precompile_placeholder bin/rails react_on_rails:locale

# These files change together, /app/lib/bs are temp build files for rescript,
# and /app/client/app are the client assets that are bundled, so not needed once built
# Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images
RUN yarn res:build && bin/rails assets:precompile && rm -rf /app/lib/bs /app/client/app
# SECRET_KEY_BASE is required for asset precompilation but is not persisted in the image
RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
SECRET_KEY_BASE=precompile_placeholder bin/rails assets:precompile && \
rm -rf /app/lib/bs /app/client/app

# This is like the shell initialization that will take the CMD as args
# For Kubernetes and ControlPlane, this is the command on the workload.
ENTRYPOINT ["./.controlplane/entrypoint.sh"]

# Default args to pass to the entry point that can be overridden
# For Kubernetes and ControlPlane, these are the "workload args"
CMD ["./bin/rails", "server"]
# Use Thruster HTTP/2 proxy for optimized performance
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
168 changes: 168 additions & 0 deletions .controlplane/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,174 @@ If you needed to push a new image with a specific commit SHA, you can run the fo
cpflow build-image -a $APP_NAME --commit ABCD
```

## HTTP/2 and Thruster Configuration

This application uses [Thruster](https://github.com/basecamp/thruster), a zero-config HTTP/2 proxy from Basecamp, for optimized performance on Control Plane.

### What is Thruster?

Thruster is a small, fast HTTP/2 proxy designed for Ruby web applications. It provides:
- **HTTP/2 Support**: Automatic HTTP/2 with multiplexing for faster asset loading
- **Asset Caching**: Intelligent caching of static assets
- **Compression**: Automatic gzip/Brotli compression
- **TLS Termination**: Built-in Let's Encrypt support (not needed on Control Plane)

### Control Plane Configuration for Thruster

To enable Thruster with HTTP/2 on Control Plane, two configuration changes are required:

#### 1. Dockerfile CMD (`.controlplane/Dockerfile`)

The Dockerfile must use Thruster to start the Rails server:

```dockerfile
# Use Thruster HTTP/2 proxy for optimized performance
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
```

**Note:** Do NOT use `--early-hints` flag as Thruster handles this automatically.

#### 2. Workload Port Protocol (`.controlplane/templates/rails.yml`)

The workload port should remain as HTTP/1.1:

```yaml
ports:
- number: 3000
protocol: http # Keep as http, not http2
```

**Important:** This may seem counter-intuitive, but here's why:
- **Thruster handles HTTP/2** on the public-facing TLS connection
- **Control Plane's load balancer** communicates with the container via HTTP/1.1
- Setting `protocol: http2` causes a protocol mismatch and 502 errors
- Thruster automatically provides HTTP/2 to end users through its TLS termination

### Important: Dockerfile vs Procfile

**On Heroku:** The `Procfile` defines how dynos start:
```
web: bundle exec thrust bin/rails server
```

**On Control Plane/Kubernetes:** The `Dockerfile CMD` defines how containers start. The Procfile is ignored.

This is a common source of confusion when migrating from Heroku. Always ensure your Dockerfile CMD matches your intended startup command.

### Verifying HTTP/2 is Enabled

After deployment, verify HTTP/2 is working:

1. **Check workload logs:**
```bash
cpflow logs -a react-webpack-rails-tutorial-staging
```

You should see Thruster startup messages:
```
[thrust] Starting Thruster HTTP/2 proxy
[thrust] Proxying to http://localhost:3000
[thrust] Serving from ./public
```

2. **Test HTTP/2 in browser:**
- Open DevTools → Network tab
- Load the site
- Check the Protocol column (should show "h2" for HTTP/2)

3. **Check response headers:**
```bash
curl -I https://your-app.cpln.app
```
Look for HTTP/2 indicators in the response.

### Troubleshooting

#### Workload fails to start

**Symptom:** Workload shows as unhealthy or crashing

**Solution:** Check logs with `cpflow logs -a <app-name>`. Common issues:
- Missing `thruster` gem in Gemfile
- Incorrect CMD syntax in Dockerfile
- Port mismatch (ensure Rails listens on 3000)

#### Getting 502 errors after enabling HTTP/2

**Symptom:** Workload returns 502 Bad Gateway with "protocol error"

**Root Cause:** Setting `protocol: http2` in rails.yml causes a protocol mismatch

**Solution:**
1. Change `protocol: http2` back to `protocol: http` in `.controlplane/templates/rails.yml`
2. Apply the template: `cpflow apply-template rails -a <app-name>`
3. The workload will immediately update (no redeploy needed)

**Why:** Thruster provides HTTP/2 to end users, but Control Plane's load balancer communicates with containers via HTTP/1.1. Setting the port protocol to `http2` tells the load balancer to expect HTTP/2 from the container, which Thruster doesn't provide on the backend.

#### Assets not loading or CORS errors

**Symptom:** Static assets return 404 or fail to load

**Solution:**
- Ensure `bin/rails assets:precompile` runs in Dockerfile
- Verify `public/packs/` directory exists in container
- Check Thruster is serving from correct directory

### Performance Benefits

With Thruster and HTTP/2 enabled on Control Plane, you should see:
- **20-30% faster** initial page loads due to HTTP/2 multiplexing
- **40-60% reduction** in transfer size with Brotli compression
- **Improved caching** of static assets
- **Lower server load** due to efficient asset serving

For detailed Thruster documentation, see [docs/thruster.md](../docs/thruster.md).

### Key Learnings: Thruster + HTTP/2 Architecture

This section documents important insights gained from deploying Thruster with HTTP/2 on Control Plane.

#### Protocol Configuration is Critical

**Common Mistake:** Setting `protocol: http2` in the workload port configuration
**Result:** 502 Bad Gateway with "protocol error"
**Correct Configuration:** Use `protocol: http`

#### Why This Works

Control Plane's architecture differs from standalone Thruster deployments:

**Standalone Thruster (e.g., VPS):**
```
User → HTTPS/HTTP2 → Thruster → HTTP/1.1 → Rails
(Thruster handles TLS + HTTP/2)
```

**Control Plane + Thruster:**
```
User → HTTPS/HTTP2 → Control Plane LB → HTTP/1.1 → Thruster → HTTP/1.1 → Rails
(LB handles TLS) (protocol: http) (HTTP/2 features)
```

#### What Thruster Provides on Control Plane

Even with `protocol: http`, Thruster still provides:
- ✅ Asset caching and compression
- ✅ Efficient static file serving
- ✅ Early hints support
- ✅ HTTP/2 multiplexing features (via Control Plane LB)

The HTTP/2 protocol is terminated at Control Plane's load balancer, which then communicates with Thruster via HTTP/1.1. Thruster's caching, compression, and early hints features work regardless of the protocol between the LB and container.

#### Debugging Tips

If you encounter 502 errors:
1. Verify Thruster is running: `cpln workload exec ... -- cat /proc/1/cmdline`
2. Test internal connectivity: `cpln workload exec ... -- curl localhost:3000`
3. Check protocol setting: Should be `protocol: http` not `http2`
4. Review workload logs: `cpln workload eventlog <workload> --gvc <gvc> --org <org>`

## Other notes

### `entrypoint.sh`
Expand Down
4 changes: 3 additions & 1 deletion .controlplane/release_script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ log 'Running release_script.sh per controlplane.yml'

if [ -x ./bin/rails ]; then
log 'Run DB migrations'
./bin/rails db:prepare || error_exit "Failed to run DB migrations"
# SECRET_KEY_BASE is required for Rails 8.1+ initialization but not used for migrations
# The actual secret key will be provided at runtime by the environment
SECRET_KEY_BASE="${SECRET_KEY_BASE:-precompile_placeholder}" ./bin/rails db:prepare || error_exit "Failed to run DB migrations"
else
error_exit "./bin/rails does not exist or is not executable"
fi
Expand Down
5 changes: 5 additions & 0 deletions .controlplane/templates/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ spec:
- name: REDIS_URL
# No password for GVC local Redis. See comment above for postgres.
value: 'redis://redis.{{APP_NAME}}.cpln.local:6379'
- name: SECRET_KEY_BASE
# For test apps, a placeholder value is fine. For production apps, this should be
# set to a secure random value using: openssl rand -hex 64
# Production apps should configure this manually after app creation via a secret.
value: 'placeholder_secret_key_base_for_test_apps_only'
# Part of standard configuration
staticPlacement:
locationLinks:
Expand Down
2 changes: 2 additions & 0 deletions .controlplane/templates/rails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ spec:
ports:
- number: 3000
protocol: http
# Note: Keep as 'http' - Thruster handles HTTP/2 on the TLS frontend,
# but the load balancer communicates with the container via HTTP/1.1
defaultOptions:
# Start out like this for "test apps"
autoscaling:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/js_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
node: [22.x]
ruby: [3.4.3]
ruby: [3.4.6]

env:
RAILS_ENV: test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
node: [22.x]
ruby: [3.4.3]
ruby: [3.4.6]

env:
RAILS_ENV: test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rspec_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
node: [22.x]
ruby: [3.4.3]
ruby: [3.4.6]

services:
postgres:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,8 @@ client/app/bundles/comments/rescript/**/*.bs.js
**/*.bs.js

.claude/
.context/

# Chrome DevTools debug scripts (development-only)
check_early_hints.js
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines should not be ignored if we are going to be committing these to the project.

check_early_hints.py
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.4.3
3.4.6
7 changes: 4 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.4.3"
ruby "3.4.6"

gem "react_on_rails", "16.2.0.beta.20"
gem "shakapacker", "9.3.4.beta.0"
gem "react_on_rails", "16.2.0.rc1"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to final version 16.2.1.

gem "shakapacker", "9.5.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails"
gem "listen"
Expand All @@ -15,6 +15,7 @@ gem "rails", "~> 8.0"
gem "pg"

gem "puma"
gem "thruster", "~> 0.1"

# Use SCSS for stylesheets
gem "sass-rails"
Expand Down
Loading