rails_claude_skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- metadata +199 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
## Rails Deployment Examples
|
|
2
|
+
|
|
3
|
+
Complete deployment patterns and examples for production Rails applications with Kamal.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Kamal Configuration
|
|
8
|
+
|
|
9
|
+
### Basic Single-Server Setup
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# config/deploy.yml
|
|
13
|
+
service: my-blog-app
|
|
14
|
+
image: username/my-blog-app
|
|
15
|
+
|
|
16
|
+
servers:
|
|
17
|
+
web:
|
|
18
|
+
hosts:
|
|
19
|
+
- 192.168.1.100
|
|
20
|
+
labels:
|
|
21
|
+
traefik.http.routers.my-blog-app.rule: Host(`blog.example.com`)
|
|
22
|
+
traefik.http.routers.my-blog-app-secure.entrypoints: websecure
|
|
23
|
+
traefik.http.routers.my-blog-app-secure.rule: Host(`blog.example.com`)
|
|
24
|
+
traefik.http.routers.my-blog-app-secure.tls: true
|
|
25
|
+
traefik.http.routers.my-blog-app-secure.tls.certresolver: letsencrypt
|
|
26
|
+
|
|
27
|
+
registry:
|
|
28
|
+
username: username
|
|
29
|
+
password:
|
|
30
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
31
|
+
|
|
32
|
+
env:
|
|
33
|
+
secret:
|
|
34
|
+
- RAILS_MASTER_KEY
|
|
35
|
+
clear:
|
|
36
|
+
RAILS_ENV: production
|
|
37
|
+
RAILS_LOG_TO_STDOUT: enabled
|
|
38
|
+
RAILS_SERVE_STATIC_FILES: enabled
|
|
39
|
+
|
|
40
|
+
builder:
|
|
41
|
+
arch: amd64
|
|
42
|
+
remote:
|
|
43
|
+
host: 192.168.1.100
|
|
44
|
+
|
|
45
|
+
traefik:
|
|
46
|
+
options:
|
|
47
|
+
publish:
|
|
48
|
+
- 443:443
|
|
49
|
+
- 80:80
|
|
50
|
+
volume:
|
|
51
|
+
- "/letsencrypt:/letsencrypt"
|
|
52
|
+
args:
|
|
53
|
+
entryPoints.web.address: ":80"
|
|
54
|
+
entryPoints.websecure.address: ":443"
|
|
55
|
+
certificatesResolvers.letsencrypt.acme.email: "admin@example.com"
|
|
56
|
+
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
|
|
57
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge: true
|
|
58
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
|
|
59
|
+
|
|
60
|
+
healthcheck:
|
|
61
|
+
path: /up
|
|
62
|
+
port: 3000
|
|
63
|
+
max_attempts: 10
|
|
64
|
+
interval: 10s
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Multi-Server Setup with Load Balancing
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
# config/deploy.yml
|
|
71
|
+
service: my-app
|
|
72
|
+
image: username/my-app
|
|
73
|
+
|
|
74
|
+
servers:
|
|
75
|
+
web:
|
|
76
|
+
hosts:
|
|
77
|
+
- 192.168.1.101
|
|
78
|
+
- 192.168.1.102
|
|
79
|
+
- 192.168.1.103
|
|
80
|
+
labels:
|
|
81
|
+
traefik.http.routers.my-app.rule: Host(`app.example.com`)
|
|
82
|
+
traefik.http.routers.my-app-secure.entrypoints: websecure
|
|
83
|
+
traefik.http.routers.my-app-secure.rule: Host(`app.example.com`)
|
|
84
|
+
traefik.http.routers.my-app-secure.tls: true
|
|
85
|
+
traefik.http.routers.my-app-secure.tls.certresolver: letsencrypt
|
|
86
|
+
options:
|
|
87
|
+
network: "private"
|
|
88
|
+
|
|
89
|
+
# Separate worker servers for background jobs
|
|
90
|
+
workers:
|
|
91
|
+
hosts:
|
|
92
|
+
- 192.168.1.104
|
|
93
|
+
- 192.168.1.105
|
|
94
|
+
cmd: bundle exec rake solid_queue:start
|
|
95
|
+
options:
|
|
96
|
+
network: "private"
|
|
97
|
+
|
|
98
|
+
registry:
|
|
99
|
+
username: username
|
|
100
|
+
password:
|
|
101
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
102
|
+
|
|
103
|
+
env:
|
|
104
|
+
secret:
|
|
105
|
+
- RAILS_MASTER_KEY
|
|
106
|
+
- DATABASE_URL
|
|
107
|
+
clear:
|
|
108
|
+
RAILS_ENV: production
|
|
109
|
+
RAILS_LOG_TO_STDOUT: enabled
|
|
110
|
+
WEB_CONCURRENCY: 3
|
|
111
|
+
RAILS_MAX_THREADS: 5
|
|
112
|
+
|
|
113
|
+
builder:
|
|
114
|
+
arch: amd64
|
|
115
|
+
|
|
116
|
+
traefik:
|
|
117
|
+
host_port: 192.168.1.100 # Dedicated load balancer
|
|
118
|
+
options:
|
|
119
|
+
publish:
|
|
120
|
+
- 443:443
|
|
121
|
+
- 80:80
|
|
122
|
+
volume:
|
|
123
|
+
- "/letsencrypt:/letsencrypt"
|
|
124
|
+
network: "private"
|
|
125
|
+
args:
|
|
126
|
+
entryPoints.web.address: ":80"
|
|
127
|
+
entryPoints.websecure.address: ":443"
|
|
128
|
+
certificatesResolvers.letsencrypt.acme.email: "admin@example.com"
|
|
129
|
+
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
|
|
130
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge: true
|
|
131
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
|
|
132
|
+
|
|
133
|
+
healthcheck:
|
|
134
|
+
path: /up
|
|
135
|
+
port: 3000
|
|
136
|
+
max_attempts: 10
|
|
137
|
+
interval: 10s
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Staging + Production Environments
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
# config/deploy.yml (production)
|
|
144
|
+
service: my-app-production
|
|
145
|
+
image: username/my-app
|
|
146
|
+
|
|
147
|
+
servers:
|
|
148
|
+
web:
|
|
149
|
+
hosts:
|
|
150
|
+
- production.example.com
|
|
151
|
+
labels:
|
|
152
|
+
traefik.http.routers.my-app-prod.rule: Host(`app.example.com`)
|
|
153
|
+
|
|
154
|
+
env:
|
|
155
|
+
secret:
|
|
156
|
+
- RAILS_MASTER_KEY
|
|
157
|
+
clear:
|
|
158
|
+
RAILS_ENV: production
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
# config/deploy.staging.yml (staging)
|
|
163
|
+
service: my-app-staging
|
|
164
|
+
image: username/my-app
|
|
165
|
+
|
|
166
|
+
servers:
|
|
167
|
+
web:
|
|
168
|
+
hosts:
|
|
169
|
+
- staging.example.com
|
|
170
|
+
labels:
|
|
171
|
+
traefik.http.routers.my-app-staging.rule: Host(`staging.app.example.com`)
|
|
172
|
+
|
|
173
|
+
env:
|
|
174
|
+
secret:
|
|
175
|
+
- RAILS_MASTER_KEY
|
|
176
|
+
clear:
|
|
177
|
+
RAILS_ENV: staging
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Deploy to staging
|
|
182
|
+
kamal deploy -c config/deploy.staging.yml
|
|
183
|
+
|
|
184
|
+
# Deploy to production
|
|
185
|
+
kamal deploy -c config/deploy.yml
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Docker Configuration
|
|
191
|
+
|
|
192
|
+
### Optimized Multi-Stage Dockerfile
|
|
193
|
+
|
|
194
|
+
```dockerfile
|
|
195
|
+
# syntax = docker/dockerfile:1
|
|
196
|
+
|
|
197
|
+
ARG RUBY_VERSION=3.4.7
|
|
198
|
+
FROM ruby:$RUBY_VERSION-slim AS base
|
|
199
|
+
|
|
200
|
+
# Set working directory
|
|
201
|
+
WORKDIR /rails
|
|
202
|
+
|
|
203
|
+
# Install base packages
|
|
204
|
+
RUN apt-get update -qq && \
|
|
205
|
+
apt-get install --no-install-recommends -y \
|
|
206
|
+
curl \
|
|
207
|
+
libjemalloc2 \
|
|
208
|
+
libvips \
|
|
209
|
+
sqlite3 && \
|
|
210
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
211
|
+
|
|
212
|
+
# Set production environment
|
|
213
|
+
ENV RAILS_ENV="production" \
|
|
214
|
+
BUNDLE_DEPLOYMENT="1" \
|
|
215
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
|
216
|
+
BUNDLE_WITHOUT="development:test" \
|
|
217
|
+
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000"
|
|
218
|
+
|
|
219
|
+
# Build stage
|
|
220
|
+
FROM base AS build
|
|
221
|
+
|
|
222
|
+
# Install build packages
|
|
223
|
+
RUN apt-get update -qq && \
|
|
224
|
+
apt-get install --no-install-recommends -y \
|
|
225
|
+
build-essential \
|
|
226
|
+
git \
|
|
227
|
+
pkg-config && \
|
|
228
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
229
|
+
|
|
230
|
+
# Install gems
|
|
231
|
+
COPY Gemfile Gemfile.lock ./
|
|
232
|
+
RUN bundle install && \
|
|
233
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
|
234
|
+
bundle exec bootsnap precompile --gemfile
|
|
235
|
+
|
|
236
|
+
# Copy application code
|
|
237
|
+
COPY . .
|
|
238
|
+
|
|
239
|
+
# Precompile bootsnap code for faster boot times
|
|
240
|
+
RUN bundle exec bootsnap precompile app/ lib/
|
|
241
|
+
|
|
242
|
+
# Precompile assets
|
|
243
|
+
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
|
244
|
+
|
|
245
|
+
# Final stage
|
|
246
|
+
FROM base
|
|
247
|
+
|
|
248
|
+
# Copy built artifacts
|
|
249
|
+
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
|
250
|
+
COPY --from=build /rails /rails
|
|
251
|
+
|
|
252
|
+
# Create user
|
|
253
|
+
RUN groupadd --system --gid 1000 rails && \
|
|
254
|
+
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
|
255
|
+
chown -R rails:rails db log storage tmp
|
|
256
|
+
USER 1000:1000
|
|
257
|
+
|
|
258
|
+
# Entrypoint
|
|
259
|
+
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
|
260
|
+
|
|
261
|
+
EXPOSE 3000
|
|
262
|
+
CMD ["./bin/thrust", "./bin/rails", "server"]
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Docker Entrypoint Script
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
#!/bin/bash -e
|
|
269
|
+
# bin/docker-entrypoint
|
|
270
|
+
|
|
271
|
+
# Remove pre-existing server.pid
|
|
272
|
+
rm -f /rails/tmp/pids/server.pid
|
|
273
|
+
|
|
274
|
+
# Run migrations if DATABASE_URL is set
|
|
275
|
+
if [ -n "$DATABASE_URL" ]; then
|
|
276
|
+
echo "Running database migrations..."
|
|
277
|
+
bundle exec rails db:prepare
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Execute CMD
|
|
281
|
+
exec "${@}"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Make executable
|
|
286
|
+
chmod +x bin/docker-entrypoint
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### .dockerignore
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
# .dockerignore
|
|
293
|
+
.git
|
|
294
|
+
.github
|
|
295
|
+
.gitignore
|
|
296
|
+
.dockerignore
|
|
297
|
+
|
|
298
|
+
# Development/Test
|
|
299
|
+
.env*
|
|
300
|
+
!.env.example
|
|
301
|
+
coverage/
|
|
302
|
+
log/*
|
|
303
|
+
tmp/*
|
|
304
|
+
*.log
|
|
305
|
+
*.pid
|
|
306
|
+
|
|
307
|
+
# Documentation
|
|
308
|
+
README.md
|
|
309
|
+
CHANGELOG.md
|
|
310
|
+
doc/
|
|
311
|
+
|
|
312
|
+
# Dependencies
|
|
313
|
+
node_modules/
|
|
314
|
+
vendor/bundle/
|
|
315
|
+
|
|
316
|
+
# Build artifacts
|
|
317
|
+
public/assets/
|
|
318
|
+
public/packs/
|
|
319
|
+
|
|
320
|
+
# IDE
|
|
321
|
+
.idea/
|
|
322
|
+
.vscode/
|
|
323
|
+
*.swp
|
|
324
|
+
*.swo
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Environment Secrets
|
|
330
|
+
|
|
331
|
+
### Using Rails Credentials
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# Edit production credentials
|
|
335
|
+
EDITOR=nano rails credentials:edit --environment production
|
|
336
|
+
|
|
337
|
+
# Save to config/credentials/production.key (keep secure, don't commit)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```yaml
|
|
341
|
+
# config/credentials/production.yml.enc (encrypted, safe to commit)
|
|
342
|
+
secret_key_base: abc123...
|
|
343
|
+
|
|
344
|
+
database:
|
|
345
|
+
host: db.example.com
|
|
346
|
+
username: myapp_prod
|
|
347
|
+
password: secure_password
|
|
348
|
+
|
|
349
|
+
smtp:
|
|
350
|
+
address: smtp.sendgrid.net
|
|
351
|
+
username: apikey
|
|
352
|
+
password: SG.xxx
|
|
353
|
+
|
|
354
|
+
aws:
|
|
355
|
+
access_key_id: AKIA...
|
|
356
|
+
secret_access_key: xxx
|
|
357
|
+
|
|
358
|
+
stripe:
|
|
359
|
+
publishable_key: pk_live_...
|
|
360
|
+
secret_key: sk_live_...
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
# config/database.yml
|
|
365
|
+
production:
|
|
366
|
+
<<: *default
|
|
367
|
+
host: <%= Rails.application.credentials.dig(:database, :host) %>
|
|
368
|
+
database: <%= ENV.fetch("DATABASE_NAME", "myapp_production") %>
|
|
369
|
+
username: <%= Rails.application.credentials.dig(:database, :username) %>
|
|
370
|
+
password: <%= Rails.application.credentials.dig(:database, :password) %>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Using Environment Variables
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# .env (local only, not committed)
|
|
377
|
+
KAMAL_REGISTRY_PASSWORD=docker_hub_password
|
|
378
|
+
RAILS_MASTER_KEY=abc123...
|
|
379
|
+
DATABASE_URL=postgresql://user:pass@host:5432/dbname
|
|
380
|
+
SMTP_USERNAME=apikey
|
|
381
|
+
SMTP_PASSWORD=sendgrid_key
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
# config/deploy.yml
|
|
386
|
+
env:
|
|
387
|
+
secret:
|
|
388
|
+
- RAILS_MASTER_KEY
|
|
389
|
+
- DATABASE_URL
|
|
390
|
+
- SMTP_USERNAME
|
|
391
|
+
- SMTP_PASSWORD
|
|
392
|
+
clear:
|
|
393
|
+
RAILS_ENV: production
|
|
394
|
+
RAILS_LOG_TO_STDOUT: enabled
|
|
395
|
+
RAILS_SERVE_STATIC_FILES: enabled
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Database Deployment
|
|
401
|
+
|
|
402
|
+
### PostgreSQL with External Database
|
|
403
|
+
|
|
404
|
+
```yaml
|
|
405
|
+
# config/deploy.yml
|
|
406
|
+
env:
|
|
407
|
+
secret:
|
|
408
|
+
- DATABASE_URL # postgresql://user:pass@host:5432/dbname
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# config/database.yml
|
|
413
|
+
production:
|
|
414
|
+
<<: *default
|
|
415
|
+
url: <%= ENV["DATABASE_URL"] %>
|
|
416
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### SQLite with Docker Volumes
|
|
420
|
+
|
|
421
|
+
```yaml
|
|
422
|
+
# config/deploy.yml
|
|
423
|
+
volumes:
|
|
424
|
+
- "/var/lib/myapp/storage:/rails/storage"
|
|
425
|
+
- "/var/lib/myapp/db:/rails/db"
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
# config/database.yml
|
|
430
|
+
production:
|
|
431
|
+
<<: *default
|
|
432
|
+
database: db/production.sqlite3
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Safe Migration Strategy
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
# 1. Deploy code without pushing (uses existing image)
|
|
439
|
+
kamal deploy --skip-push
|
|
440
|
+
|
|
441
|
+
# 2. Check migration status
|
|
442
|
+
kamal app exec "bin/rails db:migrate:status"
|
|
443
|
+
|
|
444
|
+
# 3. Run migrations
|
|
445
|
+
kamal app exec "bin/rails db:migrate"
|
|
446
|
+
|
|
447
|
+
# 4. Verify success
|
|
448
|
+
kamal app exec "bin/rails db:version"
|
|
449
|
+
|
|
450
|
+
# 5. If issues, rollback migration
|
|
451
|
+
kamal app exec "bin/rails db:rollback"
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Pre-Deployment Migration Hook
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
# config/deploy.yml
|
|
458
|
+
hooks:
|
|
459
|
+
pre-deploy:
|
|
460
|
+
- path: config/deploy/hooks/pre-deploy.sh
|
|
461
|
+
on_error: abort
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# config/deploy/hooks/pre-deploy.sh
|
|
466
|
+
#!/bin/bash
|
|
467
|
+
set -e
|
|
468
|
+
|
|
469
|
+
echo "Running database migrations..."
|
|
470
|
+
kamal app exec "bin/rails db:migrate"
|
|
471
|
+
|
|
472
|
+
echo "Migrations complete!"
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## SSL/TLS Configuration
|
|
478
|
+
|
|
479
|
+
### Let's Encrypt (Automatic)
|
|
480
|
+
|
|
481
|
+
```yaml
|
|
482
|
+
# config/deploy.yml
|
|
483
|
+
traefik:
|
|
484
|
+
options:
|
|
485
|
+
publish:
|
|
486
|
+
- 443:443
|
|
487
|
+
- 80:80
|
|
488
|
+
volume:
|
|
489
|
+
- "/letsencrypt:/letsencrypt"
|
|
490
|
+
args:
|
|
491
|
+
entryPoints.web.address: ":80"
|
|
492
|
+
entryPoints.web.http.redirections.entryPoint.to: websecure
|
|
493
|
+
entryPoints.web.http.redirections.entryPoint.scheme: https
|
|
494
|
+
entryPoints.websecure.address: ":443"
|
|
495
|
+
certificatesResolvers.letsencrypt.acme.email: "admin@example.com"
|
|
496
|
+
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
|
|
497
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge: true
|
|
498
|
+
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
|
|
499
|
+
|
|
500
|
+
servers:
|
|
501
|
+
web:
|
|
502
|
+
labels:
|
|
503
|
+
traefik.http.routers.myapp-secure.tls.certresolver: letsencrypt
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Custom SSL Certificate
|
|
507
|
+
|
|
508
|
+
```yaml
|
|
509
|
+
# config/deploy.yml
|
|
510
|
+
traefik:
|
|
511
|
+
options:
|
|
512
|
+
publish:
|
|
513
|
+
- 443:443
|
|
514
|
+
- 80:80
|
|
515
|
+
volume:
|
|
516
|
+
- "/etc/ssl/certs:/certs:ro"
|
|
517
|
+
args:
|
|
518
|
+
entryPoints.web.address: ":80"
|
|
519
|
+
entryPoints.websecure.address: ":443"
|
|
520
|
+
providers.file.filename: /etc/traefik/tls.yml
|
|
521
|
+
|
|
522
|
+
# Upload certificates to server
|
|
523
|
+
# /etc/traefik/tls.yml:
|
|
524
|
+
# tls:
|
|
525
|
+
# certificates:
|
|
526
|
+
# - certFile: /certs/example.com.crt
|
|
527
|
+
# keyFile: /certs/example.com.key
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Health Checks
|
|
533
|
+
|
|
534
|
+
### Health Check Endpoint
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
# config/routes.rb
|
|
538
|
+
Rails.application.routes.draw do
|
|
539
|
+
get "up", to: "health#show", as: :health_check
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# app/controllers/health_controller.rb
|
|
543
|
+
class HealthController < ApplicationController
|
|
544
|
+
skip_before_action :authenticate_user!
|
|
545
|
+
|
|
546
|
+
def show
|
|
547
|
+
# Check database connectivity
|
|
548
|
+
ActiveRecord::Base.connection.execute("SELECT 1")
|
|
549
|
+
|
|
550
|
+
# Check Solid Queue
|
|
551
|
+
SolidQueue::Job.count
|
|
552
|
+
|
|
553
|
+
# All checks passed
|
|
554
|
+
render json: {
|
|
555
|
+
status: "ok",
|
|
556
|
+
timestamp: Time.current,
|
|
557
|
+
version: ENV["APP_VERSION"]
|
|
558
|
+
}, status: :ok
|
|
559
|
+
rescue StandardError => e
|
|
560
|
+
render json: {
|
|
561
|
+
status: "error",
|
|
562
|
+
error: e.message
|
|
563
|
+
}, status: :service_unavailable
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
```yaml
|
|
569
|
+
# config/deploy.yml
|
|
570
|
+
healthcheck:
|
|
571
|
+
path: /up
|
|
572
|
+
port: 3000
|
|
573
|
+
max_attempts: 10
|
|
574
|
+
interval: 10s
|
|
575
|
+
timeout: 5s
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Background Jobs Deployment
|
|
581
|
+
|
|
582
|
+
### Solid Queue Workers
|
|
583
|
+
|
|
584
|
+
```yaml
|
|
585
|
+
# config/deploy.yml
|
|
586
|
+
servers:
|
|
587
|
+
web:
|
|
588
|
+
hosts:
|
|
589
|
+
- web1.example.com
|
|
590
|
+
- web2.example.com
|
|
591
|
+
|
|
592
|
+
workers:
|
|
593
|
+
hosts:
|
|
594
|
+
- worker1.example.com
|
|
595
|
+
- worker2.example.com
|
|
596
|
+
cmd: bundle exec rake solid_queue:start
|
|
597
|
+
options:
|
|
598
|
+
network: "private"
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
# config/recurring.yml (Solid Queue recurring tasks)
|
|
603
|
+
production:
|
|
604
|
+
cleanup_old_jobs:
|
|
605
|
+
class: CleanupJob
|
|
606
|
+
schedule: "0 2 * * *" # Daily at 2 AM
|
|
607
|
+
queue: maintenance
|
|
608
|
+
|
|
609
|
+
send_daily_digest:
|
|
610
|
+
class: DailyDigestJob
|
|
611
|
+
schedule: "0 8 * * *" # Daily at 8 AM
|
|
612
|
+
queue: mailers
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Rollback Procedures
|
|
618
|
+
|
|
619
|
+
### Rolling Back Deployment
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
# List deployed versions
|
|
623
|
+
kamal app version
|
|
624
|
+
|
|
625
|
+
# Rollback to previous version
|
|
626
|
+
kamal rollback <VERSION>
|
|
627
|
+
|
|
628
|
+
# Example
|
|
629
|
+
kamal rollback 20240115123456
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Database Rollback
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
# Check migration status
|
|
636
|
+
kamal app exec "bin/rails db:migrate:status"
|
|
637
|
+
|
|
638
|
+
# Rollback last migration
|
|
639
|
+
kamal app exec "bin/rails db:rollback"
|
|
640
|
+
|
|
641
|
+
# Rollback multiple migrations
|
|
642
|
+
kamal app exec "bin/rails db:rollback STEP=3"
|
|
643
|
+
|
|
644
|
+
# Rollback to specific version
|
|
645
|
+
kamal app exec "bin/rails db:migrate:down VERSION=20240115123456"
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Monitoring & Logging
|
|
651
|
+
|
|
652
|
+
### Structured Logging with Lograge
|
|
653
|
+
|
|
654
|
+
```ruby
|
|
655
|
+
# Gemfile
|
|
656
|
+
gem "lograge"
|
|
657
|
+
|
|
658
|
+
# config/environments/production.rb
|
|
659
|
+
Rails.application.configure do
|
|
660
|
+
# Use Lograge for structured logs
|
|
661
|
+
config.lograge.enabled = true
|
|
662
|
+
config.lograge.formatter = Lograge::Formatters::Json.new
|
|
663
|
+
|
|
664
|
+
config.lograge.custom_options = lambda do |event|
|
|
665
|
+
{
|
|
666
|
+
params: event.payload[:params].except("controller", "action"),
|
|
667
|
+
user_id: event.payload[:user_id],
|
|
668
|
+
ip: event.payload[:ip],
|
|
669
|
+
request_id: event.payload[:request_id]
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Viewing Logs
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
# Tail logs from all web servers
|
|
679
|
+
kamal app logs -f
|
|
680
|
+
|
|
681
|
+
# Tail logs from specific server
|
|
682
|
+
kamal app logs -f --hosts 192.168.1.101
|
|
683
|
+
|
|
684
|
+
# View last 100 lines
|
|
685
|
+
kamal app logs --lines 100
|
|
686
|
+
|
|
687
|
+
# Filter logs
|
|
688
|
+
kamal app logs -f | grep ERROR
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Error Tracking with Sentry
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
# Gemfile
|
|
695
|
+
gem "sentry-ruby"
|
|
696
|
+
gem "sentry-rails"
|
|
697
|
+
|
|
698
|
+
# config/initializers/sentry.rb
|
|
699
|
+
Sentry.init do |config|
|
|
700
|
+
config.dsn = Rails.application.credentials.dig(:sentry, :dsn)
|
|
701
|
+
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
|
|
702
|
+
config.traces_sample_rate = 0.1
|
|
703
|
+
config.environment = Rails.env
|
|
704
|
+
end
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## CI/CD with GitHub Actions
|
|
710
|
+
|
|
711
|
+
### Automated Deployment Workflow
|
|
712
|
+
|
|
713
|
+
```yaml
|
|
714
|
+
# .github/workflows/deploy.yml
|
|
715
|
+
name: Deploy
|
|
716
|
+
|
|
717
|
+
on:
|
|
718
|
+
push:
|
|
719
|
+
branches: [main]
|
|
720
|
+
|
|
721
|
+
jobs:
|
|
722
|
+
test:
|
|
723
|
+
runs-on: ubuntu-latest
|
|
724
|
+
steps:
|
|
725
|
+
- uses: actions/checkout@v4
|
|
726
|
+
|
|
727
|
+
- name: Set up Ruby
|
|
728
|
+
uses: ruby/setup-ruby@v1
|
|
729
|
+
with:
|
|
730
|
+
bundler-cache: true
|
|
731
|
+
|
|
732
|
+
- name: Run tests
|
|
733
|
+
run: |
|
|
734
|
+
bin/rails test
|
|
735
|
+
bin/rubocop
|
|
736
|
+
|
|
737
|
+
deploy:
|
|
738
|
+
needs: test
|
|
739
|
+
runs-on: ubuntu-latest
|
|
740
|
+
steps:
|
|
741
|
+
- uses: actions/checkout@v4
|
|
742
|
+
|
|
743
|
+
- name: Set up Ruby
|
|
744
|
+
uses: ruby/setup-ruby@v1
|
|
745
|
+
with:
|
|
746
|
+
bundler-cache: true
|
|
747
|
+
|
|
748
|
+
- name: Install Kamal
|
|
749
|
+
run: gem install kamal
|
|
750
|
+
|
|
751
|
+
- name: Setup SSH
|
|
752
|
+
uses: webfactory/ssh-agent@v0.8.0
|
|
753
|
+
with:
|
|
754
|
+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
755
|
+
|
|
756
|
+
- name: Deploy with Kamal
|
|
757
|
+
env:
|
|
758
|
+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
759
|
+
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
|
760
|
+
run: |
|
|
761
|
+
kamal deploy
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Manual Deployment Workflow
|
|
765
|
+
|
|
766
|
+
```yaml
|
|
767
|
+
# .github/workflows/manual-deploy.yml
|
|
768
|
+
name: Manual Deploy
|
|
769
|
+
|
|
770
|
+
on:
|
|
771
|
+
workflow_dispatch:
|
|
772
|
+
inputs:
|
|
773
|
+
environment:
|
|
774
|
+
description: "Environment to deploy"
|
|
775
|
+
required: true
|
|
776
|
+
type: choice
|
|
777
|
+
options:
|
|
778
|
+
- staging
|
|
779
|
+
- production
|
|
780
|
+
|
|
781
|
+
jobs:
|
|
782
|
+
deploy:
|
|
783
|
+
runs-on: ubuntu-latest
|
|
784
|
+
steps:
|
|
785
|
+
- uses: actions/checkout@v4
|
|
786
|
+
|
|
787
|
+
- name: Deploy to ${{ inputs.environment }}
|
|
788
|
+
run: |
|
|
789
|
+
if [ "${{ inputs.environment }}" = "staging" ]; then
|
|
790
|
+
kamal deploy -c config/deploy.staging.yml
|
|
791
|
+
else
|
|
792
|
+
kamal deploy -c config/deploy.yml
|
|
793
|
+
fi
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Performance Optimization
|
|
799
|
+
|
|
800
|
+
### Docker Image Optimization
|
|
801
|
+
|
|
802
|
+
```dockerfile
|
|
803
|
+
# Use specific Ruby version
|
|
804
|
+
ARG RUBY_VERSION=3.4.7
|
|
805
|
+
FROM ruby:$RUBY_VERSION-slim
|
|
806
|
+
|
|
807
|
+
# Reduce layer count with multi-command RUN
|
|
808
|
+
RUN apt-get update -qq && \
|
|
809
|
+
apt-get install --no-install-recommends -y pkg1 pkg2 && \
|
|
810
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
811
|
+
|
|
812
|
+
# Copy only what's needed
|
|
813
|
+
COPY Gemfile Gemfile.lock ./
|
|
814
|
+
RUN bundle install
|
|
815
|
+
|
|
816
|
+
# Use .dockerignore to exclude unnecessary files
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Production Configuration
|
|
820
|
+
|
|
821
|
+
```ruby
|
|
822
|
+
# config/environments/production.rb
|
|
823
|
+
Rails.application.configure do
|
|
824
|
+
# Serve assets via CDN
|
|
825
|
+
config.asset_host = ENV["CDN_HOST"]
|
|
826
|
+
|
|
827
|
+
# Enable caching
|
|
828
|
+
config.cache_classes = true
|
|
829
|
+
config.eager_load = true
|
|
830
|
+
config.consider_all_requests_local = false
|
|
831
|
+
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
|
|
832
|
+
|
|
833
|
+
# Compress CSS
|
|
834
|
+
config.assets.css_compressor = :sass
|
|
835
|
+
|
|
836
|
+
# Enable Rack::Deflate for gzip compression
|
|
837
|
+
config.middleware.use Rack::Deflate
|
|
838
|
+
|
|
839
|
+
# Use SolidCache
|
|
840
|
+
config.cache_store = :solid_cache_store
|
|
841
|
+
|
|
842
|
+
# Active Job queue adapter
|
|
843
|
+
config.active_job.queue_adapter = :solid_queue
|
|
844
|
+
|
|
845
|
+
# Logging
|
|
846
|
+
config.log_level = :info
|
|
847
|
+
config.log_tags = [:request_id]
|
|
848
|
+
end
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Resource Limits
|
|
852
|
+
|
|
853
|
+
```yaml
|
|
854
|
+
# config/deploy.yml
|
|
855
|
+
env:
|
|
856
|
+
clear:
|
|
857
|
+
WEB_CONCURRENCY: 3 # Number of Puma workers
|
|
858
|
+
RAILS_MAX_THREADS: 5 # Threads per worker
|
|
859
|
+
MALLOC_ARENA_MAX: 2 # Reduce memory fragmentation
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## Troubleshooting
|
|
865
|
+
|
|
866
|
+
### Common Deployment Issues
|
|
867
|
+
|
|
868
|
+
```bash
|
|
869
|
+
# Container not starting
|
|
870
|
+
kamal app logs -f
|
|
871
|
+
kamal app containers
|
|
872
|
+
|
|
873
|
+
# Check server resources
|
|
874
|
+
kamal app exec "free -h"
|
|
875
|
+
kamal app exec "df -h"
|
|
876
|
+
|
|
877
|
+
# Database connection issues
|
|
878
|
+
kamal app exec "bin/rails db:version"
|
|
879
|
+
kamal app exec "bin/rails console"
|
|
880
|
+
|
|
881
|
+
# Restart services
|
|
882
|
+
kamal app restart
|
|
883
|
+
kamal traefik restart
|
|
884
|
+
|
|
885
|
+
# Complete cleanup and redeploy
|
|
886
|
+
kamal app remove
|
|
887
|
+
kamal deploy
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### Debug Mode
|
|
891
|
+
|
|
892
|
+
```bash
|
|
893
|
+
# Enable verbose logging
|
|
894
|
+
kamal deploy --verbose
|
|
895
|
+
|
|
896
|
+
# SSH into container
|
|
897
|
+
kamal app exec --interactive bash
|
|
898
|
+
|
|
899
|
+
# Check environment variables
|
|
900
|
+
kamal app exec "env | grep RAILS"
|
|
901
|
+
```
|