Preview Environments for Remix: Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202611 min read

Preview Environments for Remix: Automated Per-PR Deployments with Bunnyshell

Why Preview Environments for Remix?

Every Remix team has felt the friction: you build a new loader that joins three tables, test it locally against a seeded SQLite database, push to the shared staging server — and the page crashes because staging's Postgres has different column defaults from your last migration. Or someone deployed a half-finished action to staging and now every form submission returns a 500. Or the QA team can't verify your UI changes because another developer's broken redirect is blocking the entire staging app.

Preview environments solve this. Every pull request gets its own isolated deployment — Remix app running on Node.js, PostgreSQL database with a clean schema, Redis for sessions — all deployed in Kubernetes with production-like configuration. Reviewers click a link and see the actual running app, not just the diff.

With Bunnyshell, you get:

  • Automatic deployment — A new environment spins up for every PR
  • Production parity — Same Docker images, same database engine, same environment variables
  • Isolation — Each PR environment is fully independent, no shared staging conflicts
  • Automatic cleanup — Environments are destroyed when the PR is merged or closed

Remix is particularly well-suited for preview environments. Its loader/action architecture means all data fetching and mutations happen server-side by default — there's no separate API server to deploy. Every route is a self-contained unit of UI and data logic, which means a preview environment gives reviewers the complete picture with zero extra configuration.

Choose Your Approach

Bunnyshell supports three ways to set up preview environments for Remix. Pick the one that fits your workflow:

ApproachBest forComplexityCI/CD maintenance
Approach A: Bunnyshell UITeams that want the fastest setup with zero pipeline maintenanceEasiestNone — Bunnyshell manages webhooks automatically
Approach B: Docker Compose ImportTeams already using docker-compose.yml for local developmentEasyNone — import converts to Bunnyshell config automatically
Approach C: Helm ChartsTeams with existing Helm infrastructure or complex K8s needsAdvancedOptional — can use CLI or Bunnyshell UI

All three approaches end the same way: a toggle in Bunnyshell Settings that enables automatic preview environments for every PR. No GitHub Actions, no GitLab CI pipelines to maintain — Bunnyshell adds webhooks to your Git provider and listens for PR events.

Prerequisites: Prepare Your Remix App

Regardless of which approach you choose, your Remix app needs a proper Docker setup and the right configuration for running in Kubernetes.

1. Create a Production-Ready Dockerfile

Remix builds to a standalone Node.js server. Here's a multi-stage Dockerfile optimized for production:

Dockerfile
1FROM node:20-alpine AS base
2WORKDIR /app
3
4# Install dependencies only when package files change
5FROM base AS deps
6COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
7COPY prisma ./prisma/
8RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
9    elif [ -f package-lock.json ]; then npm ci; \
10    elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
11    else echo "No lockfile found" && exit 1; fi
12
13# Generate Prisma client
14RUN npx prisma generate
15
16# Build the Remix app
17FROM base AS build
18COPY --from=deps /app/node_modules ./node_modules
19COPY . .
20RUN npm run build
21
22# Production image — minimal footprint
23FROM base AS production
24ENV NODE_ENV=production
25ENV HOST=0.0.0.0
26ENV PORT=3000
27
28# Copy built app and production dependencies
29COPY --from=deps /app/node_modules ./node_modules
30COPY --from=build /app/build ./build
31COPY --from=build /app/public ./public
32COPY --from=build /app/package.json ./
33COPY --from=build /app/prisma ./prisma
34
35# Run database migrations on startup, then start the server
36COPY docker-entrypoint.sh ./
37RUN chmod +x docker-entrypoint.sh
38
39EXPOSE 3000
40ENTRYPOINT ["./docker-entrypoint.sh"]

2. Create the Entrypoint Script

Create docker-entrypoint.sh in your repo root:

Bash
1#!/bin/sh
2set -e
3
4echo "Running Prisma migrations..."
5npx prisma migrate deploy
6
7echo "Starting Remix server..."
8exec npx remix-serve ./build/server/index.js

If you use the Express adapter instead of the built-in Remix server, replace the last line with exec node server.js. The Express adapter gives you more control over middleware, but either approach works for preview environments.

3. Configure Remix for Kubernetes

