Skip to content
Deploy.etPREVIEW
PRE-LAUNCH GUIDE · DETAILS MAY CHANGE

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

  1. Connect a repository from GitHub or GitLab and choose a branch.
  2. Push code. Every push to that branch starts a new deployment.
  3. We build it in an isolated environment, using your framework’s default commands or the ones you set.
  4. We start it and check its health. Your previous version keeps serving traffic in the meantime.
  5. 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

package.json
{
  "scripts": {
    "build": "next build",
    "start": "next start -p ${PORT:-3000}"
  }
}

Node.js (Express)

server.js
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.

settings.py
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"
Start command
gunicorn myproject.wsgi --bind 0.0.0.0:$PORT

FastAPI

Start command
uvicorn main:app --host 0.0.0.0 --port $PORT

Docker

If there’s a Dockerfile in your repository, we use it. Any language works, as long as the container listens on PORT.

Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]
Commit your lockfile

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:

VariableAddedWhat it is
PORTAlwaysThe port your app must listen on.
DATABASE_URLWhen a database is attachedPostgreSQL connection string.
S3_ENDPOINTWhen a bucket is attachedYour object storage endpoint URL.
S3_REGIONWhen a bucket is attachedRegion name to pass to your S3 client.
S3_BUCKETWhen a bucket is attachedThe name of the attached bucket.
S3_ACCESS_KEY_IDWhen a bucket is attachedAccess key for that bucket.
S3_SECRET_ACCESS_KEYWhen a bucket is attachedSecret key for that bucket.
Never commit secrets

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.

A good health endpoint
// 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.

Release command examples
# Django
python manage.py migrate --noinput

# Prisma
npx prisma migrate deploy

# Drizzle
npx drizzle-kit migrate

Connect from your computer

Copy the external connection string from your database’s page, then use any PostgreSQL client.

Terminal
psql "postgresql://USER:PASSWORD@HOST:5432/DATABASE?sslmode=require"
Backups

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.

  1. Create a bucket from your project. Buckets are private: nothing is readable without keys or a presigned link.
  2. Attach it to your app. The S3_* variables are added automatically.
  3. Point your S3 client at S3_ENDPOINT and turn on path-style addressing.

JavaScript / TypeScript

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
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

pip install boto3
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.

settings.py
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

~/.config/rclone/rclone.conf
[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
Terminal
# 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-bucket

Option B: mc (MinIO client)

Terminal
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-bucket

Switch your app over

  1. Create the bucket on Deploy.et first, with the same name if you can.
  2. Run the copy once while your old app is still live.
  3. Update S3_ENDPOINT, S3_REGION, and your keys in your app’s settings, and deploy.
  4. Run the copy one last time to catch files uploaded during the switch.
  5. Keep your MinIO server for a few days before shutting it down.
Using public links?

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.

Custom domains aren’t supported

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