If you need a free demo account for Control Plane (no CC required), you can contact Justin Gordon, CEO of ShakaCode.
Check how the cpflow gem (this project) is used in the Github actions.
Here is a brief video overview.
This simple example shows how to deploy a simple app on Control Plane using the cpflow gem.
To maximize simplicity, this example creates Postgres and Redis as workloads in the same GVC as the app. In a real app, you would likely use persistent, external resources, such as AWS RDS and AWS ElastiCache.
You can see the definition of Postgres and Redis in the .controlplane/templates directory.
-
Ensure your Control Plane account is set up. You should have an
organization<your-org>for testing in that account. Set ENV variableCPLN_ORGto<your-org>. Alternatively, you may modify the value foraliases.common.cpln_orgin.controlplane/controlplane.yml. If you need an organization, please contact Shakacode. -
Install Control Plane CLI (and configure access) using
npm install -g @controlplane/cli. You can update thecplncommand line withnpm update -g @controlplane/cli. Then runcpln loginto ensure access. For more informatation check out the docs here. -
Run
cpln image docker-login --org <your-org>to ensure that you have access to the Control Plane Docker registry. -
Install the latest version of
cpflowgem on your project's Gemfile or globally. For more information check out Heroku to Control Plane. -
This project has a
Dockerfilefor Control Plane in.controlplanedirectory. You can use it as an example for your project. Ensure that you have Docker running.
Do not confuse the cpflow CLI with the cpln CLI.
The cpflow CLI is the Heroku to Control Plane playbook CLI.
The cpln CLI is the Control Plane CLI.
See the filese in the ./controlplane directory.
/templates: defines the objects created with thecpflow setupcommand. These YAML files are the same as used by thecpln applycommand./controlplane.yml: defines your application, including the organization, location, and app name.Dockerfile: defines the Docker image used to run the app on Control Plane.entrypoint.sh: defines the entrypoint script used to run the app on Control Plane.
Check if the Control Plane organization and location are correct in .controlplane/controlplane.yml.
Alternatively, you can use CPLN_ORG environment variable to set the organization name.
You should be able to see this information in the Control Plane UI.
Note: The below commands use cpflow which is the Heroku to Control Plane playbook gem,
and not cpln which is the Control Plane CLI.
# Use environment variable to prevent repetition
export APP_NAME=react-webpack-rails-tutorial
# Provision all infrastructure on Control Plane.
# app react-webpack-rails-tutorial will be created per definition in .controlplane/controlplane.yml
cpflow setup-app -a $APP_NAME
# Build and push docker image to Control Plane repository
# Note, may take many minutes. Be patient.
# Check for error messages, such as forgetting to run `cpln image docker-login --org <your-org>`
cpflow build-image -a $APP_NAME
# Promote image to app after running `cpflow build-image command`
# Note, the UX of images may not show the image for up to 5 minutes.
# However, it's ready.
cpflow deploy-image -a $APP_NAME
# See how app is starting up
cpflow logs -a $APP_NAME
# Open app in browser (once it has started up)
cpflow open -a $APP_NAMEAfter committing code, you will update your deployment of react-webpack-rails-tutorial with the following commands:
# Assuming you have already set APP_NAME env variable to react-webpack-rails-tutorial
# Build and push new image with sequential image tagging, e.g. 'react-webpack-rails-tutorial:1', then 'react-webpack-rails-tutorial:2', etc.
cpflow build-image -a $APP_NAME
# Run database migrations (or other release tasks) with latest image,
# while app is still running on previous image.
# This is analogous to the release phase.
cpflow run -a $APP_NAME --image latest -- rails db:migrate
# Pomote latest image to app after migrations run
cpflow deploy-image -a $APP_NAMEIf you needed to push a new image with a specific commit SHA, you can run the following command:
# Build and push with sequential image tagging and commit SHA, e.g. 'react-webpack-rails-tutorial:123_ABCD'
cpflow build-image -a $APP_NAME --commit ABCDThis application uses Thruster, a zero-config HTTP/2 proxy from Basecamp, for optimized performance on Control Plane.
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)
To enable Thruster with HTTP/2 on Control Plane, two configuration changes are required:
The Dockerfile must use Thruster to start the Rails server:
# 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.
The workload port should remain as HTTP/1.1:
ports:
- number: 3000
protocol: http # Keep as http, not http2Important: 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: http2causes a protocol mismatch and 502 errors - Thruster automatically provides HTTP/2 to end users through its TLS termination
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.
After deployment, verify HTTP/2 is working:
-
Check workload logs:
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 -
Test HTTP/2 in browser:
- Open DevTools → Network tab
- Load the site
- Check the Protocol column (should show "h2" for HTTP/2)
-
Check response headers:
curl -I https://your-app.cpln.app
Look for HTTP/2 indicators in the response.
Symptom: Workload shows as unhealthy or crashing
Solution: Check logs with cpflow logs -a <app-name>. Common issues:
- Missing
thrustergem in Gemfile - Incorrect CMD syntax in Dockerfile
- Port mismatch (ensure Rails listens on 3000)
Symptom: Workload returns 502 Bad Gateway with "protocol error"
Root Cause: Setting protocol: http2 in rails.yml causes a protocol mismatch
Solution:
- Change
protocol: http2back toprotocol: httpin.controlplane/templates/rails.yml - Apply the template:
cpflow apply-template rails -a <app-name> - 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.
Symptom: Static assets return 404 or fail to load
Solution:
- Ensure
bin/rails assets:precompileruns in Dockerfile - Verify
public/packs/directory exists in container - Check Thruster is serving from correct directory
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.
This section documents important insights gained from deploying Thruster with HTTP/2 on Control Plane.
Common Mistake: Setting protocol: http2 in the workload port configuration
Result: 502 Bad Gateway with "protocol error"
Correct Configuration: Use protocol: http
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)
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.
If you encounter 502 errors:
- Verify Thruster is running:
cpln workload exec ... -- cat /proc/1/cmdline - Test internal connectivity:
cpln workload exec ... -- curl localhost:3000 - Check protocol setting: Should be
protocol: httpnothttp2 - Review workload logs:
cpln workload eventlog <workload> --gvc <gvc> --org <org>
For detailed debugging guidance, see docs/debugging-deployment-failures.md.
The Control Plane MCP (Model Context Protocol) provides AI-assisted debugging tools that can diagnose deployment issues quickly. If you're using Claude Code or another MCP-compatible AI assistant, you can use these tools:
# Set context for your organization
mcp__cpln__set_context(org="your-org", defaultGvc="your-gvc")
# Check workload deployment status
mcp__cpln__get_workload_deployments(gvc="your-gvc", name="rails")
# View events (shows Capacity AI changes, errors)
mcp__cpln__get_workload_events(gvc="your-gvc", name="rails")
# Update workload settings (e.g., disable Capacity AI)
mcp__cpln__update_workload(gvc="your-gvc", name="rails", capacityAI=false, memory="512Mi")Symptoms: High restart count, 503 errors, "MinimumReplicasUnavailable"
Debugging steps:
- Check container logs:
cpflow logs -a <app-name> - Look for application errors (missing env vars, database issues)
- Check if Capacity AI reduced resources too low
Common causes:
- Missing
SECRET_KEY_BASE(required for Rails 8.1+) - Database connection failures
- Capacity AI memory starvation
Symptoms: Memory reduced to <100Mi, crash loop
Fix:
# Via cpflow
cpflow run -a <app-name> -- echo "test" # Verify access
# Via MCP tools
mcp__cpln__update_workload(
gvc="your-gvc",
name="rails",
capacityAI=false,
memory="512Mi",
cpu="300m"
)Rails 8.1+ requires SECRET_KEY_BASE at runtime. Add it to your GVC environment:
# Generate a secret
openssl rand -hex 64
# Add to GVC via Control Plane UI or MCP tools- waits for Postgres and Redis to be available
- runs
rails db:prepareto create/seed or migrate the database
Note, some of the URL references are internal for the ShakaCode team.
Review Apps (deployment of apps based on a PR) are done via Github Actions.
The review apps work by creating isolated deployments for each branch through this automated process. When a branch is pushed, the action:
- Sets up the necessary environment and tools
- Creates a unique deployment for that branch if it doesn't exist
- Builds a Docker image tagged with the branch's commit SHA
- Deploys this image to Control Plane with its own isolated environment
This allows teams to:
- Preview changes in a production-like environment
- Test features independently
- Share working versions with stakeholders
- Validate changes before merging to main branches
The system uses Control Plane's infrastructure to manage these deployments, with each branch getting its own resources as defined in the controlplane.yml configuration.
- Create a PR with changes to the Github Actions workflow
- Make edits to file such as
.github/actions/deploy-to-control-plane/action.yml - Run a script like
ga .github && gc -m fixes && gpto commit and push changes (ga = git add, gc = git commit, gp = git push) - Check the Github Actions tab in the PR to see the status of the workflow