Remix needs minimal configuration to work behind a Kubernetes ingress. The key settings:

HOST=0.0.0.0 — Remix must bind to all interfaces, not just localhost, or it won't be reachable inside the container.

PORT=3000 — Standard port for the Node.js server. Must match the EXPOSE in your Dockerfile.

If you're using the Express adapter, ensure your server.js reads from environment variables:

JavaScript
1// server.js (Express adapter)
2import { createRequestHandler } from "@remix-run/express";
3import express from "express";
4
5const app = express();
6
7app.use(express.static("public", { maxAge: "1h" }));
8app.all("*", createRequestHandler({ build: await import("./build/server/index.js") }));
9
10const port = process.env.PORT || 3000;
11const host = process.env.HOST || "0.0.0.0";
12
13app.listen(port, host, () => {
14  console.log(`Remix app listening on http://${host}:${port}`);
15});

Unlike Express or Next.js, Remix doesn't need a reverse proxy (Nginx) in front of it. The built-in remix-serve or the Express adapter handles HTTP directly. This simplifies your Kubernetes setup — one container, one port.

4. Environment Variables

Create or update your .env.example with Bunnyshell-friendly defaults:

.env
1NODE_ENV=production
2HOST=0.0.0.0
3PORT=3000
4
5DATABASE_URL=postgresql://remix:password@postgres:5432/remix_db?schema=public
6SESSION_SECRET=your-session-secret
7
8# Optional
9REDIS_URL=redis://redis:6379

Remix Deployment Checklist

  • Dockerfile uses multi-stage build with Node 20 Alpine
  • Prisma client generated in the deps stage
  • docker-entrypoint.sh runs migrations before starting the server
  • HOST=0.0.0.0 so the server binds to all interfaces
  • SESSION_SECRET will be set via SECRET["..."] in Bunnyshell
  • DATABASE_URL uses PostgreSQL connection string format
  • No Nginx sidecar needed — Remix serves HTTP directly

Approach A: Bunnyshell UI — Zero CI/CD Maintenance

This is the easiest approach. You connect your repo, paste a YAML config, deploy, and flip a toggle. No CI/CD pipelines to write or maintain — Bunnyshell automatically adds webhooks to your Git provider and creates/destroys preview environments when PRs are opened/closed.

Step 1: Create a Project and Environment

  1. Log into Bunnyshell
  2. Click Create project and name it (e.g., "Remix App")
  3. Inside the project, click Create environment and name it (e.g., "remix-main")

Step 2: Define the Environment Configuration

Click Configuration in your environment view and paste this bunnyshell.yaml:

YAML
1kind: Environment
2name: remix-preview
3type: primary
4
5environmentVariables:
6  SESSION_SECRET: SECRET["your-session-secret"]
7  DB_PASSWORD: SECRET["your-db-password"]
8  DB_USER: remix
9  DB_NAME: remix_db
10
11components:
12  # ── Remix Application ──
13  - kind: Application
14    name: remix-app
15    gitRepo: 'https://github.com/your-org/your-remix-repo.git'
16    gitBranch: main
17    gitApplicationPath: /
18    dockerCompose:
19      build:
20        context: .
21        dockerfile: Dockerfile
22      environment:
23        NODE_ENV: production
24        HOST: '0.0.0.0'
25        PORT: '3000'
26        DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@postgres:5432/{{ env.vars.DB_NAME }}?schema=public'
27        SESSION_SECRET: '{{ env.vars.SESSION_SECRET }}'
28      ports:
29        - '3000:3000'
30    dependsOn:
31      - postgres
32    hosts:
33      - hostname: 'app-{{ env.base_domain }}'
34        path: /
35        servicePort: 3000
36
37  # ── PostgreSQL Database ──
38  - kind: Database
39    name: postgres
40    dockerCompose:
41      image: 'postgres:16-alpine'
42      environment:
43        POSTGRES_USER: '{{ env.vars.DB_USER }}'
44        POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
45        POSTGRES_DB: '{{ env.vars.DB_NAME }}'
46      ports:
47        - '5432:5432'
48
49  # ── Redis (Sessions / Cache) ──
50  - kind: Service
51    name: redis
52    dockerCompose:
53      image: 'redis:7-alpine'
54      ports:
55        - '6379:6379'
56
57volumes:
58  - name: postgres-data
59    mount:
60      component: postgres
61      containerPath: /var/lib/postgresql/data
62    size: 1Gi

