This guide hardens RexOne Core's current Coolify, Traefik, Rails, Rack Attack, Garage, and worker deployment. Apply changes gradually in UAT, observe normal traffic, and then promote the same configuration to production.
No application setting makes a service "DDoS proof." Rack Attack protects Rails from endpoint abuse and brute-force traffic that reaches the process. Cloudflare and the origin firewall must reject volumetric traffic before it consumes VPS bandwidth, sockets, memory, or Puma threads.
Use four defensive layers:
Internet
↓
Cloudflare — absorbs network/HTTP floods, WAF, bots, edge rate limits
↓
Coolify/Traefik — connection limits and request-size limits
↓
Rack Attack — endpoint-specific and account-aware limits
↓
Rails/Puma — bounded threads, queues, timeouts, database pools
Add your domain to Cloudflare and ensure these DNS records are proxied—the orange cloud:
app.example.com→ RexOne Webapi.example.com→ RexOne Core- Your public Garage download hostname, if appropriate
Do not proxy Garage’s administration endpoint or PostgreSQL.
Cloudflare’s managed DDoS protection is automatic and detects network and HTTP attack patterns before traffic reaches your server. Cloudflare DDoS documentation
Use:
- SSL/TLS mode:
Full (strict) - Always Use HTTPS: enabled
- Minimum TLS: 1.2
- HTTP DDoS managed rules: enabled
- Cloudflare managed WAF rules: enabled if included in your plan
This is the most important part. If attackers discover the VPS IP and port 3000, they can bypass Cloudflare completely.
Your production Compose currently publishes Rails publicly:
ports:
- "${RAILS_PORT_MAP:-3000:3000}"For Coolify, Rails should normally be reachable only through its internal Docker network and Traefik. Prefer:
expose:
- "3000"Do the same for Garage unless its raw S3 endpoint genuinely needs direct public access.
At the VPS firewall:
- Allow inbound
80and443. - Allow SSH only from trusted IPs or through a VPN.
- Do not expose
3000,3100,3101, or5432. - Keep Garage admin port
3101private. - Keep PostgreSQL entirely private.
For stronger origin lockdown, allow HTTP/HTTPS traffic only from Cloudflare IP ranges or use Cloudflare Tunnel. Cloudflare also recommends ensuring the origin cannot be accessed directly. Cloudflare proactive defense
Be careful: restricting ports 80/443 to Cloudflare requires an automated process to keep Cloudflare IP ranges current.
From a machine outside the VPS network:
# The Cloudflare hostname should succeed.
curl -I https://api.example.com/up
# Direct access to the origin application port should time out or be refused.
curl --connect-timeout 5 http://ORIGIN_IP:3000/upDo not put credentials, tokens, or signed Garage URLs in shell history while performing these checks.
Create rules under Security → WAF → Rate limiting rules.
Start conservatively and observe real usage before blocking aggressively.
Expression:
(http.host eq "api.example.com"
and http.request.method eq "POST"
and http.request.uri.path eq "/signin")
Suggested limit:
- 10 requests per minute per IP
- Mitigation: Managed Challenge
- Duration: 10 minutes
Rack Attack already allows 10 per five minutes. The Cloudflare limit stops excess requests before they consume Rails resources.
Expression:
(http.host eq "api.example.com"
and http.request.method in {"POST" "PUT"}
and (
starts_with(http.request.uri.path, "/signup")
or starts_with(http.request.uri.path, "/password")
or starts_with(http.request.uri.path, "/confirmation")
))
Suggested limit:
- 20 requests per minute per IP
- Managed Challenge or block
- Duration: 10 minutes
POST /v1/client/logs is unauthenticated, so attackers could use it to fill storage or consume database capacity.
Suggested limit:
- 30 requests per minute per IP
- Burst allowance if available
- Block for 10 minutes
These are expensive and should have stricter account-based server limits in addition to IP limits:
/v1/ai/*: perhaps 30 requests/minute/v1/speech/*: perhaps 10 requests/minute- Enforce user quotas inside Rails—not solely by IP, because many legitimate mobile users can share one carrier IP.
Cloudflare documents rate-limit rules specifically for login and API abuse prevention. Cloudflare rate-limiting rules
- Create one rule at a time.
- Use logging or Managed Challenge before a hard block when the plan supports it.
- Observe Security Events and origin
429rates for at least one normal traffic cycle. - Exclude trusted health monitors only when they use stable, controlled addresses.
- Promote the thresholds to production and document every exception.
Do not exempt requests based on a client-supplied header unless Cloudflare or another trusted proxy removes incoming copies and sets the header itself.
Traefik supports token-bucket rate limiting, maximum in-flight requests, and request-body limits. Rate limit, in-flight requests, request buffering
Conceptually, your API service needs middleware equivalent to:
labels:
- "traefik.http.middlewares.rexone-api-rate.ratelimit.average=50"
- "traefik.http.middlewares.rexone-api-rate.ratelimit.period=1s"
- "traefik.http.middlewares.rexone-api-rate.ratelimit.burst=100"
- "traefik.http.middlewares.rexone-api-inflight.inflightreq.amount=100"
- "traefik.http.middlewares.rexone-api-body.buffering.maxRequestBodyBytes=5242880"
- "traefik.http.middlewares.rexone-api-body.buffering.memRequestBodyBytes=1048576"Then attach them to the Coolify-generated API router:
- "traefik.http.routers.<coolify-api-router>.middlewares=rexone-api-rate,rexone-api-inflight,rexone-api-body"The exact router name is generated by Coolify, so inspect the deployed container’s Traefik labels before setting this.
Before editing labels, record the current router name and middleware chain so the change can be rolled back. After deployment, verify that ordinary API calls, WebSocket upgrades, uploads, and health checks still work.
Important: a global 5 MB body limit would break media uploads. Ideally use two routers:
- General API: 1–5 MB maximum
/v1/assets/uploadand admin asset uploads: slightly above your actual maximum supported media size
Your videos currently permit large files, so avoid a buffering middleware that makes Traefik spool hundreds of megabytes through memory/disk. The best long-term design is direct presigned uploads to Garage, followed by a lightweight Rails request that registers the completed asset.
Once Cloudflare and Traefik are in front, verify that req.ip is the actual client IP—not Cloudflare or Traefik’s IP.
If every request appears to originate from one proxy IP, the first ten sign-in attempts across all users could throttle everyone.
Rails must trust only your actual proxy network or known Cloudflare ranges. Never trust arbitrary X-Forwarded-For headers from the open Internet.
Test production headers by logging these temporarily:
Rails.logger.info(
remote_ip: request.remote_ip,
forwarded_for: request.headers["X-Forwarded-For"],
cf_connecting_ip: request.headers["CF-Connecting-IP"]
)Then make a normal request through Cloudflare and confirm request.remote_ip is your client address.
Remove temporary IP logging after verification. IP addresses are operational data and should not be retained indefinitely without a defined purpose and retention policy.
Useful server-side limits:
POST /v1/client/logs: per IPPOST /v1/feedbacks: per user and IP/v1/ai/*: per authenticated user/v1/speech/*: per authenticated user- Media upload initialization: per user
- Stripe webhook: do not apply a simplistic IP throttle; authenticate using Stripe signatures
/up: allow monitoring, but stop extremely high request rates
Use separate limits for:
- Anonymous IP
- Authenticated user ID
- Expensive endpoint
- General API fallback
Do not use only a broad global per-IP limit. Mobile carrier NAT, offices, monasteries, schools, and VPNs may place many legitimate users behind one address.
For your current setup:
- Keep
RAILS_MAX_THREADSandAPI_DB_POOLequal. - Start with one Puma process and 5–7 threads on a small VPS.
- Give API, normal jobs, and media workers separate CPU/memory limits.
- Keep media work in the dedicated
mediaworker. - Add timeouts around Stripe, Brevo, OneSignal, DeepSeek, Garage, and other outbound calls.
- Limit queue concurrency so a flood cannot start unlimited FFmpeg processes.
- Alert on elevated
429,413,5xx, queue depth, memory, CPU, and database connections.
Never perform load or flood testing against production without explicit provider approval. Cloudflare, the VPS provider, or adjacent tenants may interpret it as an attack.
Use UAT and test one control at a time:
# Core regression coverage for the Rack Attack rules.
docker compose -f docker-compose.dev.yaml exec api \
bundle exec rspec spec/requests/rack_attack_spec.rbFor external testing, send only enough requests to cross the configured UAT threshold. Confirm:
- Cloudflare or Traefik returns
429before the request reaches Rails. - Rack Attack returns its JSON error envelope and
Retry-Afterheader when the edge deliberately allows the test through. - A different test IP is not accidentally sharing the same rate-limit bucket.
- Mobile clients handle
429without retry loops. - Uploads above the configured limit return
413without consuming a Rails worker. - Action Cable connections and normal API traffic remain healthy.
Create alerts for unusual changes in:
- Cloudflare challenged and blocked requests.
- Traefik
429and413responses. - Rails
429,5xx, latency, and Puma saturation. - Database connections and slow queries.
- Solid Queue depth, failed jobs, and retry volume.
- API, worker, media-worker, and Garage CPU, memory, disk, and network usage.
During an incident:
- Confirm whether traffic reaches Cloudflare, Traefik, or Rails.
- Preserve a short sample of request metadata without recording secrets.
- Add the narrowest effective edge rule for the observed path or fingerprint.
- Reduce expensive endpoint concurrency if the application is saturated.
- Avoid broad country or ASN blocks unless evidence supports them.
- Review false positives and remove temporary emergency rules after traffic normalizes.
If legitimate users are blocked:
- Disable the newest Cloudflare rule or switch it from Block to Log/Challenge.
- Detach the newest Traefik middleware from the router before deleting its definition.
- Restore the previous body-size and in-flight limits.
- Confirm Rails receives the correct client IP.
- Retest sign-in, password recovery, uploads, AI, speech, and sockets.
Do these first:
- Proxy the API through Cloudflare.
- Remove direct public access to ports
3000,3101, and5432. - Confirm Rails sees the real client IP.
- Add Cloudflare rules for sign-in, password, client logs, AI, and speech.
- Add Traefik in-flight and body-size limits.
- Add user-based Rack Attack limits for expensive endpoints.
- Move large uploads to presigned direct-to-Garage uploads.
The biggest immediate risk in the current Compose configuration is the public 3000:3000 port mapping. If that port is externally reachable, Cloudflare protection can be bypassed entirely.