Deploying a Phoenix App
Phoenix is a powerful, modern web framework for Elixir that emphasizes scalability, developer productivity, and real-time capabilities. Built on top of the Erlang VM, Phoenix leverages Elixir’s functional programming model and fault-tolerant architecture to enable building high-concurrency web applications, real-time features with WebSockets, and Live views that sync state automatically without writing JavaScript. Its comprehensive tooling, excellent documentation, and integration with Ecto ORM make it ideal for building robust, maintainable web applications.
This guide explains how to deploy a Phoenix application to Klutch.sh, both with and without a Dockerfile, along with database configuration, environment setup, persistent storage, real-time patterns, security, and production deployment strategies.
Prerequisites
- Elixir 1.14 or higher
- Erlang 25 or higher
- Phoenix 1.7 or higher
- Git and a GitHub account
- Klutch.sh account
- Basic knowledge of Elixir and web framework concepts
Getting Started: Installing Elixir and Creating a Phoenix App
-
Install Elixir and Erlang (if not already installed):
Use asdf for version management (recommended):
Terminal window asdf install elixir 1.16.0asdf install erlang 27.0asdf local elixir 1.16.0 erlang 27.0Or follow Elixir’s official installation guide.
-
Install Phoenix:
Terminal window mix archive.install hex phx_new -
Create a new Phoenix app with PostgreSQL:
Terminal window mix phx.new my-phoenix-appcd my-phoenix-app -
Create the database locally:
Terminal window mix ecto.create -
Start the development server:
Terminal window mix phx.serverYour app should be running at http://localhost:4000.
Sample Code for Getting Started
Create a complete Phoenix application with controllers, models, and real-time capabilities:
defmodule MyPhoenixAppWeb.ArticleHTML do embed_templates "article_html/*"end
# lib/my_phoenix_app_web/controllers/article_controller.exdefmodule MyPhoenixAppWeb.ArticleController do use MyPhoenixAppWeb, :controller alias MyPhoenixApp.Blog alias MyPhoenixApp.Blog.Article
def index(conn, _params) do articles = Blog.list_articles() render(conn, :index, articles: articles) end
def show(conn, %{"id" => id}) do article = Blog.get_article!(id) render(conn, :show, article: article) end
def new(conn, _params) do changeset = Blog.change_article(%Article{}) render(conn, :new, changeset: changeset) end
def create(conn, %{"article" => article_params}) do case Blog.create_article(article_params) do {:ok, article} -> conn |> put_flash(:info, "Article created successfully.") |> redirect(to: ~p"/articles/#{article}")
{:error, %Ecto.Changeset{} = changeset} -> render(conn, :new, changeset: changeset) end endend
# lib/my_phoenix_app/blog/article.exdefmodule MyPhoenixApp.Blog.Article do use Ecto.Schema import Ecto.Changeset
schema "articles" do field :title, :string field :content, :string field :published, :boolean, default: false
timestamps(type: :utc_datetime_usec) end
def changeset(article, attrs) do article |> cast(attrs, [:title, :content, :published]) |> validate_required([:title, :content]) |> validate_length(:title, min: 3, max: 255) endend
# lib/my_phoenix_app/blog.exdefmodule MyPhoenixApp.Blog do import Ecto.Query alias MyPhoenixApp.Repo alias MyPhoenixApp.Blog.Article
def list_articles do Repo.all(Article) end
def get_article!(id), do: Repo.get!(Article, id)
def create_article(attrs \\ %{}) do %Article{} |> Article.changeset(attrs) |> Repo.insert() end
def change_article(%Article{} = article, attrs \\ %{}) do Article.changeset(article, attrs) endendDeploying Without a Dockerfile (Using Nixpacks)
Klutch.sh uses Nixpacks to automatically detect and build your Phoenix application. Nixpacks analyzes your project and determines the necessary dependencies, build steps, and runtime configuration.
Steps to Deploy with Nixpacks
-
Prepare your Phoenix app for production:
Update
config/prod.exswith production settings:config :my_phoenix_app, MyPhoenixAppWeb.Endpoint,url: [scheme: "https", host: System.get_env("PHX_HOST"), port: 443],http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],secret_key_base: System.get_env("SECRET_KEY_BASE"),cache_static_manifest: "priv/static/cache_manifest.json",server: trueconfig :my_phoenix_app, MyPhoenixApp.Repo,url: System.get_env("DATABASE_URL"),pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") -
Set up environment variables locally:
Create a
.envfile for local testing:Terminal window PHX_HOST=example-app.klutch.shPORT=4000SECRET_KEY_BASE=$(mix phx.gen.secret)DATABASE_URL=postgresql://user:pass@localhost/myapp_dev -
Compile assets for production:
Terminal window mix assets.deploy -
Commit your code to GitHub:
Terminal window git add .git commit -m "Initial Phoenix app setup"git push origin main -
Log in to Klutch.sh dashboard:
Visit https://klutch.sh/app
-
Create a new project:
- Click “Create Project”
- Enter your project name (e.g., “My Phoenix App”)
- Select your organization or personal account
-
Create a new app:
- Click “Create App”
- Select your Phoenix GitHub repository
- Select the branch (typically
mainormaster) - Select HTTP as the traffic type
- Set the internal port to 4000 (the default Phoenix port)
- Choose your desired region, compute power, and number of instances
-
Add environment variables:
In the app creation form, add these essential environment variables:
PHX_HOST=example-app.klutch.shPORT=4000SECRET_KEY_BASE=<generated-with-mix-phx.gen.secret>DATABASE_URL=postgresql://user:password@host:5432/myapp_prodPOOL_SIZE=10MIX_ENV=prod -
Deploy:
Click “Create” to deploy. Klutch.sh will automatically:
- Detect your Elixir/Phoenix project
- Install dependencies with
mix deps.get - Compile assets
- Build a release with
mix release - Start the application on port 4000
Your app will be available at example-app.klutch.sh.
Customizing Build and Start Commands
If you need to customize the build or start command for Nixpacks, add these environment variables:
NIXPACKS_BUILD_CMD=mix compile && mix assets.deploy && mix releaseNIXPACKS_START_CMD=bin/my_phoenix_app startDeploying With a Dockerfile
For more control over your build process and runtime environment, you can provide a custom Dockerfile. Klutch.sh will automatically detect and use a Dockerfile in your repository’s root directory.
Creating a Multi-Stage Dockerfile
-
Create a
Dockerfilein your project root:# Build stageFROM elixir:1.16-alpine AS builderWORKDIR /app# Install build dependenciesRUN apk add --no-cache build-base git npm# Install Elixir dependenciesCOPY mix.exs mix.lock ./RUN mix deps.get && mix deps.compile# Copy source codeCOPY . .# Compile assetsRUN npm install --prefix assets && npm run deploy --prefix assetsRUN mix assets.deploy# Build releaseRUN MIX_ENV=prod mix compile && mix release# Runtime stageFROM alpine:latestRUN apk add --no-cache libstdc++ ncurses-libs openssl bashWORKDIR /app# Copy release from builderCOPY --from=builder /app/_build/prod/rel/my_phoenix_app .# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1# Set environmentENV PORT=4000 MIX_ENV=prod# Expose portEXPOSE 4000# Start applicationCMD ["bin/my_phoenix_app", "start"] -
Commit the Dockerfile to GitHub:
Terminal window git add Dockerfilegit commit -m "Add production Dockerfile"git push origin main -
Log in to Klutch.sh dashboard:
Visit https://klutch.sh/app
-
Create a new app:
- Click “Create App”
- Select your Phoenix GitHub repository with the Dockerfile
- Select the branch
- Select HTTP as the traffic type
- Set the internal port to 4000
- Choose region, compute, and instances
-
Add environment variables:
PHX_HOST=example-app.klutch.shPORT=4000SECRET_KEY_BASE=<your-secret-key>DATABASE_URL=postgresql://user:password@host:5432/myapp_prodPOOL_SIZE=10MIX_ENV=prod -
Deploy:
Click “Create”. Klutch.sh will automatically build your Docker image and deploy your app.
Database Configuration
Phoenix applications typically use Ecto with PostgreSQL or MySQL for data persistence. Here’s how to configure databases with Klutch.sh.
PostgreSQL Configuration
-
Ecto is already configured in your Phoenix project for PostgreSQL by default.
-
Update
config/prod.exsto use theDATABASE_URLenvironment variable:config :my_phoenix_app, MyPhoenixApp.Repo,url: System.get_env("DATABASE_URL"),pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),ssl: true,timeout: 20_000,connect_timeout: 5_000 -
Create migrations for your database schema:
Terminal window mix ecto.gen.migration create_articles_table -
Edit the migration file in
priv/repo/migrations/:defmodule MyPhoenixApp.Repo.Migrations.CreateArticlesTable douse Ecto.Migrationdef change docreate table(:articles) doadd :title, :string, null: falseadd :content, :text, null: falseadd :published, :boolean, default: falsetimestamps(type: :utc_datetime_usec)endcreate index(:articles, [:inserted_at])endend -
During app creation on Klutch.sh, set:
DATABASE_URL=postgresql://username:password@postgres-host:5432/myapp_prod -
Run migrations after deployment:
The release will automatically run migrations if configured properly.
MySQL Configuration
-
Create a new app with MySQL:
Terminal window mix phx.new my-phoenix-app --database mysql -
Update
config/prod.exs:config :my_phoenix_app, MyPhoenixApp.Repo,url: System.get_env("DATABASE_URL"),pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") -
Set the DATABASE_URL environment variable:
DATABASE_URL=mysql://username:password@mysql-host:3306/myapp_prod
Environment Variables
Phoenix applications use environment variables for configuration. Here are the essential variables:
MIX_ENV=prodPORT=4000PHX_HOST=example-app.klutch.shSECRET_KEY_BASE=<generated-secret-key>DATABASE_URL=postgresql://user:password@host:5432/dbnamePOOL_SIZE=10REDIS_URL=redis://:password@redis-host:6379/0LOG_LEVEL=infoGenerating a Secret Key
Generate a production secret key:
mix phx.gen.secretCopy the output and set it as the SECRET_KEY_BASE environment variable.
Real-Time Features with WebSockets
Phoenix’s strength is real-time capabilities through WebSockets and LiveView:
Setting Up WebSocket Channels
defmodule MyPhoenixAppWeb.RoomChannel do use MyPhoenixAppWeb, :channel
@impl true def join("room:" <> room_id, _payload, socket) do {:ok, assign(socket, :room_id, room_id)} end
@impl true def handle_in("new_message", %{"body" => body}, socket) do broadcast(socket, "message_added", %{body: body}) {:noreply, socket} endend
# lib/my_phoenix_app_web/user_socket.exdefmodule MyPhoenixAppWeb.UserSocket do use Phoenix.Socket
channel "room:*", MyPhoenixAppWeb.RoomChannel
@impl true def connect(_params, socket, _connect_info) do {:ok, socket} end
@impl true def id(_socket), do: nilendLiveView for Real-Time UIs
defmodule MyPhoenixAppWeb.ArticlesLive do use MyPhoenixAppWeb, :live_view alias MyPhoenixApp.Blog
@impl true def mount(_params, _session, socket) do {:ok, assign(socket, :articles, Blog.list_articles())} end
@impl true def handle_event("delete_article", %{"id" => id}, socket) do Blog.delete_article(id) {:noreply, assign(socket, :articles, Blog.list_articles())} endendCaching with Redis
Phoenix applications can leverage Redis for caching to improve performance:
-
Add Redis to your
mix.exs:defp deps do[{:redis, "~> 1.2"},{:phoenix, "~> 1.7"}]end -
Configure Redis in
config/prod.exs:config :my_phoenix_app, :redis,url: System.get_env("REDIS_URL") || "redis://localhost:6379/0" -
Use caching in your application:
defmodule MyPhoenixApp.Cache dodef get_or_fetch(key, fun) docase Redis.get(key) do{:ok, value} when is_binary(value) ->{:ok, Jason.decode!(value)}_ ->case fun.() do{:ok, data} ->Redis.set(key, Jason.encode!(data), ex: 3600){:ok, data}error ->errorendendendend -
Set the REDIS_URL environment variable on Klutch.sh:
REDIS_URL=redis://:password@redis-host:6379/0
Persistent Storage
Phoenix applications may need persistent storage for uploaded files or logs. Klutch.sh supports mounting persistent volumes.
Configuring File Upload Storage
-
Configure Arc for file uploads (or similar library):
Add to
mix.exs:{:arc, "~> 0.11.0"} -
Create a storage module:
defmodule MyPhoenixApp.Uploaders.ArticleImage douse Arc.Definitionuse Arc.Ecto.Definition@versions [:original, :thumb]@extension_whitelist ~w(.jpg .jpeg .gif .png)def storage_dir(_version, {_file, scope}) dostorage_path = System.get_env("STORAGE_PATH", "uploads")"#{storage_path}/article/#{scope.id}"enddef validate({file, _}) dofile_extension = file.file_name |> Path.extname() |> String.downcase()Enum.member?(@extension_whitelist, file_extension)endend -
Use uploads in your schema:
defmodule MyPhoenixApp.Blog.Article douse Ecto.Schemaimport Ecto.Changesetschema "articles" dofield :title, :stringfield :image, MyPhoenixApp.Uploaders.ArticleImage.Typefield :content, :stringtimestamps()enddef changeset(article, attrs) doarticle|> cast(attrs, [:title, :content])|> cast_attachments(attrs, [:image])|> validate_required([:title, :content])endend -
On Klutch.sh, attach a persistent volume:
- During app creation, navigate to the “Storage” section
- Click “Add Volume”
- Set mount path to
/app/uploads - Allocate size (e.g., 10GB)
-
Set the STORAGE_PATH environment variable:
STORAGE_PATH=/app/uploads
Security Best Practices
1. Enforce HTTPS
Configure in config/prod.exs:
config :my_phoenix_app, MyPhoenixAppWeb.Endpoint, force_ssl: [rewrite_on: [:x_forwarded_proto], hsts: 31_536_000]2. Configure CORS
Use plug_cowboy with CORS middleware:
plug CORSPlug, origin: [System.get_env("ALLOWED_ORIGINS") || "http://localhost:3000"]3. Use Environment Variables for Secrets
Never hardcode secrets:
config :my_phoenix_app, api_key: System.get_env("API_KEY"), jwt_secret: System.get_env("JWT_SECRET")4. Implement Authentication
Use phx_gen_auth (built into Phoenix 1.7):
mix phx.gen.auth Accounts User users5. Use Plugs for Security Headers
defmodule MyPhoenixAppWeb.SecurityHeaders do def init(opts), do: opts
def call(conn, _opts) do conn |> put_resp_header("x-frame-options", "DENY") |> put_resp_header("x-content-type-options", "nosniff") |> put_resp_header("x-xss-protection", "1; mode=block") |> put_resp_header("referrer-policy", "strict-origin-when-cross-origin") endend6. Implement Rate Limiting
Use hammer library:
defmodule MyPhoenixAppWeb.RateLimitPlug do def init(opts), do: opts
def call(conn, _opts) do case Hammer.check_rate("requests:#{conn.remote_ip |> Tuple.to_list() |> Enum.join(".")}", 60_000, 100) do {:allow, _count} -> conn {:deny, _limit} -> conn |> put_status(429) |> halt() end endend7. Enable Content Security Policy
plug Phoenix.LiveDashboard.Router, at: "/dashboard"
config :my_phoenix_app, MyPhoenixAppWeb.Endpoint, csp_policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"Monitoring and Logging
Health Check Endpoint
Add a health check route:
defmodule MyPhoenixAppWeb.HealthController do use MyPhoenixAppWeb, :controller
def index(conn, _params) do status = %{ status: "healthy", timestamp: DateTime.utc_now() |> DateTime.to_iso8601() }
json(conn, status) endend
# lib/my_phoenix_app_web/router.exscope "/", MyPhoenixAppWeb do pipe_through :api get "/health", HealthController, :indexendLogging Configuration
Configure logging in config/prod.exs:
config :logger, level: :info, compile_time_purge_matching: [ [level_lower_than: :info] ]
config :logger, :console, format: {Jason, :encode!}, metadata: [:request_id, :user_id]Monitoring with Telemetry
Setup telemetry for monitoring:
defmodule MyPhoenixApp.Telemetry do use Supervisor
def start_link(arg) do Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end
@impl true def init(_arg) do children = [ {:telemetry_poller, telemetry_poller_metrics()} ]
Supervisor.init(children, strategy: :one_for_one) end
defp telemetry_poller_metrics do [ {:metrics, [ Telemetry.Metrics.ProcessorCounter.new("vm.memory.total"), Telemetry.Metrics.ProcessorCounter.new("vm.memory.processes_used") ]} ] endendCustom Domains
To use a custom domain with your Phoenix app on Klutch.sh:
-
Access the app settings in your Klutch.sh dashboard
-
Navigate to the “Domains” section
-
Add your custom domain (e.g.,
example.com) -
Update your DNS records at your domain registrar:
Create a CNAME record pointing to
example-app.klutch.shName: wwwType: CNAMEValue: example-app.klutch.sh -
Wait for DNS propagation (usually 5-30 minutes)
-
Verify the domain in Klutch.sh
Your app will be accessible at www.example.com.
Troubleshooting
1. App Won’t Start - Mix Compile Errors
Problem: Deployment fails with “error: could not compile application”
Solution:
- Ensure
mix.exs,mix.lock, andassets/package-lock.jsonare committed to Git - Run
mix deps.get && mix compilelocally to verify the build works - Check for deprecated Elixir/Phoenix APIs and update code
- Verify all dependencies are compatible with your Elixir version
2. Database Connection Failures
Problem: Application crashes with “connection refused” or “econnrefused”
Solution:
- Verify
DATABASE_URLis set correctly with proper credentials and host - Ensure the database is accessible from the application container
- Test the connection string locally:
mix ecto.create - Check database firewall rules allow connections from Klutch.sh
- Set
ssl: truein your database config if required
3. Port Binding Issues
Problem: Application fails with “address already in use” or “eaddrinuse”
Solution:
- Verify your
config/prod.exsbinds to0.0.0.0not127.0.0.1 - Set
PORT=4000environment variable correctly - Check that the Dockerfile exposes port 4000
- Confirm the internal port in Klutch.sh is set to 4000
4. Asset Compilation Failures
Problem: Stylesheets and JavaScript not loading, or build fails during mix assets.deploy
Solution:
- Ensure
assets/package.jsonandassets/package-lock.jsonare committed - Run
npm install --prefix assets && npm run deploy --prefix assetslocally - Check Node.js version compatibility
- Verify all CSS/JS dependencies are correctly specified
- Look for errors in
npm run deployoutput
5. Release Build Failures
Problem: Build fails during mix release or app won’t start after release
Solution:
- Run
mix releaselocally to verify it works - Check all environment variables are set correctly
- Ensure
SECRET_KEY_BASEis at least 32 characters - Review
rel/overlays/directory if you have custom configurations - Check application startup code in
lib/my_phoenix_app/application.ex
Best Practices for Phoenix on Klutch.sh
-
Use Ecto changesets thoroughly - Leverage Ecto’s changesets for data validation and transformation, making your code safer and more maintainable.
-
Organize code with contexts - Use domain-driven design with Phoenix contexts to organize your application logic and keep modules focused.
-
Implement proper supervision - Design your supervision tree carefully to handle failures gracefully and leverage Erlang’s fault tolerance.
-
Use Plug pipelines effectively - Create reusable plugs for common concerns like authentication, authorization, and logging.
-
Monitor application health - Implement health checks and telemetry to understand your application’s behavior in production.
-
Configure database connection pooling - Set
POOL_SIZEappropriately based on your concurrency needs and database capacity. -
Use LiveView for real-time features - Leverage Phoenix LiveView to build real-time, interactive applications without writing JavaScript.
-
Implement proper error handling - Use Phoenix error handlers and Ecto error changesets to provide meaningful feedback to users.
-
Keep dependencies up to date - Regularly update Elixir, Phoenix, and other dependencies to benefit from improvements and security patches.
-
Test with proper OTP applications - Ensure your tests properly start the OTP application and clean up resources between tests.