Key architecture notes:

  • Single container — Unlike PHP-FPM + Nginx setups, Remix serves HTTP directly. No sidecar needed.
  • DATABASE_URL interpolation — Bunnyshell builds the full Postgres connection string from environment variables, keeping the password in SECRET["..."] storage
  • Migrations run automatically — The docker-entrypoint.sh runs npx prisma migrate deploy before starting the server

Replace your-org/your-remix-repo with your actual repository. Save the configuration.

Step 3: Deploy

Click the Deploy button, select your Kubernetes cluster, and click Deploy Environment. Bunnyshell will:

  1. Build your Remix Docker image from the Dockerfile
  2. Pull PostgreSQL and Redis images
  3. Deploy everything into an isolated Kubernetes namespace
  4. Generate HTTPS URLs automatically with DNS

Monitor the deployment in the environment detail page. When status shows Running, click Endpoints to access your live Remix app.

Step 4: Verify the Deployment

After deployment, verify everything is working:

Bash
1export BUNNYSHELL_TOKEN=your-api-token
2bns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'
3
4# Check migration status
5bns exec COMPONENT_ID -- npx prisma migrate status
6
7# Seed data (if you have a seed script)
8bns exec COMPONENT_ID -- npx prisma db seed
9
10# Check server logs
11bns logs --component COMPONENT_ID --tail 50

Step 5: Enable Automatic Preview Environments

This is the magic step — no CI/CD configuration needed:

  1. In your environment, go to Settings
  2. Find the Ephemeral environments section
  3. Toggle "Create ephemeral environments on pull request" to ON
  4. Toggle "Destroy environment after merge or close pull request" to ON
  5. Select the Kubernetes cluster for ephemeral environments

That's it. Bunnyshell automatically adds a webhook to your Git provider (GitHub, GitLab, or Bitbucket). From now on:

  • Open a PR → Bunnyshell creates an ephemeral environment with the PR's branch
  • Push to PR → The environment redeploys with the latest changes
  • Bunnyshell posts a comment on the PR with a link to the live deployment
  • Merge or close the PR → The ephemeral environment is automatically destroyed

The primary environment must be in Running or Stopped status before ephemeral environments can be created from it.


Approach B: Docker Compose Import

Already have a docker-compose.yml for local development? Bunnyshell can import it directly and convert it to its environment format. No manual YAML writing required.

Step 1: Add a docker-compose.yml to Your Repo

If you don't already have one, create docker-compose.yml in your repo root:

YAML
1version: '3.8'
2
3services:
4  remix-app:
5    build:
6      context: .
7      dockerfile: Dockerfile
8    ports:
9      - '3000:3000'
10    environment:
11      NODE_ENV: development
12      HOST: '0.0.0.0'
13      PORT: '3000'
14      DATABASE_URL: 'postgresql://remix:secret@postgres:5432/remix_db?schema=public'
15      SESSION_SECRET: 'local-dev-secret'
16    depends_on:
17      postgres:
18        condition: service_healthy
19
20  postgres:
21    image: postgres:16-alpine
22    environment:
23      POSTGRES_USER: remix
24      POSTGRES_PASSWORD: secret
25      POSTGRES_DB: remix_db
26    volumes:
27      - postgres-data:/var/lib/postgresql/data
28    ports:
29      - '5432:5432'
30    healthcheck:
31      test: ['CMD-SHELL', 'pg_isready -U remix -d remix_db']
32      interval: 5s
33      timeout: 5s
34      retries: 5
35
36  redis:
37    image: redis:7-alpine
38    ports:
39      - '6379:6379'
40
41volumes:
42  postgres-data:

