Your first deployment
starts here.
This guide shows how to get an app ready for Deploy.et: how deploys work, what your code needs, and how to use PostgreSQL and object storage. Deploy.et is still in development, so dashboard steps and variable names may change before launch. Everything here is good practice for any modern host, so the work carries over either way.
Deploying an app?
Start with Prepare your app and Environment variables.
Storing files?
Go to Object storage, or Moving from MinIO if you already have data.
01 / How deploys work
- Connect a repository from GitHub or GitLab and choose a branch.
- Push code. Every push to that branch starts a new deployment.
- We build it in an isolated environment, using your framework’s default commands or the ones you set.
- We start it and check its health. Your previous version keeps serving traffic in the meantime.
- Traffic switches over only once the new version is healthy. If it isn’t, nothing changes for your users, and you can read the logs to see why.
Every deployment is kept, so you can roll back to an earlier one in one click.
02 / Prepare your app
Every app needs three things: it listens on the PORT environment variable, it listens on 0.0.0.0 rather than localhost, and it has a production start command. Here’s what that looks like for each stack.
Next.js
{
"scripts": {
"build": "next build",
"start": "next start -p ${PORT:-3000}"
}
}Node.js (Express)
const express = require("express");
const app = express();
app.get("/health", (req, res) => res.send("ok"));
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0", () => console.log(`Listening on ${port}`));Django
Add gunicorn and dj-database-url to requirements.txt, then read settings from the environment.
import os
import dj_database_url
DEBUG = False
ALLOWED_HOSTS = [".deploy.et"]
CSRF_TRUSTED_ORIGINS = ["https://*.deploy.et"]
DATABASES = {"default": dj_database_url.config(conn_max_age=600)}
STATIC_ROOT = BASE_DIR / "staticfiles"gunicorn myproject.wsgi --bind 0.0.0.0:$PORTFastAPI
uvicorn main:app --host 0.0.0.0 --port $PORTDocker
If there’s a Dockerfile in your repository, we use it. Any language works, as long as the container listens on PORT.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]Include package-lock.json, pnpm-lock.yaml, poetry.lock, or a pinned requirements.txt. It makes builds on our servers match your laptop.
03 / Environment variables
Set your own variables and secrets in your app’s settings. They’re encrypted and available at build time and when the app runs. Changing a variable starts a new deployment. We also add these for you:
| Variable | Added | What it is |
|---|---|---|
PORT | Always | The port your app must listen on. |
DATABASE_URL | When a database is attached | PostgreSQL connection string. |
S3_ENDPOINT | When a bucket is attached | Your object storage endpoint URL. |
S3_REGION | When a bucket is attached | Region name to pass to your S3 client. |
S3_BUCKET | When a bucket is attached | The name of the attached bucket. |
S3_ACCESS_KEY_ID | When a bucket is attached | Access key for that bucket. |
S3_SECRET_ACCESS_KEY | When a bucket is attached | Secret key for that bucket. |
Keep a .env.example file with placeholder values so your team knows which variables exist, and add .env to .gitignore.
04 / Health checks
After your app starts, we request a health path and wait for a 200 response before sending it traffic. While it runs, we keep checking, and restart the app if it stops responding. The default path is /; set a dedicated one like /health in your app’s settings.
// Fast, no login, no heavy database work
app.get("/health", (req, res) => res.status(200).send("ok"));05 / PostgreSQL
Create a database from your project and attach it to an app. DATABASE_URL is added automatically, and the connection between your app and database stays on our private network.
Run migrations on each deploy
Set a release command. It runs after the build and before the new version gets traffic. If it fails, the deploy stops and your current version keeps running.
# Django
python manage.py migrate --noinput
# Prisma
npx prisma migrate deploy
# Drizzle
npx drizzle-kit migrateConnect from your computer
Copy the external connection string from your database’s page, then use any PostgreSQL client.
psql "postgresql://USER:PASSWORD@HOST:5432/DATABASE?sslmode=require"Databases are backed up daily, and you can restore a backup into a new database. Exact retention per plan will be published before launch. For critical data, also keep your own copy with pg_dump.
06 / Object storage
Buckets are S3-compatible. Use them for user uploads, images, videos, exports, and backups, anything that shouldn’t live on your app’s disk. Any library or tool that works with Amazon S3 works here.
- Create a bucket from your project. Buckets are private: nothing is readable without keys or a presigned link.
- Attach it to your app. The
S3_*variables are added automatically. - Point your S3 client at
S3_ENDPOINTand turn on path-style addressing.
JavaScript / TypeScript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
});
// Upload a file
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: "avatars/user-42.png",
Body: fileBuffer,
ContentType: "image/png",
}));
// Share a private file for 1 hour
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: "avatars/user-42.png" }),
{ expiresIn: 3600 },
);Python
import os
import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url=os.environ["S3_ENDPOINT"],
region_name=os.environ["S3_REGION"],
aws_access_key_id=os.environ["S3_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"],
config=Config(s3={"addressing_style": "path"}),
)
bucket = os.environ["S3_BUCKET"]
s3.upload_file("report.pdf", bucket, "reports/report.pdf")
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": "reports/report.pdf"},
ExpiresIn=3600,
)Django file uploads
Install django-storages[s3] so FileField and ImageField uploads go straight to your bucket.
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"endpoint_url": os.environ["S3_ENDPOINT"],
"region_name": os.environ["S3_REGION"],
"bucket_name": os.environ["S3_BUCKET"],
"access_key": os.environ["S3_ACCESS_KEY_ID"],
"secret_key": os.environ["S3_SECRET_ACCESS_KEY"],
"addressing_style": "path",
},
},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}Private buckets
Nothing is readable without keys. Use presigned links to share a file for a limited time, like invoices or user documents.
Public buckets (later)
Buckets anyone can read, for product images and static assets, will come after launch for verified paid accounts. Until then, use presigned links.
Access keys
Create extra keys for each tool or teammate, read-only or read-write. Delete a key to revoke access instantly.
Command-line tools
rclone, mc (the MinIO client), s3cmd, and the AWS CLI all work with your endpoint and keys.
07 / Moving from MinIO
MinIO and Deploy.et both speak the S3 API, so there’s no code to rewrite. You copy your objects, then change four environment variables. You can use rclone or mc.
Option A: rclone
[old]
type = s3
provider = Minio
endpoint = https://minio.example.com
access_key_id = YOUR_MINIO_KEY
secret_access_key = YOUR_MINIO_SECRET
[deployet]
type = s3
provider = Other
endpoint = YOUR_S3_ENDPOINT
access_key_id = YOUR_DEPLOYET_KEY
secret_access_key = YOUR_DEPLOYET_SECRET# 1. Copy everything (safe to run again; it only copies what changed)
rclone sync old:my-bucket deployet:my-bucket --progress
# 2. Confirm both sides match
rclone check old:my-bucket deployet:my-bucketOption B: mc (MinIO client)
mc alias set old https://minio.example.com YOUR_MINIO_KEY YOUR_MINIO_SECRET
mc alias set deployet YOUR_S3_ENDPOINT YOUR_DEPLOYET_KEY YOUR_DEPLOYET_SECRET
mc mirror old/my-bucket deployet/my-bucketSwitch your app over
- Create the bucket on Deploy.et first, with the same name if you can.
- Run the copy once while your old app is still live.
- Update
S3_ENDPOINT,S3_REGION, and your keys in your app’s settings, and deploy. - Run the copy one last time to catch files uploaded during the switch.
- Keep your MinIO server for a few days before shutting it down.
If your app stores full MinIO URLs in the database, like https://minio.example.com/my-bucket/photo.jpg, update those rows too. Storing only the object key, like photo.jpg, avoids this next time.
08 / Your app’s address
Every app gets its own address as soon as it deploys, like your-app.deploy.et. HTTPS certificates are issued and renewed for you, and plain HTTP requests are redirected to HTTPS. The app name you choose becomes part of the address.
Deploy.et doesn’t sell domains or connect domains you already own. Your app is always reached at its deploy.et address.
09 / Troubleshooting
Write logs to standard output and standard error, like console.log or Python’s print and logging. Anything written there shows up in your app’s logs.
The build succeeds but the deploy never goes live
Your app is probably listening on a fixed port like 3000, or only on 127.0.0.1. Listen on the PORT variable and on 0.0.0.0.
Health check failed
Make sure your health path returns a 200 status without needing a login or a database write, and responds within a few seconds of starting.
The app crashes right after starting
Open the application logs. A missing environment variable or a failed database connection is the most common cause.
Uploaded files disappear after a deploy
The app’s own disk is replaced on every deploy. Save uploads to object storage instead.
The build runs out of memory
Large Next.js or webpack builds can need more memory. Remove unused dependencies, or move to a plan with more memory.
“Access Denied” from object storage
Check that you’re using the keys for that bucket, that the key has write access if you’re uploading, and that path-style addressing is turned on.
10 / Referrals
Your referral link is on your account page. Share it with friends, classmates, or your team. A referral counts when someone signs up with your link, verifies their account, and deploys an app. When 5 of your referrals are active, your Developer plan (799 ETB / month) is covered.
Proposed program. Final referral terms will be published before accounts open.
Get early access