Preview Environments for Phoenix (Elixir): Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202611 min read

Preview Environments for Phoenix (Elixir): Automated Per-PR Deployments with Bunnyshell

Why Preview Environments for Phoenix?

Every Phoenix team has been here: a PR looks fine in review, the tests pass, but the moment it hits staging — a database migration conflicts with another branch, LiveView pushes fail because PHX_HOST is wrong, or an Ecto query behaves differently under real load.

Preview environments solve this. Every pull request gets its own isolated deployment — Phoenix app, PostgreSQL database, and any other services — running 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 Elixir release configuration
  • Isolation — Each PR environment is fully independent, no shared staging conflicts
  • Automatic cleanup — Environments are destroyed when the PR is merged or closed

Phoenix's strengths — LiveView, channels, soft-realtime features — all work correctly in Bunnyshell environments. WebSocket support is built into the ingress controller, so you don't lose any functionality compared to production.

Choose Your Approach

Bunnyshell supports three ways to set up preview environments for Phoenix. 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 Phoenix App

Regardless of which approach you choose, your Phoenix app needs two things: a Dockerfile and the right runtime configuration.

1. Create a Production-Ready Dockerfile

Phoenix applications can be deployed two ways: using mix phx.server (simpler, good for preview environments) or as an Elixir release (smaller image, closer to production). Both are shown below.

Option A: mix phx.server (simpler, faster to build)