Step 2: Import into Bunnyshell

  1. Create a Project and Environment in Bunnyshell (same as Approach A, Step 1)
  2. Click Define environment
  3. Select your Git account and repository
  4. Set the branch (e.g., main) and the path to docker-compose.yml (use / if it's in the root)
  5. Click Continue — Bunnyshell parses and validates your Docker Compose file

Bunnyshell automatically detects:

  • All services (remix-app, postgres, redis)
  • Exposed ports
  • Build configurations (Dockerfiles)
  • Volumes
  • Environment variables

It converts everything into a bunnyshell.yaml environment definition.

The docker-compose.yml is only read during the initial import. Subsequent changes to the file won't auto-propagate — edit the environment configuration in Bunnyshell instead.

Step 3: Adjust the Configuration

After import, go to Configuration in the environment view and update:

Replace hardcoded secrets with SECRET["..."] syntax:

YAML
1environmentVariables:
2  SESSION_SECRET: SECRET["your-session-secret"]
3  DB_PASSWORD: SECRET["your-db-password"]

Add dynamic URLs using Bunnyshell interpolation — this is critical for Remix apps that need to know their public URL for redirects and session cookies:

YAML
# If your app uses ORIGIN or APP_URL for CSRF or cookie domain:
APP_URL: 'https://{{ components.remix-app.ingress.hosts[0] }}'

Remove local dev volumesvolumes: ['.:/app'] is for local hot reload. Remove these in Bunnyshell — the Docker image already contains the built app.

Step 4: Deploy and Enable Preview Environments

Same as Approach A — click Deploy, then go to Settings and toggle on ephemeral environments.

Best Practices for Docker Compose with Bunnyshell

  • No reverse proxy needed — Unlike PHP/Laravel setups, Remix handles HTTP directly. Don't add an Nginx service unless you have a specific need
  • Healthchecks matter — The Postgres healthcheck in Docker Compose ensures Remix doesn't try to run migrations before the database is ready. Bunnyshell respects healthchecks from your Compose file
  • Use Bunnyshell interpolation for dynamic values:
YAML
1# Local docker-compose.yml
2DATABASE_URL: postgresql://remix:secret@postgres:5432/remix_db
3
4# Bunnyshell environment config (after import)
5DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@postgres:5432/{{ env.vars.DB_NAME }}?schema=public'
  • Design for startup resilience — Kubernetes doesn't guarantee depends_on ordering. Your docker-entrypoint.sh should retry database connections (Prisma handles transient connection errors gracefully, but verify with your setup)

Approach C: Helm Charts

For teams with existing Helm infrastructure or complex Kubernetes requirements (custom ingress, service mesh, auto-scaling). Helm gives you full control over every Kubernetes resource.

Step 1: Create a Helm Chart

Structure your Remix Helm chart in your repo:

Text
1helm/remix/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5    ├── deployment.yaml
6    ├── service.yaml
7    ├── ingress.yaml
8    ├── configmap.yaml
9    └── migration-job.yaml

A minimal values.yaml:

YAML
1replicaCount: 1
2image:
3  repository: ""
4  tag: latest
5service:
6  port: 3000
7ingress:
8  enabled: true
9  className: bns-nginx
10  host: ""
11env:
12  NODE_ENV: production
13  HOST: "0.0.0.0"
14  PORT: "3000"
15  DATABASE_URL: ""
16  SESSION_SECRET: ""

Step 2: Define the Bunnyshell Configuration

Create a bunnyshell.yaml using Helm components:

YAML
1kind: Environment
2name: remix-helm
3type: primary
4
5environmentVariables:
6  SESSION_SECRET: SECRET["your-session-secret"]
7  DB_PASSWORD: SECRET["your-db-password"]
8  DB_USER: remix
9  DB_NAME: remix_db
10
11components:
12  # ── Docker Image Build ──
13  - kind: DockerImage
14    name: remix-image
15    context: /
16    dockerfile: Dockerfile
17    gitRepo: 'https://github.com/your-org/your-remix-repo.git'
18    gitBranch: main
19    gitApplicationPath: /
20
21  # ── PostgreSQL via Helm (Bitnami) ──
22  - kind: Helm
23    name: postgres
24    runnerImage: 'dtzar/helm-kubectl:3.8.2'
25    deploy:
26      - |
27        cat << EOF > pg_values.yaml
28          global:
29            storageClass: bns-network-sc
30          auth:
31            postgresPassword: {{ env.vars.DB_PASSWORD }}
32            database: {{ env.vars.DB_NAME }}
33            username: {{ env.vars.DB_USER }}
34            password: {{ env.vars.DB_PASSWORD }}
35        EOF
36      - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
37      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
38        --post-renderer /bns/helpers/helm/bns_post_renderer
39        -f pg_values.yaml postgres bitnami/postgresql --version 13.4.4'
40      - |
41        PG_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
42    destroy:
43      - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
44    start:
45      - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}
46        statefulset/postgres-postgresql'
47    stop:
48      - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}
49        statefulset/postgres-postgresql'
50    exportVariables:
51      - PG_HOST
52
53  # ── Remix App via Helm ──
54  - kind: Helm
55    name: remix-app
56    runnerImage: 'dtzar/helm-kubectl:3.8.2'
57    deploy:
58      - |
59        cat << EOF > remix_values.yaml
60          replicaCount: 1
61          image:
62            repository: {{ components.remix-image.image }}
63          service:
64            port: 3000
65          ingress:
66            enabled: true
67            className: bns-nginx
68            host: app-{{ env.base_domain }}
69          env:
70            NODE_ENV: production
71            HOST: '0.0.0.0'
72            PORT: '3000'
73            DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.PG_HOST }}:5432/{{ env.vars.DB_NAME }}?schema=public'
74            SESSION_SECRET: '{{ env.vars.SESSION_SECRET }}'
75        EOF
76      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
77        --post-renderer /bns/helpers/helm/bns_post_renderer
78        -f remix_values.yaml remix-{{ env.unique }} ./helm/remix'
79    destroy:
80      - 'helm uninstall remix-{{ env.unique }} --namespace {{ env.k8s.namespace }}'
81    start:
82      - 'helm upgrade --namespace {{ env.k8s.namespace }}
83        --post-renderer /bns/helpers/helm/bns_post_renderer
84        --reuse-values --set replicaCount=1 remix-{{ env.unique }} ./helm/remix'
85    stop:
86      - 'helm upgrade --namespace {{ env.k8s.namespace }}
87        --post-renderer /bns/helpers/helm/bns_post_renderer
88        --reuse-values --set replicaCount=0 remix-{{ env.unique }} ./helm/remix'
89    gitRepo: 'https://github.com/your-org/your-remix-repo.git'
90    gitBranch: main
91    gitApplicationPath: /helm/remix
92
93  # ── Redis ──
94  - kind: Service
95    name: redis
96    dockerCompose:
97      image: 'redis:7-alpine'
98      ports:
99        - '6379:6379'

