Docker Image or GitHub Repository: Which Way to Deploy, and the ARM64 Rule
SnapDeploy will take your app three ways: a GitHub repository, a public Docker image, or an uploaded archive. They end in the same place, a running container with an HTTPS URL, but they differ in what you control, how long a deploy takes, and one rule that trips people up more than any other: prebuilt images have to run on ARM64. This guide is the decision, with the trade-offs and the commands to check your image before you press Deploy.
The three paths
| GitHub repository | Public Docker image | Upload (tarball or JAR) | |
|---|---|---|---|
| What you give | A repo and branch; a Dockerfile if you have one | An image name such as ghcr.io/you/app:1.4 | A zip of the source, or a built JAR |
| Build | On the platform, from your Dockerfile or a generated one | None; the image is pulled as-is | On the platform, same as a repository |
| Time to live | A few minutes (build plus start) | About a minute (pull plus start) | A few minutes |
| Redeploy on push | Yes, automatically | No; you deploy a new tag | No; you upload again |
| Architecture | Handled for you (built for ARM64) | Must include an ARM64 variant | Handled for you |
| Best for | Your own apps; anything you change often | Off-the-shelf software; images you already build in CI | Private code without GitHub; Java builds from your machine |
Path 1: a GitHub repository
This is the default and the right choice for code you are working on. Connect the repository, pick the branch, and every push builds and deploys. If the repository has a Dockerfile, it is used exactly as written and the port is read from its EXPOSE line. If it does not, one is generated for Node.js, Python, Java, Go, PHP, Ruby or a static HTML site, with the start command inferred from the project (Gunicorn for Django and Flask, Uvicorn for FastAPI, npm start for Node, java -jar for Maven and Gradle builds). The generated file is a sensible first deploy; our language guides explain when to replace it with your own, usually for a newer runtime version or a multi-stage build.
Builds count against the free tier's deploy allowance (5 per rolling 12 hours, failed builds included), so the habit that pays for itself is docker build on your machine before you push. Environment variables that the code needs are detected from the repository (Dockerfile ENV lines, a .env.example, framework configuration files) and offered to you before the first deploy, so the usual "it crashed because SECRET_KEY was missing" round trip is skipped.
Path 2: a public Docker image
Give an image reference from Docker Hub, GitHub Container Registry or any public registry, set the port and the environment variables, and the container is live in about a minute. No build, no deploy allowance spent on builds, and it is the natural path for software you did not write (an admin tool, a dashboard, a bot) or for images your own CI already produces. What you give up is the push-to-deploy loop: a new version is a new tag and a manual deploy.
The ARM64 rule. Containers run on ARM64. Official images on Docker Hub are multi-architecture and work unchanged; many third-party images are built for x86 only, and those fail at start with a message that says the image was built for a different CPU architecture. Check before you deploy:
docker manifest inspect ghcr.io/you/app:1.4 | grep -A1 '"architecture"'
# you want to see "arm64" among the platformsIf it is x86-only and you control the build, produce a multi-architecture image once and forget about it:
docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/you/app:1.4 --push .If you do not control the build, deploy the project's repository instead and let the platform build it for ARM64, or find the project's arm64 tag; most maintained images have one.
Path 3: an upload
A zip of the source goes through the same build as a repository, without GitHub in the loop; a JAR is wrapped in a Java runtime and started. It suits code that lives somewhere GitHub is not, and Java projects you would rather build locally. Redeploys are manual, so it is the least convenient path for anything that changes daily.
Choosing, in one paragraph
Your own app that you change: repository. Someone else's software, or an image your CI already builds for ARM64: image. Code that cannot be on GitHub: upload. If you are unsure, start with the repository; the generated Dockerfile gets a first version live, and switching to an image later is a matter of pointing the container at a tag.
What is the same on every path
- The container reads
PORTfrom the environment or you set the port in the container settings; the app must listen on0.0.0.0, not localhost. - Environment variables are set in the dashboard and injected at start; database add-ons inject their connection string automatically.
- The filesystem is ephemeral: anything that must survive a redeploy or a wake goes in a database or object storage.
- Free containers sleep after 15 minutes without traffic and wake in about a minute; Always-On ($12/month per container) never sleeps and unlocks custom domains.
- Build and runtime logs are in the dashboard; when a deploy fails, the reason (wrong port, x86 image, missing library, a start command pointing at a file that is not there) is stated at the top of the deployment.
Trying each path in five minutes
- Image: deploy
nginx:alpine(multi-architecture) on port 80. You have a page in about a minute. Deploying a sample from the first-deploy wizard is the same path and does not count against deploys. - Repository: push a folder with an
index.htmland no Dockerfile; watch the generated static build, then add your own Dockerfile and see it take over. - Upload: zip the same folder and upload it. Same result, no GitHub involved.
Free tier: 4 containers, 512 MB and 0.25 vCPU each, 100 container-hours a month, 10 deploys a day, no card. Details on the free tier docs page; the Docker hosting page covers the image path in more depth.