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.
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 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.
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.
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
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.
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.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
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"]
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.
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.
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.
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
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 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.
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.
convox cloud services -a your-app-name -i your-machine
Output:
SERVICE DOMAIN PORTS
web web.your-app.0a1b2c3d4e5f.convox.cloud 443:3000
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.
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.
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.
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.
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.
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.