Always include --post-renderer /bns/helpers/helm/bns_post_renderer in your Helm commands. This adds labels so Bunnyshell can track resources, show logs, and manage component lifecycle.

Step 3: Deploy and Enable Preview Environments

Same flow: paste the config in Configuration, hit Deploy, then enable ephemeral environments in Settings.


Enabling Preview Environments (All Approaches)

Regardless of which approach you used, enabling automatic preview environments is the same:

  1. Ensure your primary environment has been deployed at least once (Running or Stopped status)
  2. Go to Settings in your environment
  3. Toggle "Create ephemeral environments on pull request" → ON
  4. Toggle "Destroy environment after merge or close pull request" → ON
  5. Select the target Kubernetes cluster

What happens next:

  • Bunnyshell adds a webhook to your Git provider automatically
  • When a developer opens a PR, Bunnyshell creates an ephemeral environment cloned from the primary, using the PR's branch
  • Bunnyshell posts a comment on the PR with a direct link to the running deployment
  • When the PR is merged or closed, the ephemeral environment is automatically destroyed

No GitHub Actions. No GitLab CI pipelines. No maintenance. It just works.

Optional: CI/CD Integration via CLI

If you prefer to control preview environments from your CI/CD pipeline (e.g., for custom seed scripts), you can use the Bunnyshell CLI:

Bash
1# Install
2brew install bunnyshell/tap/bunnyshell-cli
3
4# Authenticate
5export BUNNYSHELL_TOKEN=your-api-token
6
7# Create, deploy, and seed in one flow
8bns environments create --from-path bunnyshell.yaml --name "pr-123" --project PROJECT_ID --k8s CLUSTER_ID
9bns environments deploy --id ENV_ID --wait
10bns exec COMPONENT_ID -- npx prisma db seed

