One Pipeline to Guard Them All: A Practical CircleCI Guide for Rails, React, and Python Teams
One Pipeline to Guard Them All: A Practical CircleCI Guide for Rails, React, and Python Teams
Introduction
At some point in a growing engineering team, the question is no longer whether you need CI/CD. The real question becomes:
How do we build one reliable pipeline that respects the reality of our stack?
For many of us, that stack is not a clean single-language demo project. It is a real application:
- a Ruby on Rails backend
- a ReactJS frontend
- some Python modules, scripts, or services
- Minitest and RSpec for Rails
- Jest for React
- pytest and unittest for Python
This is exactly where CircleCI becomes valuable.
CircleCI is a popular CI/CD platform because it gives teams a clean way to describe build, test, and deployment automation in one place: .circleci/config.yml. That file becomes the operational contract of your repository. It defines:
- what environments your code runs in
- which jobs exist
- what each job does
- what order jobs run in
- which jobs can run in parallel
- which jobs must succeed before deployment
- which branches trigger which workflows
So this post is not just “what is CircleCI?”
This is a high-level engineering guide and a practical Rails-focused implementation guide for developers who want a serious CircleCI setup for a mixed Rails, React, and Python codebase.
What CircleCI Really Solves
At a high level, CircleCI solves a coordination problem.
Without CI/CD, every developer carries local assumptions:
- “It works on my machine.”
- “RSpec passed for me.”
- “I forgot to run Jest.”
- “The Python helper script is unrelated.”
- “We can deploy now and test later.”
In a serious application, these assumptions become expensive.
CI/CD gives you an automated system that says:
- every commit must be built in a clean environment
- every important test suite must run consistently
- failures must be visible early
- releases should be gated by evidence, not optimism
That is why CircleCI is not just a tool for automation. It is a tool for engineering discipline.
Why .circleci/config.yml Matters So Much
If CircleCI is the engine, .circleci/config.yml is the blueprint.
This file is required because CircleCI needs an explicit description of how your project should behave. In that YAML document, we usually define:
- version of the configuration syntax
- executors or Docker images used for jobs
- commands we want to reuse
- jobs such as install, test, build, and deploy
- workflows that orchestrate job order
- caches, artifacts, and workspaces
- branch filters
- optional approval gates
For a Rails developer, this matters because your pipeline usually has more than one responsibility:
- install Ruby gems
- prepare the database
- run
minitest - run
rspec - install JavaScript packages
- run
jest - install Python dependencies
- run
pytest - run
unittest - maybe precompile assets
- maybe build Docker images
- maybe deploy only from
main
If that logic is incomplete or vague, your CI becomes unreliable. A good config.yml should be boring in the best possible sense: explicit, predictable, repeatable.
Core CircleCI Concepts Every Rails Developer Should Know
Before writing the configuration, it helps to understand the building blocks.
1. Executors
An executor defines where a job runs.
In CircleCI, this is often:
- a Docker executor
- a machine executor
- or a self-hosted runner
For most web applications, the Docker executor is enough. It gives you a clean, repeatable environment for Ruby, Node, and Python jobs.
2. Jobs
A job is a unit of work.
Examples:
- install dependencies
- run Rails Minitest
- run RSpec
- run React Jest tests
- run Python tests
- precompile assets
Each job has its own steps and environment.
3. Workflows
A workflow orchestrates jobs.
This is where you define whether jobs:
- run in parallel
- depend on each other
- require manual approval
- only run on certain branches
For example, a workflow can say:
- first prepare dependencies
- then run Minitest, RSpec, Jest,
pytest, andunittestin parallel - then build release artifacts
- then deploy from
main
4. Caching
CI can become slow if every run reinstalls everything from scratch.
Caching helps you reuse:
- Bundler gems
- Node modules or package-manager caches
- Python package caches
Good caching does not remove the need for correctness, but it makes correctness faster.
5. Workspaces and Artifacts
Workspaces let jobs share files with downstream jobs.
Artifacts let CircleCI keep useful output such as:
- test results
- coverage reports
- screenshots
- build archives
In large teams, artifacts are underrated. They reduce guessing after failures.
What a Senior-Grade Pipeline Should Do
If I were setting up CircleCI for a large Rails application with React and Python modules, I would expect the pipeline to cover at least these concerns:
Source control hygiene
- run automatically on pull requests and key branches
- make failures visible before merge
Backend confidence
- install gems
- boot required services like PostgreSQL and Redis
- prepare the database
- run Rails Minitest
- run Rails RSpec
Frontend confidence
- install Node dependencies
- run Jest
- optionally run frontend build checks
Python confidence
- create a virtual environment or use project-local installs
- install Python dependencies
- run pytest
- run unittest
Release discipline
- only deploy if all required jobs pass
- gate production deploys behind the
mainbranch - optionally require manual approval for production
That is the mindset behind the configuration below.
A Practical .circleci/config.yml for Rails, React, and Python
The following example is intentionally broad and production-minded. You will still tailor paths, dependency files, and deployment commands for your application, but this gives you a strong starting point.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
version: 2.1
executors:
ruby_executor:
docker:
- image: cimg/ruby:3.3-node
environment:
RAILS_ENV: test
BUNDLE_PATH: vendor/bundle
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
DATABASE_URL: postgresql://circleci@127.0.0.1:5432/app_test
REDIS_URL: redis://127.0.0.1:6379/1
- image: cimg/postgres:16.2
environment:
POSTGRES_USER: circleci
POSTGRES_DB: app_test
POSTGRES_HOST_AUTH_METHOD: trust
- image: cimg/redis:7.2
working_directory: ~/project
node_executor:
docker:
- image: cimg/node:20.11
working_directory: ~/project
python_executor:
docker:
- image: cimg/python:3.12
working_directory: ~/project
commands:
restore_ruby_cache:
steps:
- restore_cache:
keys:
- bundle-v1-
- bundle-v1-
save_ruby_cache:
steps:
- save_cache:
key: bundle-v1-
paths:
- vendor/bundle
restore_node_cache:
steps:
- restore_cache:
keys:
- node-v1-
- node-v1-
save_node_cache:
steps:
- save_cache:
key: node-v1-
paths:
- ~/.npm
- node_modules
restore_python_cache:
steps:
- restore_cache:
keys:
- pip-v1-
- pip-v1-
save_python_cache:
steps:
- save_cache:
key: pip-v1-
paths:
- ~/.cache/pip
- venv
jobs:
setup_rails:
executor: ruby_executor
steps:
- checkout
- restore_ruby_cache
- run:
name: Install system libraries if needed
command: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- run:
name: Install Ruby gems
command: bundle install
- save_ruby_cache
- run:
name: Wait for PostgreSQL
command: dockerize -wait tcp://127.0.0.1:5432 -timeout 1m
- run:
name: Prepare Rails database
command: |
bundle exec rails db:create db:schema:load --trace
- persist_to_workspace:
root: .
paths:
- .
rails_minitest:
executor: ruby_executor
parallelism: 2
steps:
- attach_workspace:
at: ~/project
- restore_ruby_cache
- run:
name: Run Rails Minitest
command: |
mkdir -p test-results/minitest
bundle exec ruby -Itest test
- store_test_results:
path: test-results
- store_artifacts:
path: log
rails_rspec:
executor: ruby_executor
parallelism: 2
steps:
- attach_workspace:
at: ~/project
- restore_ruby_cache
- run:
name: Run RSpec
command: |
mkdir -p test-results/rspec
bundle exec rspec
- store_test_results:
path: test-results
- store_artifacts:
path: log
react_jest:
executor: node_executor
steps:
- checkout
- restore_node_cache
- run:
name: Install frontend dependencies
command: |
if [ -f yarn.lock ]; then
yarn install --frozen-lockfile
else
npm ci
fi
- save_node_cache
- run:
name: Run Jest
command: |
mkdir -p test-results/jest
if [ -f yarn.lock ]; then
yarn test --ci --watchAll=false
else
npm test -- --ci --watchAll=false
fi
- store_test_results:
path: test-results
python_pytest:
executor: python_executor
steps:
- checkout
- restore_python_cache
- run:
name: Create virtual environment
command: |
python -m venv venv
. venv/bin/activate
pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- save_python_cache
- run:
name: Run pytest
command: |
. venv/bin/activate
mkdir -p test-results/pytest
pytest
- store_test_results:
path: test-results
python_unittest:
executor: python_executor
steps:
- checkout
- restore_python_cache
- run:
name: Create virtual environment
command: |
python -m venv venv
. venv/bin/activate
pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- save_python_cache
- run:
name: Run unittest discovery
command: |
. venv/bin/activate
mkdir -p test-results/unittest
python -m unittest discover -s tests -p "test_*.py"
- store_test_results:
path: test-results
build_release:
executor: ruby_executor
steps:
- attach_workspace:
at: ~/project
- restore_ruby_cache
- run:
name: Precompile Rails assets
command: |
bundle exec rails assets:precompile
- store_artifacts:
path: public/assets
hold_production:
type: approval
deploy_production:
executor: ruby_executor
steps:
- attach_workspace:
at: ~/project
- run:
name: Deploy application
command: |
echo "Replace this step with Capistrano, Kamal, Heroku, Render, or Kubernetes deployment commands."
workflows:
version: 2
ci_pipeline:
jobs:
- setup_rails
- rails_minitest:
requires:
- setup_rails
- rails_rspec:
requires:
- setup_rails
- react_jest
- python_pytest
- python_unittest
- build_release:
requires:
- rails_minitest
- rails_rspec
- react_jest
- python_pytest
- python_unittest
filters:
branches:
only:
- main
- staging
- hold_production:
requires:
- build_release
filters:
branches:
only: main
- deploy_production:
requires:
- hold_production
filters:
branches:
only: main
How to Read This Configuration Like an Engineer
Let us break down what this file is doing.
version: 2.1
This tells CircleCI to use the modern configuration syntax.
executors
We define three execution environments:
ruby_executorfor Rails jobsnode_executorfor React jobspython_executorfor Python jobs
This is a practical choice for mixed stacks because each language ecosystem gets a clean environment instead of forcing everything into one oversized container.
commands
The reusable commands centralize cache logic.
That matters because CI files become messy very quickly when dependency installation is repeated in every job. Reusable commands keep the file maintainable.
setup_rails
This job does the heavy backend preparation:
- checks out the code
- installs Ruby gems
- waits for PostgreSQL
- prepares the test database
- persists the project into the workspace
For Rails applications, separating setup from test execution is often a good move because multiple downstream jobs can depend on the same prepared codebase.
rails_minitest and rails_rspec
Many Rails codebases evolve over time. Some teams start with Minitest, later adopt RSpec, and end up with both.
That is not unusual.
Instead of pretending only one test framework exists, the configuration treats both as first-class CI jobs. This is honest engineering. The pipeline should reflect the actual repository, not the idealized one.
react_jest
This job installs JavaScript dependencies and runs Jest in CI mode.
The logic supports both:
yarnnpm
That small flexibility matters in long-lived applications, especially when frontend package management has changed over time.
python_pytest and python_unittest
The Python section deliberately runs both frameworks.
Again, this mirrors the real world. Python code in a Rails organization may include:
- background data jobs
- ETL scripts
- analytics modules
- ML utilities
- internal tooling
Some of those were likely written with unittest, and newer ones may use pytest. Your pipeline should cover both instead of forcing a rewrite before gaining CI coverage.
build_release
This job runs only after all major test suites pass.
That is an important pattern. Build steps should usually be downstream of validation steps, especially if they produce deployable artifacts.
hold_production and deploy_production
This introduces a manual approval gate before production deployment.
That is often a healthy practice for bigger systems. Automation is powerful, but production is where business risk becomes real. A manual approval step creates a small but meaningful checkpoint.
Practical Adjustments You Will Likely Make
No sample config should be copied blindly. Here are the most common adjustments you will make in a real Rails application.
Database preparation
If your app uses schema loading, db:schema:load is fine. If it depends on migrations, seeds, or multiple databases, you may need:
1
bundle exec rails db:create db:migrate
or even:
1
bundle exec rails db:prepare
Frontend path layout
If your React app lives in a subdirectory such as frontend/, then your Jest job should cd frontend before installing packages and running tests.
Python path layout
If the Python modules live in something like services/python_tools/, adjust the working directory or test commands accordingly.
Parallelism
I set parallelism: 2 for the Rails test jobs as a reasonable placeholder.
In a larger project, you may increase this and split test files intelligently so CI stays fast as the suite grows.
Deployment commands
The deployment step is intentionally a placeholder because every team deploys differently:
- Capistrano
- Kamal
- Heroku
- Render
- ECS
- Kubernetes
The important architectural point is that deploy should be downstream of trusted validation.
What Else I Would Add in a Mature Team
Once the basic pipeline is stable, I would usually expand it with a few more safeguards.
Linting
rubocopeslintprettier --checkflake8orruff
Security scanning
bundle-auditbrakemannpm auditor a safer curated equivalent- Python dependency scanning
Coverage reporting
Coverage is not everything, but it helps teams detect when test discipline is drifting.
Scheduled workflows
Some teams also use scheduled workflows for heavier test suites, nightly builds, or dependency checks.
Self-hosted runners
If your workload requires private networking, special hardware, or custom build environments, CircleCI runners can become useful beyond standard cloud executors.
Common Mistakes in CircleCI Setups
The most common failure is not syntax. It is design.
Mistake 1: One giant job
If everything runs in one huge job, failures are harder to isolate and pipelines become slower than necessary.
Mistake 2: No caching strategy
A slow CI pipeline eventually becomes an ignored CI pipeline.
Mistake 3: Only testing the “main” framework
If your repo contains RSpec, Minitest, Jest, pytest, and unittest, then only testing one or two of them creates false confidence.
Mistake 4: Deployment not gated by test success
This is how teams accidentally automate risk instead of quality.
Mistake 5: CI configuration not evolving with the codebase
Your config.yml is not a one-time setup file. It is part of the application’s operational code and should evolve with the system.
Conclusion
CircleCI becomes truly valuable when we stop treating it as a checkbox and start treating it as part of software architecture.
For a Rails developer working in a modern codebase, .circleci/config.yml should not just “run something.” It should express the real shape of the system:
- Rails backend responsibilities
- React frontend responsibilities
- Python module responsibilities
- test frameworks across all layers
- release gates that protect production
That is why a strong CircleCI configuration feels less like a script and more like an engineering agreement. It says: this is what must be true before we trust our software.
And in a large application, that kind of clarity is not optional. It is one of the things that keeps the team moving safely.