{
  "manifest_version": "1",
  "slug": "migrate-heroku",
  "title": "Apps from Heroku",
  "headline": "Migrate a Heroku app to exe.dev",
  "description": "Move a Heroku app to an exe.dev VM: Procfile processes, config vars, Postgres, Key-Value Store, and Scheduler jobs, rehearsed while Heroku keeps serving.",
  "category": "migration",
  "tags": [
    "heroku",
    "migrate off heroku",
    "heroku alternative",
    "procfile",
    "heroku postgres",
    "heroku redis",
    "heroku scheduler",
    "buildpacks",
    "git push deploy"
  ],
  "url": "https://exe.dev/deploy/migrate-heroku",
  "markdown_url": "https://exe.dev/deploy/migrate-heroku.md",
  "upstream": {
    "website": "https://www.heroku.com/",
    "source": "https://github.com/heroku/cli",
    "docs": "https://devcenter.heroku.com/",
    "license": "ISC (Heroku CLI)"
  },
  "example_vm_name": "my-app",
  "time": "1 hour",
  "stack": [
    {
      "name": "Your app",
      "detail": "Each Procfile process as a systemd unit, built the way its buildpack built it; web on port 8000 behind exe.dev's HTTPS proxy",
      "port": 8000,
      "data": "~/app, from ~/app.git"
    },
    {
      "name": "PostgreSQL 18",
      "detail": "In place of Heroku Postgres, set up as in the PostgreSQL recipe and restored from Heroku's backup",
      "data": "/var/lib/postgresql/18/main, ~/postgres/backups"
    },
    {
      "name": "Valkey",
      "detail": "In place of Heroku Key-Value Store (Redis), on localhost"
    },
    {
      "name": "systemd timers",
      "detail": "In place of Heroku Scheduler"
    },
    {
      "name": "The move kit",
      "detail": "What Heroku handed over, the plan, check, move, and the report",
      "data": "~/heroku"
    }
  ],
  "access": [
    {
      "url": "https://my-app.exe.xyz/",
      "who": "private",
      "what": "the app, until you make the move"
    },
    {
      "url": "https://www.example.com/",
      "who": "public",
      "what": "the app, once your domain points at the VM and it's public"
    },
    {
      "url": "ssh my-app.exe.xyz",
      "who": "private",
      "what": "the shell, in place of `heroku run bash`; `git push exe main` deploys"
    }
  ],
  "inputs": [
    {
      "name": "HEROKU_APP",
      "description": "the Heroku app's name, as `heroku apps` lists it"
    }
  ],
  "delivers": [
    "The app's code, config vars, and a copy of its database on the VM, handed over by the owner with the Heroku CLI; no Heroku API token anywhere.",
    "Each Procfile process as a systemd unit, built the way its buildpack built it, with the web process answering on port 8000; workers and Scheduler jobs (as timers) installed but off until the move.",
    "`git push exe main` deploying like `git push heroku main`: build, release phase, restart, and a push rejected, with the previous release still running, if anything fails.",
    "`~/heroku/check` finding every page in `~/heroku/paths.txt` the same as on Heroku, and a nightly database backup in `~/postgres/backups`.",
    "A report in `~/heroku/report.md`: what moved, what changed for exe.dev, what stays with Heroku or its add-on vendors, and the owner's steps for the move."
  ],
  "deployable": true,
  "deploy": {
    "new_vm_url": "https://exe.dev/new?prompt=HEROKU_APP%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Apps%20from%20Heroku%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe.dev%2Fdeploy%2Fmigrate-heroku.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20HEROKU_APP%3A%20the%20Heroku%20app%27s%20name%2C%20as%20%60heroku%20apps%60%20lists%20it%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-app.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-app.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20The%20app%27s%20code%2C%20config%20vars%2C%20and%20a%20copy%20of%20its%20database%20on%20the%20VM%2C%20handed%20over%20by%20the%20owner%20with%20the%20Heroku%20CLI%3B%20no%20Heroku%20API%20token%20anywhere.%0A-%20Each%20Procfile%20process%20as%20a%20systemd%20unit%2C%20built%20the%20way%20its%20buildpack%20built%20it%2C%20with%20the%20web%20process%20answering%20on%20port%208000%3B%20workers%20and%20Scheduler%20jobs%20%28as%20timers%29%20installed%20but%20off%20until%20the%20move.%0A-%20%60git%20push%20exe%20main%60%20deploying%20like%20%60git%20push%20heroku%20main%60%3A%20build%2C%20release%20phase%2C%20restart%2C%20and%20a%20push%20rejected%2C%20with%20the%20previous%20release%20still%20running%2C%20if%20anything%20fails.%0A-%20%60~%2Fheroku%2Fcheck%60%20finding%20every%20page%20in%20%60~%2Fheroku%2Fpaths.txt%60%20the%20same%20as%20on%20Heroku%2C%20and%20a%20nightly%20database%20backup%20in%20%60~%2Fpostgres%2Fbackups%60.%0A-%20A%20report%20in%20%60~%2Fheroku%2Freport.md%60%3A%20what%20moved%2C%20what%20changed%20for%20exe.dev%2C%20what%20stays%20with%20Heroku%20or%20its%20add-on%20vendors%2C%20and%20the%20owner%27s%20steps%20for%20the%20move.%0A%0ARead%20the%20whole%20recipe%2C%20including%20the%20closed%20details%2C%20and%20do%20%22Rehearse%20the%0Amove%20by%20hand%22%20in%20order%2C%20skipping%20VM%20creation%3B%20the%20walkthrough%20above%20it%20is%0Afor%20the%20owner.%20Use%20its%20scripts%20and%20units%20as%20written%3A%20they%20were%20tested%0Atogether%2C%20and%20move%20and%20check%20rely%20on%20them.%20Change%20them%20only%20where%20this%0Aapp%20needs%20it%20%28another%20Procfile%20type%2C%20a%20build%20step%2C%20a%20Scheduler%20job%29%2C%20and%0Alist%20the%20changes%20in%20the%20report.%20For%20PostgreSQL%2C%20follow%20the%20PostgreSQL%0Arecipe%27s%20steps%20as%20%22Install%20PostgreSQL%20and%20Valkey%22%20says%20%28its%20Markdown%20is%0A%2Fdeploy%2Fpostgres.md%20on%20the%20same%20site%29%2C%20skipping%20Adminer.%0AYou%20need%20the%20owner%20once%2C%20early%3A%20show%20them%20the%20commands%20in%0A%22Hand%20over%20the%20app%22%20with%20this%20VM%27s%20name%20and%20HEROKU_APP%2C%20and%20wait%20until%0A~%2Fheroku%2Fconfig.json%20is%20there%20and%20~%2Fapp.git%20has%20a%20commit.%20Install%20what%0Adoesn%27t%20depend%20on%20them%20meanwhile.%0ANever%20run%20heroku%20commands%20yourself%2C%20and%20never%20write%20to%20Heroku%27s%20database%2C%0AKey-Value%20Store%2C%20or%20other%20add-ons%3A%20the%20URLs%20in%20~%2Fheroku%2Fconfig.json%20are%0Alive%20production%20ones.%20Never%20run%20~%2Fheroku%2Fmove%2C%20change%20DNS%2C%20or%20run%20share%0Aset-public%3A%20the%20move%20is%20the%20owner%27s%20call.%20Until%20then%2C%20keep%20the%20workers%0Aand%20Scheduler%20timers%20off%20on%20this%20VM.%0AKeep%20config%20var%20values%20and%20the%20backup%20URL%20out%20of%20your%20replies%20and%20the%0Areport.%0AIf%20the%20app%20relies%20on%20something%20exe.dev%20can%27t%20run%20%28Heroku%20Connect%2C%20Apache%0AKafka%20on%20Heroku%2C%20inbound%20ports%20other%20than%20HTTP%20and%20SSH%29%2C%20don%27t%20work%0Aaround%20it%20silently%3A%20put%20it%20in%20the%20report%20and%20ask.%0AFinish%20with%20the%20report%27s%20summary%2C%20https%3A%2F%2F%3Cvm%3E.exe.xyz%2F%2C%20and%20the%20owner%27s%0Asteps%20in%20%22Make%20the%20move%22%20with%20this%20VM%27s%20name%2C%20the%20Heroku%20app%2C%20and%20its%0Adomains.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A",
    "terminal": "curl -fsSL https://exe.dev/deploy/migrate-heroku/prompt.txt | ssh exe.dev new --prompt=/dev/stdin",
    "prompt": "HEROKU_APP=\u003cfill-this-in\u003e\n\nSet up Apps from Heroku on this VM by following the exe.dev recipe at https://exe.dev/deploy/migrate-heroku.md from top to bottom.\n\nThe values at the top of this message are mine:\n- HEROKU_APP: the Heroku app's name, as `heroku apps` lists it\nAsk me for any that still say \u003cfill-this-in\u003e before you need them.\n\nThe recipe's example VM name is my-app. This VM's name is the output of `hostname`: use it wherever the recipe writes my-app. Skip the step that creates the VM; you are on it.\n\nYou are done when all of these are true. Check each one yourself:\n- The app's code, config vars, and a copy of its database on the VM, handed over by the owner with the Heroku CLI; no Heroku API token anywhere.\n- Each Procfile process as a systemd unit, built the way its buildpack built it, with the web process answering on port 8000; workers and Scheduler jobs (as timers) installed but off until the move.\n- `git push exe main` deploying like `git push heroku main`: build, release phase, restart, and a push rejected, with the previous release still running, if anything fails.\n- `~/heroku/check` finding every page in `~/heroku/paths.txt` the same as on Heroku, and a nightly database backup in `~/postgres/backups`.\n- A report in `~/heroku/report.md`: what moved, what changed for exe.dev, what stays with Heroku or its add-on vendors, and the owner's steps for the move.\n\nRead the whole recipe, including the closed details, and do \"Rehearse the\nmove by hand\" in order, skipping VM creation; the walkthrough above it is\nfor the owner. Use its scripts and units as written: they were tested\ntogether, and move and check rely on them. Change them only where this\napp needs it (another Procfile type, a build step, a Scheduler job), and\nlist the changes in the report. For PostgreSQL, follow the PostgreSQL\nrecipe's steps as \"Install PostgreSQL and Valkey\" says (its Markdown is\n/deploy/postgres.md on the same site), skipping Adminer.\nYou need the owner once, early: show them the commands in\n\"Hand over the app\" with this VM's name and HEROKU_APP, and wait until\n~/heroku/config.json is there and ~/app.git has a commit. Install what\ndoesn't depend on them meanwhile.\nNever run heroku commands yourself, and never write to Heroku's database,\nKey-Value Store, or other add-ons: the URLs in ~/heroku/config.json are\nlive production ones. Never run ~/heroku/move, change DNS, or run share\nset-public: the move is the owner's call. Until then, keep the workers\nand Scheduler timers off on this VM.\nKeep config var values and the backup URL out of your replies and the\nreport.\nIf the app relies on something exe.dev can't run (Heroku Connect, Apache\nKafka on Heroku, inbound ports other than HTTP and SSH), don't work\naround it silently: put it in the report and ask.\nFinish with the report's summary, https://\u003cvm\u003e.exe.xyz/, and the owner's\nsteps in \"Make the move\" with this VM's name, the Heroku app, and its\ndomains.\n\nFinish with the URLs I should open.\n",
    "prompt_url": "https://exe.dev/deploy/migrate-heroku/prompt.txt"
  },
  "updated": "2026-10-17",
  "related": [
    "migrate-digitalocean-droplet",
    "postgres",
    "rails",
    "django",
    "dev-test-prod",
    "migrate-durable-objects"
  ],
  "alternatives": [
    "Heroku"
  ]
}