Remote Development and Debugging

Bunnyshell makes it easy to develop and debug directly against any environment — primary or ephemeral:

Port Forwarding

Connect your local tools to the remote database:

Bash
1# Forward PostgreSQL to local port 15432
2bns port-forward 15432:5432 --component POSTGRES_COMPONENT_ID
3
4# Connect with psql, TablePlus, Prisma Studio, or any DB tool
5psql -h 127.0.0.1 -p 15432 -U remix -d remix_db
6
7# Forward Redis to local port 16379
8bns port-forward 16379:6379 --component REDIS_COMPONENT_ID
9redis-cli -p 16379

Execute Remix Commands

Bash
1# Check migration status
2bns exec COMPONENT_ID -- npx prisma migrate status
3
4# Run migrations manually
5bns exec COMPONENT_ID -- npx prisma migrate deploy
6
7# Open Prisma Studio (forward the port first)
8bns exec COMPONENT_ID -- npx prisma studio --port 5555 &
9bns port-forward 5555:5555 --component COMPONENT_ID
10
11# Seed data
12bns exec COMPONENT_ID -- npx prisma db seed
13
14# Interactive Prisma shell
15bns exec COMPONENT_ID -- npx prisma db execute --stdin
16
17# Check Node.js process
18bns exec COMPONENT_ID -- ps aux
19
20# View Remix build output
21bns exec COMPONENT_ID -- ls -la build/

Live Logs

Bash
1# Stream logs in real time
2bns logs --component COMPONENT_ID -f
3
4# Last 200 lines
5bns logs --component COMPONENT_ID --tail 200
6
7# Logs from the last 5 minutes
8bns logs --component COMPONENT_ID --since 5m

Live Code Sync

For active development, sync your local code changes to the remote container in real time:

Bash
1bns remote-development up --component COMPONENT_ID
2# Edit files locally — changes sync automatically
3# Remix dev server will hot-reload on file changes
4# When done:
5bns remote-development down

This is especially useful for debugging issues that only reproduce in the Kubernetes environment — you get the fast feedback loop of local development with the infrastructure of production.


Troubleshooting

IssueSolution
502 Bad GatewayRemix server isn't running. Check that HOST=0.0.0.0 is set (not localhost). Verify PORT=3000 matches the ingress servicePort. Check container logs for startup errors.
ECONNREFUSED to databasePostgreSQL isn't ready yet. Verify the environment status shows Running. Check that DATABASE_URL uses postgres (the component name) as the host, not localhost.
Prisma migration failedCheck that the Prisma schema matches the database engine. Ensure DATABASE_URL includes ?schema=public for PostgreSQL. Run bns exec COMPONENT_ID -- npx prisma migrate status to debug.
Session not persistingSESSION_SECRET not set or changes between deployments. Use SECRET["..."] in environmentVariables so the value stays consistent across redeployments.
Loader returns empty dataDatabase exists but has no data. Run bns exec COMPONENT_ID -- npx prisma db seed to populate test data.
Static assets 404The public/ directory wasn't copied in the Dockerfile. Verify the COPY --from=build /app/public ./public line exists in your production stage.
Form actions return 405The ingress may be stripping the POST method. Verify the Bunnyshell ingress configuration allows all HTTP methods (this is the default — if you've customized it, check there).
CSS not loading / unstyled pageRemix built CSS isn't being served. Check that public/build/ exists in the container. If using Tailwind, ensure tailwindcss is in dependencies (not devDependencies) or that CSS is built in the Docker build stage.
remix-serve not found@remix-run/serve is in devDependencies. Move it to dependencies or install all deps in the build stage and copy the binary.
522 Connection timed outCluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller.

What's Next?

  • Add Redis for sessions — Use createCookieSessionStorage with Redis-backed sessions for multi-instance deployments
  • Add Mailpit — Test email sending with a local SMTP server (axllent/mailpit as a Service component)
  • Seed test data — Add npx prisma db seed to your docker-entrypoint.sh with a conditional flag
  • Add MinIO — S3-compatible object storage for file uploads (minio/minio as a Service component)
  • Storybook preview — Deploy Storybook alongside your Remix app in the same environment for component review

Ship faster starting today.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.