Back to Blog

Heroku to Convox: A Migration Guide for Growing Teams

You open your email on the first of the month and there it is: your Heroku invoice. The number is larger than last month. Again. Your team added two more dynos to handle traffic spikes, upgraded to Standard-2X to stop the memory warnings, and suddenly you are paying $800 for infrastructure that used to cost $200. The worst part is not the amount itself. It is that you cannot predict what next month will look like.

This moment happens to nearly every growing startup on Heroku. The platform that made your early days so productive starts to feel like a tax on your success. You begin searching for Heroku alternatives, reading comparison posts, and wondering if the migration pain is worth the savings.

Here is the good news: migrating from Heroku to Convox is genuinely straightforward. You do not need to learn Kubernetes. You do not need to hire a DevOps engineer. If you can run heroku config and edit a YAML file, you can complete this migration in an afternoon. This guide walks you through every step with concrete examples for Rails, Node.js, and Python apps.

Why Teams Migrate from Heroku

Before diving into the technical details, let us acknowledge the reasons teams start looking for alternatives. Heroku's pricing model creates several pain points as you scale:

Unpredictable costs. Dyno pricing scales linearly while your needs scale non-linearly. Adding a worker dyno doubles your compute cost even if that worker runs at 5% CPU most of the day. Heroku Postgres pricing jumps dramatically at tier boundaries.

Limited visibility. You pay for a managed platform, but debugging production issues often requires more access than Heroku provides. When something goes wrong at 2 AM, you want to see what is actually happening on your infrastructure.

Vendor lock-in concerns. Your entire deployment pipeline is tied to Heroku's proprietary system. Moving to any other platform requires learning an entirely new paradigm. Convox addresses this by running on standard Kubernetes, but you interact with it through a Heroku-like CLI experience.

Convox Cloud Machines: Heroku Simplicity with Transparent Pricing

Convox Cloud Machines provides the developer experience you loved about Heroku with pricing you can actually predict. Machines start at $12 per month with 250 free hours included. You know exactly what you are paying for because you select the machine size and count yourself.

The deployment model will feel familiar. You push code, Convox builds it, and your app is running. No Kubernetes manifests. No load balancer configuration. No certificate management. Convox handles all of that the same way Heroku does, but with the flexibility to scale down costs as your usage patterns become clear.

Cloud Databases offer managed PostgreSQL, MySQL, and MariaDB with the same transparent pricing model. You pick the instance size and storage amount. No surprise charges for IOPS or connections.

Step 1: Convert Your Procfile to convox.yml

Your Heroku app uses a Procfile to define processes. Convox uses a convox.yml file that serves the same purpose with a bit more structure. The conversion is mechanical.

Here is a typical Heroku Procfile for a Rails application:

web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
release: bundle exec rails db:migrate

The equivalent convox.yml looks like this:

environment:
  - DATABASE_URL
  - REDIS_URL
  - RAILS_ENV=production
  - SECRET_KEY_BASE

services:
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    port: 3000
    scale:
      count: 2
      memory: 512

  worker:
    build: .
    command: bundle exec sidekiq
    scale:
      count: 1
      memory: 512

Notice a few differences. First, environment variables are declared explicitly. This catches missing configuration before deployment rather than at runtime. Second, each service has its own scaling configuration. Your web service can run on larger machines than your worker if that matches your actual resource needs.

For the release process (database migrations), Convox handles this through a deployment hook. Add the initContainer attribute to your web service:

services:
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    port: 3000
    initContainer:
      command: bundle exec rails db:migrate
    scale:
      count: 2
      memory: 512

The init container runs before the main container starts, ensuring your database is migrated before new code receives traffic. This mirrors how Heroku's release phase works.

Node.js Procfile Conversion

A typical Node.js Procfile:

web: npm start
worker: node worker.js

Becomes this convox.yml:

environment:
  - NODE_ENV=production
  - DATABASE_URL
  - PORT=3000

services:
  web:
    build: .
    command: npm start
    port: 3000
    scale:
      count: 2
      memory: 256

  worker:
    build: .
    command: node worker.js
    scale:
      count: 1
      memory: 256

Python Procfile Conversion

A Django or Flask Procfile:

web: gunicorn myapp.wsgi:application
worker: celery -A myapp worker -l info
release: python manage.py migrate

Converts to:

environment:
  - DJANGO_SETTINGS_MODULE=myapp.settings.production
  - DATABASE_URL
  - SECRET_KEY

services:
  web:
    build: .
    command: gunicorn myapp.wsgi:application --bind 0.0.0.0:8000
    port: 8000
    initContainer:
      command: python manage.py migrate
    scale:
      count: 2
      memory: 512

  worker:
    build: .
    command: celery -A myapp worker -l info
    scale:
      count: 1
      memory: 512

See the convox.yml reference for the complete list of configuration options.

Step 2: Create a Dockerfile

Heroku uses buildpacks to detect your language and build your application. Convox uses Dockerfiles, which gives you more control while remaining straightforward for standard apps. If you have never written a Dockerfile, do not worry. The templates below work for most applications.

Node.js Dockerfile

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000
CMD ["npm", "start"]

Rails Dockerfile

FROM ruby:3.2-slim

RUN apt-get update -qq && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
    bundle config set --local without 'development test' && \
    bundle install

COPY . .

RUN bundle exec rails assets:precompile

EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Python (Django) Dockerfile

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"]

Place the Dockerfile in your project root alongside convox.yml. For more details on optimizing your Dockerfile, see the Dockerfile documentation.

Step 3: Migrate Your Environment Variables

Export your current Heroku configuration:

heroku config -a your-app-name

This outputs something like:

