Python on Previewops
Previewops builds whatever your Dockerfile produces and runs it as a container, so Python needs no special support. What it does need is what every stack needs: read the injected PORT and listen on 0.0.0.0.
| Read the port | os.environ["PORT"] |
| Bind all interfaces | gunicorn -b 0.0.0.0:$PORT |
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8080
CMD ["sh", "-c", "gunicorn -b 0.0.0.0:$PORT app:app"]
ENV PORT=8080 is only a default. Previewops overrides it at deploy time, so the app must read the value rather than assume it.
Application code
A typical setup with Flask or FastAPI behind Gunicorn:
import os
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "ok"
if __name__ == "__main__":
# Only used for local runs — in the container Gunicorn binds the socket.
port = int(os.environ.get("PORT", 8080))
app.run(host="0.0.0.0", port=port)
Python-specific pitfalls
flask runbinds 127.0.0.1. It is a development server on loopback — pass--host=0.0.0.0, or use Gunicorn/Uvicorn as the Dockerfile above does.$PORTneeds a shell to expand. In exec form,CMD ["gunicorn", "-b", "0.0.0.0:$PORT"]passes the literal string$PORT. Wrap it insh -cas shown.- Uvicorn equivalent:
uvicorn app:app --host 0.0.0.0 --port $PORTfor ASGI apps.
Configuration
If your Dockerfile is not at the repository root, point at it from .previewops.yaml:
provider: cloud-run
dockerfile: ./docker/Dockerfile
port: 8080
ttl_hours: 24
See configuration.md for every available option.
Do not gate startup behind a migration
Language-agnostic, and the hardest failure here to diagnose:
# Avoid this
CMD ["sh", "-c", "run-migrations && start-server"]
If the migration cannot reach the database — a paused instance, an expired connection string, a cold-start timeout — it never exits, the server never starts, and the port never opens. The deploy itself already succeeded, so the preview shows as Live while nothing answers. Run migrations as a separate, visible step instead.
Related pages
- getting-started.md — your first deploy end to end
- configuration.md —
.previewops.yamlreference - databases.md — connecting a database to a preview
- ui-deploy.md — deploying from the dashboard