If you already have a Docker container that runs an HTTP server, you can move it to fal with minimal changes. There are two common paths:
- Direct Server Mode: Deploy the container directly with
pyproject.toml.
- Proxy App Mode: Run the server inside a custom image and wrap it with
fal.App.
Use Direct Server Mode when your existing server API can be exposed as-is. Use Proxy App Mode when you want a typed Python API, Pydantic validation, fal CDN helpers, or Playground support.
Option 1: Direct Server Mode
Direct Server Mode routes incoming traffic straight to your container’s HTTP port.
Your server must bind to 0.0.0.0, listen on the configured exposed_port, and return HTTP 200 from GET /health.
[tool.fal.apps.my-server]
auth = "private"
machine_type = "GPU-A100"
exposed_port = 8000
keep_alive = 300
[tool.fal.apps.my-server.image]
dockerfile = "Dockerfile"
FROM your-base-image
# ... your setup
EXPOSE 8000
CMD ["your-server", "--host", "0.0.0.0", "--port", "8000"]
Run it:
Deploy it:
Your server’s API is exposed as-is. If it serves /generate, call:
curl -H "Authorization: Key $FAL_KEY" \
https://fal.run/<your-username>/my-server/generate
For queue-backed requests, submit to the same endpoint ID on queue.fal.run:
curl -X POST https://queue.fal.run/<your-username>/my-server/generate \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "a mountain at sunrise"}'
The Playground is not supported for Docker container apps. Use direct HTTP calls to fal.run or queue-backed calls to queue.fal.run.
Option 2: Proxy App Mode
Use fal.App to wrap your server with custom endpoints. The internal server runs inside the same container, while the public API is defined by Python methods.
import subprocess
import time
import fal
import requests
from fal.container import ContainerImage
from fal.toolkit import Image
from fastapi import Request
from pydantic import BaseModel, Field
DOCKERFILE = """
FROM your-base-image
# ... your setup
RUN pip install --no-cache-dir fal requests
"""
SERVER_PORT = 8000
class GenerateRequest(BaseModel):
prompt: str = Field(description="Text prompt")
class GenerateResponse(BaseModel):
image: Image
class MyServerProxy(fal.App, keep_alive=300, max_concurrency=1):
machine_type = "GPU-A100"
image = ContainerImage.from_dockerfile_str(DOCKERFILE)
def setup(self):
self.process = subprocess.Popen(
["your-server", "--host", "127.0.0.1", "--port", str(SERVER_PORT)],
)
self._wait_for_server()
def _wait_for_server(self, timeout=120):
start = time.time()
while time.time() - start < timeout:
try:
if requests.get(f"http://127.0.0.1:{SERVER_PORT}/", timeout=5).ok:
return
except requests.ConnectionError:
pass
time.sleep(1)
raise TimeoutError("Server did not start")
@fal.endpoint("/generate")
def generate(self, input: GenerateRequest, request: Request) -> GenerateResponse:
resp = requests.post(
f"http://127.0.0.1:{SERVER_PORT}/api/generate",
json={"prompt": input.prompt},
timeout=300,
)
resp.raise_for_status()
image = Image.from_path(resp.json()["path"], request=request)
return GenerateResponse(image=image)
Your fal.App controls the API. You can validate inputs, process outputs, upload files to the fal CDN, and use the Playground generated from your Pydantic schemas.
Using a Private Registry
If your Dockerfile pulls from a private base image, configure registry credentials in the image table:
[tool.fal.apps.my-server.image]
dockerfile = "Dockerfile"
[tool.fal.apps.my-server.image.registries."registry.example.com"]
username = "my-user"
password = "$REGISTRY_TOKEN"
Create the secret before deploying:
fal secrets set REGISTRY_TOKEN=<token>
Best Practices
Download model weights to persistent storage during startup rather than baking them into the Docker image. This keeps the image smaller and lets weights persist across runner restarts.
For Python proxy apps, install fal-specific packages such as fal, pydantic, protobuf, and boto3 at the end of the Dockerfile to avoid dependency conflicts. Direct Docker container apps that do not import fal do not need those packages.
Set keep_alive based on startup cost and traffic. A longer value avoids repeated cold starts for heavy models; a shorter value reduces idle billing for fast-starting servers.
Next Steps