=== your-app-name Config Vars
DATABASE_URL:     postgres://user:pass@host:5432/db
REDIS_URL:        redis://h:pass@host:6379
SECRET_KEY_BASE:  abc123def456...
RAILS_ENV:        production

Set these on your Convox app using the CLI. First, create your app:

convox cloud apps create your-app-name -i your-machine

Then set your environment variables:

convox cloud env set \
  SECRET_KEY_BASE=abc123def456... \
  RAILS_ENV=production \
  -a your-app-name \
  -i your-machine

Do not migrate DATABASE_URL and REDIS_URL yet. You will set those after creating your Convox resources. See the environment variables documentation for more options including the --replace flag for bulk updates.

Step 4: Migrate Your Database

Database migration is often the most anxiety-inducing part of moving platforms. The process is actually straightforward with pg_dump and a few careful steps.

Create a Convox Database

First, provision a Cloud Database. Add a resource to your convox.yml:

resources:
  database:
    type: postgres
    options:
      storage: 20

services:
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    port: 3000
    resources:
      - database

Deploy the app to create the database:

convox cloud deploy -a your-app-name -i your-machine

Export from Heroku Postgres

Put your Heroku app into maintenance mode to prevent writes during migration:

heroku maintenance:on -a your-app-name

Create a database dump:

heroku pg:backups:capture -a your-app-name
heroku pg:backups:download -a your-app-name

This creates a file called latest.dump in your current directory.

Import to Convox

Import the dump into your Convox database:

convox cloud resources import database -f latest.dump -a your-app-name -i your-machine

The DATABASE_URL environment variable is automatically set when you link a resource to a service. Convox handles the connection string for you.

Step 5: Zero-Downtime DNS Cutover

With your app deployed and database migrated, the final step is pointing your domain to Convox. This can be done with zero downtime if you follow the right sequence.

Get Your Convox Service URL

convox cloud services -a your-app-name -i your-machine

Output:

SERVICE  DOMAIN                                      PORTS
web      web.your-app.0a1b2c3d4e5f.convox.cloud     443:3000

Test Before Switching

Visit the Convox domain directly to verify everything works. Test critical paths: authentication, database queries, background jobs. Only proceed when you are confident the new deployment is functioning correctly.

Update Your DNS

If you use a custom domain, update your CNAME record to point to the Convox service domain. For example, if your app runs at app.yourcompany.com, update the CNAME from the Heroku target to:

app.yourcompany.com CNAME web.your-app.0a1b2c3d4e5f.convox.cloud

You can also configure custom domains directly in convox.yml:

services:
  web:
    build: .
    domain: app.yourcompany.com
    port: 3000

DNS propagation typically takes 5 to 30 minutes depending on your TTL settings. During this window, some users will hit Heroku and some will hit Convox. Since both are running the same code and share the same database (until you complete the migration), this is safe.

See the custom domains documentation for SSL certificate configuration.

Common Gotchas

Port Binding

Your application must bind to 0.0.0.0, not localhost or 127.0.0.1. Heroku apps usually handle this correctly, but double-check your server configuration. For Express.js apps, ensure you have app.listen(PORT, '0.0.0.0'). For Rails with Puma, your config/puma.rb should include bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}".

DATABASE_URL Format

Convox generates DATABASE_URL automatically when you link a resource to a service. The format is standard: postgres://user:password@host:5432/database. If your app parses this URL manually, ensure it handles the standard format. Most ORMs (ActiveRecord, Sequelize, SQLAlchemy) parse this correctly out of the box.

Environment Variable Declaration

Every environment variable your app uses must be declared in the environment section of convox.yml. Variables without a default value (like DATABASE_URL) must be set before deployment. Variables with defaults (like RAILS_ENV=production) use that default if not explicitly set. This catches configuration errors before they reach production.

After the Migration

Once DNS has propagated and you have verified everything works:

Turn off Heroku maintenance mode if you want to keep it as a fallback for a few days. Some teams run both platforms in parallel during a transition period.

Monitor your Convox deployment. Use convox cloud logs -a your-app-name to tail application logs. Check for errors during the first few hours of production traffic.

Scale as needed. Adjust your service scale in convox.yml and redeploy, or use the CLI:

convox cloud scale web --count 3 -a your-app-name -i your-machine

Decommission Heroku when you are confident in the new setup. Delete the Heroku app and its add-ons to stop billing. Keep your database backup for archival purposes.

Cost Comparison

Let us look at a real scenario. A typical Rails app on Heroku with 2 Standard-2X web dynos, 1 worker dyno, and Heroku Postgres Standard-0 costs approximately:

Component Heroku Convox Cloud
Web (2 instances) $100/mo (2x Standard-2X at $50) $24/mo
Worker (1 instance) $50/mo (Standard-2X) $12/mo
PostgreSQL $50/mo (Standard-0) Transparent pricing
Monthly Total $200+ From $36+

The exact savings depend on your specific configuration, but teams typically see 50-70% cost reduction. More importantly, you know what you are going to pay each month.

Get Started

Migrating from Heroku to Convox does not require becoming an infrastructure expert. The CLI commands will feel familiar. The deployment model works the same way. Your developers can continue using git push workflows without learning Kubernetes.

The Getting Started Guide walks through creating your first Cloud Machine and deploying an application in about 15 minutes.

Create a free Convox account to start your migration. Cloud Machines include 250 free hours per month, giving you plenty of time to test your migration before committing.

For teams with compliance requirements or complex architectures, Convox also offers BYOC (Bring Your Own Cloud) racks that deploy into your own AWS, GCP, or Azure account. Same developer experience, full infrastructure control.

Questions about your specific migration? Reach out to sales@convox.com and we will help you plan the transition.

Let your team focus on what matters.