Appi.sh is zero-config hosting for your apps. Build your app once, ship it with a single command, and it goes live — no servers to configure, no infrastructure to manage.
We use Docker as the delivery format — the standard way to package
modern applications so they run the same way everywhere. When you're ready,
you run the docker push
command generated for you on the Dashboard. As soon as your app arrives,
we detect it, start hosting it, and give you a public URL at
https://<your-slot-id>.appi.sh.
Never touched Docker before? You don't need to be an expert. If you're coding with an AI assistant (Cursor, Claude Code, Copilot, ChatGPT, Aider, etc.), ask it two things — it will walk you through everything:
Dockerfile
for your app — just tell it what framework or language you're using.
That's it. After docker push finishes,
give it a few seconds — the Dashboard will flip from
Waiting for deploy to Active.
Two addresses, one slot. The address you
push to
(push.to.appi.sh/<your-push-id>/app,
shown in the command on your Dashboard) is private — treat it like a
password, because anyone holding it can replace your running app. The address you
share
(https://<your-slot-id>.appi.sh)
is public and reveals nothing about the first. If your push command ever leaks,
hit Rotate push URL on the Dashboard: you get a new one
instantly and your live app keeps running.
Active means "we've received your image and are working on it" —
not yet "your app is reachable". Next to the status you'll see a small
info icon — hover it to see the deployment sub-status. It walks through
Pushing → Deploying → Awaiting Health Check → Healthy.
Once it reads Healthy, your app is actually live at
https://<your-slot-id>.appi.sh.
Add a port_<number>
tag to your image. Examples:
Full example build command:
docker buildx build --platform linux/amd64 ^ -t push.to.appi.sh/<your-push-id>/app:port_3000 ^ --push .
Add a tag in the format
env_<KEY>___<VALUE>
— one tag per variable.
Heads up on the separator:
between the key and the value there are three underscores
(___), not one.
This is because your key itself can contain single underscores
(e.g. DB_HOST),
so we need a distinct separator.
Allowed characters in both key and value follow Docker's tag rules:
letters a-z A-Z,
digits 0-9,
underscore _,
dot .,
and hyphen -.
Anything else — spaces, slashes, colons, @, !, etc. —
won't fit directly. For those, use
env64
(see below).
Examples:
Build command with multiple tags (one -t flag each):
docker buildx build --platform linux/amd64 ^ -t push.to.appi.sh/<your-push-id>/app:port_3000 ^ -t push.to.appi.sh/<your-push-id>/app:env_NODE_ENV___production ^ -t push.to.appi.sh/<your-push-id>/app:env_LOG_LEVEL___debug ^ --push .
Values with special characters — use env64.
If your value contains anything Docker tags don't allow (spaces, ://,
@,
:,
?,
=, etc.),
use the env64_ prefix and
base64url-encode the value. We decode it before passing the variable to your app.
Format: env64_<KEY>___<BASE64URL_VALUE>
base64url is the URL-safe variant: same alphabet as base64 but
+ becomes
-,
/ becomes
_,
and the = padding is dropped.
Quick encode commands:
# Linux / macOS echo -n 'your value here' | base64 | tr '+/' '-_' | tr -d '=' # Python python -c "import base64,sys; print(base64.urlsafe_b64encode(sys.argv[1].encode()).rstrip(b'=').decode())" 'your value here'
Example — setting DATABASE_URL=postgres://user:pass@host:5432/db:
-t push.to.appi.sh/<your-push-id>/app:env64_DATABASE_URL___cG9zdGdyZXM6Ly91c2VyOnBhc3NAaG9zdDo1NDMyL2Ri
Tip: the KEY stays plain — only the VALUE gets encoded. Docker tags cap at 128 characters total, so very long values may still need an external secrets approach.
Every account gets 1 free slot with a lifetime of up to 4 hours per deployment. When the slot expires, your containers are stopped automatically.
The clock starts at your first push. Push as often as you like while the slot is running — the image updates, the clock does not. Need longer? Click Extend slot on the Dashboard for a fresh 4 hours, as many times as you want.
After the slot expires, the next push restarts it on its own once
8 hours have passed — so coming back tomorrow is
just another docker push.
Want it back sooner? Click Renew slot on the
Dashboard and push again.
Paid slots extend the lifetime up to 1 month per deployment, and will soon allow multiple slots per account.
We do not read, inspect or retain the contents of your images beyond what's required to host them and to keep the platform safe. Every running container is checked automatically for abuse patterns. Your image lives in a private registry scoped to your account.
We also store operational metadata — image digest, tags, timestamps, health-check results — to run the service itself.
Just run the same docker push
command again with your new build. The service detects the new image, replaces the running
container, and keeps the same public URL.
The previous deployment is marked as superseded automatically — no manual cleanup required.
Images must be built for linux/amd64.
Our generated build command includes --platform linux/amd64 —
keep that flag even if you're on Apple Silicon or ARM.
Any language / framework that produces a Docker image works: Node, Python, Go, Rust, Java, .NET, PHP, Ruby, static sites via nginx, etc.
When the expiration time hits, we stop your containers and mark the slot as Expired on the Dashboard. Your image itself remains in the registry — nothing is deleted.
To resume hosting, just push again (a new image or the same one) — a fresh 4 hours start. The one exception: within 8 hours after expiry a plain push is refused; to restart sooner, open the Dashboard and click Renew slot first. That gap is what keeps free slots from being kept alive by a script.
Built-in databases — coming soon. We're working on the ability to spin up a database alongside your app as part of the deployment itself, so demo apps can run end-to-end out of the box with no external dependencies.
For now, use an external database (Supabase, Neon, PlanetScale,
Railway, your own server — anything that speaks the wire protocol) and pass the connection
string through an env64
variable (base64url-encoded, so
://,
@,
:,
? etc. all pass through):
-t push.to.appi.sh/<your-push-id>/app:env64_DATABASE_URL___cG9zdGdyZXM6Ly91c2VyOnBhc3NAaG9zdDo1NDMyL2Ri
The example above decodes to
postgres://user:pass@host:5432/db.
See the "How do I pass environment variables?" section for the full
env64 format and encoding commands.
Yes — that's one of the things APPISH is built for. Push your image once,
paste https://<your-slot>.appi.sh
into your README under a "Try it live" heading, and visitors can poke at your
project without cloning anything.
Keeping the demo alive: a free slot runs for 4 hours. Pushing again swaps the image but does not reset the clock. So before a release announcement, a Show HN post, or a PR review you expect to get traffic on, open the Dashboard and click Extend slot for a fresh 4 hours — there is no limit on how often. Once a slot has expired, a push brings it back by itself after 8 hours, or immediately after a Renew slot click. For demos that need to stay up indefinitely, a paid slot extends the lifetime up to one month per deployment and renews on every push.
Join our Discord community to chat with the team, ask questions, and share what you're building: appi.sh/discord.
Prefer email? Drop us a line at [email protected] — any question, bug report or feature request is welcome.