Skip to main content

Long-running HTTP services

Use @app.inference() when a process should keep running and receive HTTP requests through a CGC URL. The workflow API creates the resource, waits when required, configures its ports, and returns a DeployedEndpoint handle.

This example deploys a small text analysis service from a custom Python image.

Define the service​

from cgc.sdk import App, Image, ShellCommand


RESOURCE_NAME = "text-analysis-service"
PORT = 8000
PORT_NAME = "api"

app = App(
name="http-service-workflow",
image=Image.from_registry("python:3.11-slim"),
)


@app.inference(
entity="custom",
name=RESOURCE_NAME,
port=PORT,
port_name=PORT_NAME,
cpu=1,
memory=2,
)
def text_analysis_service():
code = r"""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json


class Handler(BaseHTTPRequestHandler):
def _send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def do_GET(self):
if self.path in {"/health", "/api/health"}:
self._send_json(200, {"status": "ok"})
return
self._send_json(404, {"error": "not found", "path": self.path})

def do_POST(self):
if self.path not in {"/analyze", "/api/analyze"}:
self._send_json(404, {"error": "not found", "path": self.path})
return

length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length) or b"{}")
text = payload.get("text", "")
words = text.split()
self._send_json(
200,
{
"input": text,
"characters": len(text),
"words": len(words),
"uppercase": text.upper(),
"first_five_words": words[:5],
},
)

def log_message(self, format, *args):
print(format % args, flush=True)


ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
"""
return ShellCommand(executable=("python", "-c", code))

entity="custom" means that CGC runs the image selected in App. The decorated function runs locally and returns the command that CGC starts inside the container. ShellCommand keeps the executable and its arguments structured and quotes them when it renders the final shell command.

The server must listen on 0.0.0.0 and on the same port as port. In this example it listens on 8000.

Install startup dependencies​

When the selected image needs additional packages, install them in ShellCommand.prelude before starting the main process:

return ShellCommand(
executable=("python", "-m", "my_service"),
arguments=("--host 0.0.0.0", "--port 8000"),
prelude="python -m pip install --quiet fastapi 'uvicorn[standard]'",
)

The prelude runs on every container start. Package installation is not persisted in the image. For stable and fast startup, put fixed dependencies in your image; use prelude for small runtime additions and development workflows.

Deploy and call the service​

endpoint = text_analysis_service.deploy(
wait=True,
reuse_existing=False,
resource_timeout_seconds=600,
)

print("Endpoint:", endpoint.url)
endpoint.wait_http("/health", timeout_seconds=300)

result = endpoint.post_json(
"/analyze",
{"text": "CGC SDK workflows can run HTTP services."},
)
print(result)

deploy() first creates the resource and then configures the declared port. wait=True waits for the resource and refreshes its ingress URL. reuse_existing=False raises on a name conflict. True reuses the resource without updating its image, command or environment.

wait_http() waits until the application itself accepts requests. This is different from wait=True: a resource can be Running before its HTTP server is ready.

The endpoint URL may already contain the ingress path prefix derived from port_name. wait_http() and post_json() append the supplied path to that URL. The application must expose routes that match the resulting path. The complete example accepts both unprefixed routes such as /health and prefixed routes such as /api/health.

Custom Image Entrypoint Arguments​

Instead of a shell command or ShellCommand, a custom inference function may return a list of arguments:

from cgc.sdk import App, Image

args_app = App("argument-demo", image=Image.from_registry("python:3.12-slim"))

@args_app.inference(name="argument-demo", port=8000, port_name="http")
def serve_with_arguments():
return ["python", "-m", "http.server", "8000"]

plan = serve_with_arguments.deploy(dry_run=True)

The backend parses the list. See the API reference for space and repeated-key limitations. Use ShellCommand for complex commands.

Add more ports​

Use ServicePort when one resource listens on more than one port:

from cgc.sdk import App, Image, ServicePort, ShellCommand

multi_app = App("multi-port-demo", image=Image.from_registry("python:3.12-slim"))

@multi_app.inference(
name="multi-port-service", port=8000, port_name="http",
additional_ports=[
ServicePort("metrics", 9090, ingress=False),
ServicePort("internal", 5557, ingress=False),
],
)
def service():
code = r"""
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = json.dumps({"port": self.server.server_port, "status": "ok"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

for port in (8000, 9090, 5557):
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
threading.Event().wait()
"""
return ShellCommand(executable=("python", "-u", "-c", code))

plan = service.deploy(dry_run=True)

The SDK creates the resource first, waits for Running, and then adds the primary and additional ports. Port names and numbers must be unique. Each port must be between 1 and 65535. Setting ingress=False keeps a port without a public ingress URL. Declaring a port does not start a listener; the application inside the container must listen on it.

If port configuration fails, the SDK preserves the resource and reports the error so it can be inspected or deleted explicitly.

Additional ports force a wait for Running even with wait=False. With primary ingress=False, do not use endpoint.url, wait_http() or post_json(); internal ports are reachable from workloads in the same namespace.

Use a CGC resource template​

For a template, omit image and startup_command. This declaration uses the Nginx template defaults:

from cgc.sdk import App

template_app = App("template-service")
resource = template_app.resource(
entity="nginx", name="template-nginx", cpu=1, memory=1,
)
plan = resource.create(dry_run=True)

The entity must exist in the selected environment. Pass template arguments through App.resource(arguments=[...]) or return a list from an App.inference(entity=...) function.

Inspect and delete​

print(endpoint.status())
print(endpoint.logs(timestamps=True))
endpoint.delete()

Run the complete example with automatic cleanup:

python text_inference.py --delete

See the complete script on the Workflow examples page. For ready-made model APIs, continue with Ready-to-use workflows.