Dockerfile
1FROM elixir:1.16-otp-26-slim AS base
2
3ENV MIX_ENV=prod \
4    LANG=C.UTF-8 \
5    LC_ALL=C.UTF-8
6
7WORKDIR /app
8
9# Install Hex and Rebar
10RUN mix local.hex --force && mix local.rebar --force
11
12# Install system dependencies for building native extensions
13RUN apt-get update && apt-get install -y --no-install-recommends \
14    build-essential \
15    git \
16    nodejs \
17    npm \
18    && rm -rf /var/lib/apt/lists/*
19
20# Install Node dependencies for asset pipeline
21COPY assets/package.json assets/package-lock.json ./assets/
22RUN npm --prefix assets install
23
24# Install Elixir dependencies
25COPY mix.exs mix.lock ./
26RUN mix deps.get --only prod
27RUN mix deps.compile
28
29# Copy application code
30COPY . .
31
32# Compile assets (esbuild + tailwind)
33RUN mix assets.deploy
34
35# Compile the app
36RUN mix compile
37
38EXPOSE 4000
39CMD ["mix", "phx.server"]

Option B: Elixir release (multi-stage, smaller image)

Dockerfile
1# ── Build stage ──────────────────────────────────────────────────────────────
2FROM elixir:1.16-otp-26-slim AS builder
3
4ENV MIX_ENV=prod \
5    LANG=C.UTF-8
6
7WORKDIR /app
8
9RUN mix local.hex --force && mix local.rebar --force
10
11RUN apt-get update && apt-get install -y --no-install-recommends \
12    build-essential git nodejs npm \
13    && rm -rf /var/lib/apt/lists/*
14
15COPY assets/package.json assets/package-lock.json ./assets/
16RUN npm --prefix assets install
17
18COPY mix.exs mix.lock ./
19RUN mix deps.get --only prod
20RUN mix deps.compile
21
22COPY . .
23
24RUN mix assets.deploy
25RUN mix compile
26RUN mix release
27
28# ── Runtime stage ─────────────────────────────────────────────────────────────
29FROM debian:bookworm-slim AS runtime
30
31ENV LANG=C.UTF-8 \
32    LC_ALL=C.UTF-8 \
33    MIX_ENV=prod
34
35RUN apt-get update && apt-get install -y --no-install-recommends \
36    libssl3 libncurses6 \
37    && rm -rf /var/lib/apt/lists/*
38
39WORKDIR /app
40COPY --from=builder /app/_build/prod/rel/myapp ./
41
42EXPOSE 4000
43CMD ["/app/bin/myapp", "start"]

Replace myapp with your actual OTP application name (the one in mix.exs under app:). The app must listen on 0.0.0.0, not 127.0.0.1 — this is required for container networking in Kubernetes.

2. Configure Phoenix for Kubernetes

Phoenix needs specific runtime configuration to work correctly behind Kubernetes ingress (which terminates TLS). Edit config/runtime.exs:

Elixir
1# config/runtime.exs
2import Config
3
4# PHX_HOST is required — Phoenix will refuse requests without it.
5# Bunnyshell sets this to the generated ingress hostname.
6host = System.get_env("PHX_HOST") || raise "PHX_HOST is not set"
7port = String.to_integer(System.get_env("PORT") || "4000")
8
9config :myapp, MyAppWeb.Endpoint,
10  url: [host: host, port: 443, scheme: "https"],
11  http: [ip: {0, 0, 0, 0}, port: port],
12  secret_key_base:
13    System.get_env("SECRET_KEY_BASE") ||
14      raise """
15      SECRET_KEY_BASE is not set.
16      Generate with: mix phx.gen.secret
17      """
18
19# Database configuration from environment
20config :myapp, MyApp.Repo,
21  url:
22    System.get_env("DATABASE_URL") ||
23      raise """
24      DATABASE_URL is not set.
25      Format: ecto://USER:PASS@HOST/DATABASE
26      """
27
28# Kubernetes ingress terminates TLS — Phoenix sees HTTP traffic.
29# This tells Phoenix to use https:// in generated URLs.
30config :myapp, MyAppWeb.Endpoint,
31  force_ssl: false
32
33# Use the encrypted cookie session store in production
34config :myapp, MyAppWeb.Endpoint,
35  check_origin: ["https://#{host}"]

Do not set force_ssl: true in your Phoenix config. TLS is terminated at the Kubernetes ingress controller — Phoenix sees plain HTTP traffic. Setting force_ssl: true will cause redirect loops. Use check_origin with the full https:// URL instead.

Phoenix Deployment Checklist

  • PHX_HOST set to the Bunnyshell-generated ingress hostname
  • SECRET_KEY_BASE loaded from environment variable (generate with mix phx.gen.secret)
  • DATABASE_URL in standard Ecto format
  • url: [scheme: "https"] in Endpoint config so LiveView uses wss://
  • force_ssl: false — TLS is handled by ingress
  • check_origin set to the HTTPS ingress URL
  • App listens on 0.0.0.0:4000 (not 127.0.0.1)
  • Assets compiled with mix assets.deploy in Dockerfile

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., "Phoenix App")
  3. Inside the project, click Create environment and name it (e.g., "phoenix-main")

Step 2: Define the Environment Configuration

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

YAML
1kind: Environment
2name: phoenix-preview
3type: primary
4
5environmentVariables:
6  SECRET_KEY_BASE: SECRET["your-secret-key-base"]
7  DB_PASSWORD: SECRET["your-db-password"]
8
9components:
10  # ── Phoenix Application ──
11  - kind: Application
12    name: phoenix-app
13    gitRepo: 'https://github.com/your-org/your-phoenix-repo.git'
14    gitBranch: main
15    gitApplicationPath: /
16    dockerCompose:
17      build:
18        context: .
19        dockerfile: Dockerfile
20      environment:
21        SECRET_KEY_BASE: '{{ env.vars.SECRET_KEY_BASE }}'
22        DATABASE_URL: 'ecto://phoenix:{{ env.vars.DB_PASSWORD }}@postgres/phoenix_db'
23        PHX_HOST: '{{ components.phoenix-app.ingress.hosts[0] }}'
24        PORT: '4000'
25        MIX_ENV: prod
26        PHX_SERVER: 'true'
27      ports:
28        - '4000:4000'
29    hosts:
30      - hostname: 'app-{{ env.base_domain }}'
31        path: /
32        servicePort: 4000
33    dependsOn:
34      - postgres
35
36  # ── PostgreSQL Database ──
37  - kind: Database
38    name: postgres
39    dockerCompose:
40      image: 'postgres:16-alpine'
41      environment:
42        POSTGRES_DB: phoenix_db
43        POSTGRES_USER: phoenix
44        POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
45      ports:
46        - '5432:5432'
47
48volumes:
49  - name: postgres-data
50    mount:
51      component: postgres
52      containerPath: /var/lib/postgresql/data
53    size: 1Gi

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

PHX_SERVER=true is required when using releases to start the Phoenix endpoint. If you are using mix phx.server (Option A Dockerfile above), this variable is not needed but harmless.

Step 3: Deploy

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

  1. Build your Phoenix Docker image from the Dockerfile
  2. Pull the PostgreSQL image
  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 Phoenix app.

Step 4: Run Ecto Migrations

After deployment, run migrations via the component terminal in the Bunnyshell UI, or via CLI:

Bash
1export BUNNYSHELL_TOKEN=your-api-token
2
3# Get the component ID for phoenix-app
4bns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'
5
6# Run migrations (mix phx.server approach)
7bns exec COMPONENT_ID -- mix ecto.migrate
8
9# Run migrations (release approach)
10bns exec COMPONENT_ID -- /app/bin/myapp eval "MyApp.Release.migrate()"

For releases, you need a migrate/0 function in your release module. Add it to lib/myapp/release.ex:

Elixir
1defmodule MyApp.Release do
2  @app :myapp
3
4  def migrate do
5    load_app()
6
7    for repo <- repos() do
8      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
9    end
10  end
11
12  defp repos do
13    Application.fetch_env!(@app, :ecto_repos)
14  end
15
16  defp load_app do
17    Application.load(@app)
18  end
19end

Step 5: Enable Automatic Preview Environments

This is the key 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  phoenix-app:
5    build:
6      context: .
7      dockerfile: Dockerfile
8    ports:
9      - '4000:4000'
10    environment:
11      SECRET_KEY_BASE: 'local-dev-secret-key-base-change-in-prod'
12      DATABASE_URL: 'ecto://phoenix:phoenix@postgres/phoenix_dev'
13      PHX_HOST: 'localhost'
14      PORT: '4000'
15      MIX_ENV: dev
16      PHX_SERVER: 'true'
17    depends_on:
18      - postgres
19
20  postgres:
21    image: postgres:16-alpine
22    environment:
23      POSTGRES_DB: phoenix_dev
24      POSTGRES_USER: phoenix
25      POSTGRES_PASSWORD: phoenix
26    volumes:
27      - postgres-data:/var/lib/postgresql/data
28    ports:
29      - '5432:5432'
30
31volumes:
32  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 (phoenix-app, postgres)
  • 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
  • Update PHX_HOST to use the Bunnyshell ingress hostname interpolation:
YAML
1PHX_HOST: '{{ components.phoenix-app.ingress.hosts[0] }}'
2DATABASE_URL: 'ecto://phoenix:{{ env.vars.DB_PASSWORD }}@postgres/phoenix_db'
3SECRET_KEY_BASE: '{{ env.vars.SECRET_KEY_BASE }}'

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

  • Separate env configs — Use different MIX_ENV values: dev locally, prod on Bunnyshell
  • Design for startup resilience — Kubernetes doesn't guarantee depends_on ordering. Make your Phoenix app retry database connections on startup. You can add a startup script or use a library like pg_backoff
  • Use Bunnyshell interpolation for dynamic values like the endpoint URL:
YAML
1# Local docker-compose.yml
2PHX_HOST: localhost
3
4# Bunnyshell environment config (after import)
5PHX_HOST: '{{ components.phoenix-app.ingress.hosts[0] }}'

Approach C: Helm Charts

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

Step 1: Create a Helm Chart

Structure your Phoenix Helm chart in your repo:

Text
1helm/phoenix/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5    ├── deployment.yaml
6    ├── service.yaml
7    ├── ingress.yaml
8    └── configmap.yaml

A minimal values.yaml:

YAML
1replicaCount: 1
2image:
3  repository: ""
4  tag: latest
5service:
6  port: 4000
7ingress:
8  enabled: true
9  className: bns-nginx
10  host: ""
11env:
12  SECRET_KEY_BASE: ""
13  DATABASE_URL: ""
14  PHX_HOST: ""
15  PORT: "4000"
16  MIX_ENV: prod
17  PHX_SERVER: "true"

Step 2: Define the Bunnyshell Configuration

Create a bunnyshell.yaml using Helm components:

YAML
1kind: Environment
2name: phoenix-helm
3type: primary
4
5environmentVariables:
6  SECRET_KEY_BASE: SECRET["your-secret-key-base"]
7  DB_PASSWORD: SECRET["your-db-password"]
8  POSTGRES_DB: phoenix_db
9  POSTGRES_USER: phoenix
10
11components:
12  # ── Docker Image Build ──
13  - kind: DockerImage
14    name: phoenix-image
15    context: /
16    dockerfile: Dockerfile
17    gitRepo: 'https://github.com/your-org/your-phoenix-repo.git'
18    gitBranch: main
19    gitApplicationPath: /
20
21  # ── PostgreSQL via Helm ──
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.POSTGRES_DB }}
33            username: {{ env.vars.POSTGRES_USER }}
34        EOF
35      - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
36      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
37        --post-renderer /bns/helpers/helm/bns_post_renderer
38        -f pg_values.yaml postgres bitnami/postgresql --version 11.9.11'
39      - |
40        POSTGRES_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
41    destroy:
42      - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
43    start:
44      - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}
45        statefulset/postgres-postgresql'
46    stop:
47      - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}
48        statefulset/postgres-postgresql'
49    exportVariables:
50      - POSTGRES_HOST
51
52  # ── Phoenix App via Helm ──
53  - kind: Helm
54    name: phoenix-app
55    runnerImage: 'dtzar/helm-kubectl:3.8.2'
56    deploy:
57      - |
58        cat << EOF > phoenix_values.yaml
59          replicaCount: 1
60          image:
61            repository: {{ components.phoenix-image.image }}
62          service:
63            port: 4000
64          ingress:
65            enabled: true
66            className: bns-nginx
67            host: app-{{ env.base_domain }}
68          env:
69            SECRET_KEY_BASE: '{{ env.vars.SECRET_KEY_BASE }}'
70            DATABASE_URL: 'ecto://{{ env.vars.POSTGRES_USER }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.POSTGRES_HOST }}/{{ env.vars.POSTGRES_DB }}'
71            PHX_HOST: 'app-{{ env.base_domain }}'
72            PORT: '4000'
73            MIX_ENV: prod
74            PHX_SERVER: 'true'
75        EOF
76      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
77        --post-renderer /bns/helpers/helm/bns_post_renderer
78        -f phoenix_values.yaml phoenix-{{ env.unique }} ./helm/phoenix'
79    destroy:
80      - 'helm uninstall phoenix-{{ 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 phoenix-{{ env.unique }} ./helm/phoenix'
85    stop:
86      - 'helm upgrade --namespace {{ env.k8s.namespace }}
87        --post-renderer /bns/helpers/helm/bns_post_renderer
88        --reuse-values --set replicaCount=0 phoenix-{{ env.unique }} ./helm/phoenix'
89    gitRepo: 'https://github.com/your-org/your-phoenix-repo.git'
90    gitBranch: main
91    gitApplicationPath: /helm/phoenix
92    dependsOn:
93      - postgres
94      - phoenix-image

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 migration 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 run migrations 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
10
11# Run Ecto migrations (mix phx.server approach)
12bns exec COMPONENT_ID -- mix ecto.migrate
13
14# Run Ecto migrations (release approach)
15bns exec COMPONENT_ID -- /app/bin/myapp eval "MyApp.Release.migrate()"

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 or any DB tool
5psql -h localhost -p 15432 -U phoenix phoenix_db
6
7# Run Ecto migrations against the remote DB from local mix
8DATABASE_URL=ecto://phoenix:password@localhost:15432/phoenix_db mix ecto.migrate

Execute Phoenix/Elixir Commands

Bash
1# Run migrations
2bns exec COMPONENT_ID -- mix ecto.migrate
3
4# Open an IEx session connected to the running app
5bns exec COMPONENT_ID -- iex -S mix
6
7# Run a release eval
8bns exec COMPONENT_ID -- /app/bin/myapp eval "MyApp.Release.migrate()"
9
10# Inspect DB state
11bns exec COMPONENT_ID -- mix ecto.migrations
12
13# Run seeds
14bns exec COMPONENT_ID -- mix run priv/repo/seeds.exs

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 to the running container
3# When done:
4bns remote-development down

This is especially useful with Phoenix's hot-reload: changes to templates and LiveView modules reflect immediately in the browser without rebuilding the Docker image.


Troubleshooting

IssueSolution
502 Bad GatewayPhoenix isn't listening on 0.0.0.0:4000. Verify http: [ip: {0, 0, 0, 0}, port: 4000] in runtime.exs.
PHX_HOST not set crash on startupSet PHX_HOST in your environment variables. Bunnyshell provides {{ components.phoenix-app.ingress.hosts[0] }} for this.
LiveView disconnects / wss:// errorsEnsure url: [scheme: "https"] is set in Endpoint config so LiveView generates wss:// WebSocket URLs.
Mixed content / HTTPS errorsDo not set force_ssl: true. TLS is terminated at ingress — Phoenix sees HTTP. Set url: [scheme: "https"] instead.
Check origin blockedSet check_origin: ["https://#{host}"] in runtime.exs using the PHX_HOST value.
Assets not loading (404s)Ensure mix assets.deploy runs in Dockerfile and static files are included in the release.
Ecto migration errors at startupRun migrations manually via bns exec after the first deploy. For releases, add a Release.migrate() step.
Connection refused to PostgreSQLVerify DATABASE_URL uses postgres as the hostname (the component name), not localhost.
Service startup order issuesKubernetes doesn't guarantee depends_on ordering. Add a startup health check or retry logic in your app.
522 Connection timed outCluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller.

What's Next?

  • Add Oban workers — Add another component for background job processing with Oban
  • Seed test data — Run bns exec <ID> -- mix run priv/repo/seeds.exs post-deploy
  • Add Redis for PubSub — Replace the default PubSub adapter with Phoenix.PubSub.Redis for multi-node setups
  • Monitor with AppSignal or Sentry — Pass APPSIGNAL_APP_KEY or SENTRY_DSN as environment variables

Ship faster starting today.

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