Migrate from Easypanel
Move your services from Easypanel to sh0 one at a time, keeping Easypanel online until each one is verified.
Before You Start
This guide follows the migration we ran ourselves: about twenty production services from several projects, moved from Easypanel to sh0 and compared against production one by one. The method is deliberately sequential: deploy one service on sh0, compare it with what Easypanel serves, then move on.
1. List What You Run
For every Easypanel service, write down:
- Its source: Git repository and branch, the build path if the app lives in a subfolder, and the Dockerfile name if it is not the default one.
- Its environment variables, including the ones used only at build time.
- Its domains and the port the app listens on.
- Its data: databases it uses and volumes or mounts where it writes files.
- Whether it listens on a port at all. Queue consumers and schedulers do not, and are created differently on sh0.
2. Prepare the sh0 Server
Install sh0 on a separate server, not on the Easypanel machine: both need ports 80 and 443. Follow Installation, then create the owner account.
Before moving anything, check the host. The command reports Docker, the ports, disk space, certificates and the clock, and exits with 0 when nothing blocks:
sudo sh0 doctorA sample report and the meaning of each line are in Updates & Diagnostics.
3. Recreate Each App
Create each app on sh0 from the same source it uses on Easypanel:
- From your own machine, without any Git host: sh0 push uploads the project directory and builds it, with its Dockerfile or, if there is none, by detecting the stack.
- From the same Git repository and branch: see Git Deploy. Private repositories, non-default branches and repositories from another organization all worked in our migration.
- Monorepos: set the build path to the app’s subfolder, and the Dockerfile name if it is not Dockerfile. A named Dockerfile is resolved relative to the build path.
- Services without a port (Celery, BullMQ, a scheduler): create them as Worker (no port). sh0 then checks that the process stays up instead of waiting for a port.
Copy the environment variables before the first deployment, then compare the new app with production: same status codes on the same paths, same page title, same health endpoint answer.
4. Move the Databases
Create a database server on sh0 with the same engine and major version as the source. Then copy the data with the engine’s own dump and restore tools. The example below is PostgreSQL.
Replace EASYPANEL_DB with the container of the Easypanel database (find it with docker ps), and sh0-dbsrv-<name> with the container of the sh0 database server. Use the database name and user shown in the server’s connection details.
docker exec EASYPANEL_DB pg_dump -U app -d app -Fc > app.dumpdocker exec -i sh0-dbsrv-NAME pg_restore -U app -d app --no-owner --no-privileges < app.dump
docker exec sh0-dbsrv-NAME psql -U app -d app -tAc "select count(*) from orders"Copy the dump file between the two servers with scp or rsync. Then point the app’s DATABASE_URL (or equivalent) at the sh0 database server and redeploy the app.
5. Move Files and Volumes
Files written by an app (uploads, generated documents) live in a Docker volume. Copy the volume content through a tar archive, then add a volume with the same mount path to the app on sh0:
docker run --rm -v SOURCE_VOLUME:/data alpine:3.19 tar -czf - -C /data . > uploads.tgzdocker run --rm -i -v TARGET_VOLUME:/data alpine:3.19 tar -xzf - -C /data < uploads.tgz
docker run --rm -v TARGET_VOLUME:/data alpine:3.19 find /data -type fFind the volume names with docker volume ls on each server. On sh0, volume names start with sh0- and the app name.
What Behaves Differently
- Memory: Easypanel sets no limit by default; sh0 caps each app at 512 MB unless you raise it. A Python backend with four workers needed more in our migration and was stopped by the limit until we raised it.
- Service names: inside a project, an app reaches another app by its name or by
<app>.internal, which stays the same across deployments. Update hostnames that pointed at Easypanel service names. - Build context: sh0 reads your .dockerignore with Docker’s rules, so a project that builds on Easypanel builds the same way on sh0.
- A fresh database exposes what a long-lived one hides: in our migration, one migration chain could not replay from an empty database and one app created its tables from four workers at once. Those were bugs in the projects, not in either platform; restoring the data first avoids them.
6. Switch the Domains
- A day before, lower the TTL of each DNS record you will move, so the switch spreads quickly.
- Add the domain to the app on sh0.
- Point the DNS record at the sh0 server’s IP address. sh0 obtains the certificate on its own once the record resolves to it.
- Check the live domain against what Easypanel was serving.
- Only then stop the service on Easypanel. Keep its data until you are sure you will not need to go back.