Deploy a lightweight AI utility on Civo using relaxAI and Civo Database
This tutorial will guide you through building an internal AI utility using relaxAI, Civo Database, and Civo Kubernetes to create a customizable and deployable AI application.
Written by
Software Engineer @ GoCardless
Written by
Software Engineer @ GoCardless
Imagine having a lightweight AI assistant, like a summarizer or tone-adjuster, that your team can access with a click. This tutorial walks through building an internal AI utility using relaxAI (Civo’s privacy-first LLM), Civo Database (for dynamic prompt templates), and Civo Kubernetes for fast, affordable deployment.
As AI adoption surges with enterprises spending $13.8B on generative AI in 2024, teams increasingly want tools like rewriters or explainers. The implemented stack makes it easy: prompt templates live in Civo Database for real-time updates, while relaxAI handles secure inference via Llama 4. No redeploy needed to change behavior — just update the prompt.
By the end of this tutorial, there will be a fully functional AI application powered by a prompt-driven backend. This guide covers the setup of Civo Database, backend and frontend code, Docker containerization, and Kubernetes YAML configuration, all optimized for a fast, cost-effective developer workflow on Civo. Let’s get started!
Prerequisites
Before diving in, make sure you have the following in place:
- Civo account – (free credits available for new users) to create a K3s Kubernetes cluster.
- relaxAI API key – Available with the relaxAI Plus plan (API access is not included in the free tier).
- Docker installed – needed to build and push container images.
- Node.js 18+ – for building the React frontend.
- Python 3.11+ – for the FastAPI backend.
Now, let’s jump into building the app!
What this project builds
A lightweight web app that processes text using relaxAI, powered by flexible Civo Database-based prompt templates. Users input text, pick a tool (e.g., Summarizer or Rewriter), and get instant AI output.
Rewriter (Polite tone)
Transforms casual or blunt messages into professional, respectful ones, great for improving internal communication.
.png%3F2026-04-13T09%3A26%3A20.972Z&w=3840&q=100)
Source: Image by author
Summarizer
Condenses long content into key points, ideal for meeting notes, articles, or docs.

Source: Image by author
These core features show how prompt-driven logic + relaxAI = a fast, flexible internal AI assistant.
Step-by-step guide
Below are the detailed steps to build and deploy the AI utility. Each step includes code examples and explanations, so you can follow along or adapt the pieces you need.
Step 1: Set up your Civo Database
- Create a Database Instance
- In the Civo dashboard, click Databases, then Create Database.
- Choose MySQL as the engine, pick your plan and region, and give it a name (e.g.
ai-prompts-db).
- Retrieve connection etails
- After provisioning, click your new instance to view Host, Port, Username, and Password.
- Keep these handy, you’ll need them to connect from your app or local shell.
Step 2: Initialize your prompts table
- Install and Configure the MySQL Client
- Download the MySQL Installer and select Server only (this includes the MySQL CLI).
- If mysql isn’t recognized, add its
bin/directory (e.g.C:\Program Files\MySQL\MySQL Server 8.0\bin) to your systemPATH.
- Connect to your Civo Database
mysql -h <HOST> -P <PORT> -u <USERNAME> -p
Enter the password when prompted
- Create database & table
CREATE DATABASE IF NOT EXISTS ai_tools;USE ai_tools;CREATE TABLE IF NOT EXISTS prompts (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(100) NOT NULL UNIQUE,template TEXT NOT NULL);
- Seed initial prompt templates
INSERT INTO prompts (name, template) VALUES('summarizer', 'Summarize the following text: {text}'),('polite_rewriter', 'Rewrite this message in a polite tone: {text}'),('code_explainer', 'Explain this code in simple terms: {text}');
Tip: You can update any prompt template on the fly by executing:
UPDATE prompts SET template = '…' WHERE name = '…';
Your app will pick up changes on the next query.
Step 3: Build the backend (FastAPI + Civo Database + relaxAI)
Now, create a /process API in FastAPI to:
- Accept
{ "input": "...", "tool": "summarizer" }. - Fetch the matching template from Civo Database.
- Inject user input using .format(input=...).
Here’s an example app.py for the backend:
import osimport requestsimport mysql.connector # For connecting to MySQL databasefrom fastapi import FastAPI, HTTPException # Web framework and exception handlingfrom pydantic import BaseModel # For request validationfrom fastapi.middleware.cors import CORSMiddleware # To handle CORSimport logging # For logging error/info/debug messages# --- Logging Configuration ---# Sets up logging format and level globallylogging.basicConfig(level=logging.INFO, format='%(levelname)s:%(name)s:%(message)s')logger = logging.getLogger(__name__)# --- MySQL Database Configuration using environment variables ---DB_HOST = os.getenv("MYSQL_HOST")DB_USER = os.getenv("MYSQL_USER")DB_PASSWORD = os.getenv("MYSQL_PASSWORD")DB_NAME = os.getenv("MYSQL_DATABASE")# Function to establish and return a MySQL connectiondef get_db_connection():try:conn = mysql.connector.connect(host=DB_HOST,user=DB_USER,password=DB_PASSWORD,database=DB_NAME)return connexcept mysql.connector.Error as err:logger.error(f"Error connecting to MySQL database: {err}")raise HTTPException(status_code=500, detail="Database connection error.")# --- FastAPI App Initialization ---app = FastAPI()# --- CORS Configuration ---# This allows cross-origin requests from any domain# In production, restrict `allow_origins` to specific domainsapp.add_middleware(CORSMiddleware,allow_origins=["*"], # Allow all origins (for development/testing)allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)# --- Request Schema ---# Defines the expected JSON request bodyclass ProcessRequest(BaseModel):text: str # User's input texttool: str # Name of the tool whose prompt template will be used# --- API Endpoint to Process Text ---@app.post("/process")async def process_text(request: ProcessRequest):# Load API key from environment variableapi_key = os.getenv("RELAXAI_API_KEY")if not api_key:logger.error("RELAXAI_API_KEY environment variable not set.")raise HTTPException(status_code=500, detail="Backend configuration error: API Key not found.")# --- Step 1: Load Prompt Template from MySQL ---template = Noneconn = Nonetry:conn = get_db_connection()cursor = conn.cursor(dictionary=True) # Results returned as dictionaries# SQL query to fetch the prompt template for the selected toolquery = "SELECT template FROM prompts WHERE name = %s"cursor.execute(query, (request.tool,))result = cursor.fetchone()if not result:# If no matching tool is found in the DBlogger.error(f"Prompt for tool '{request.tool}' not found in MySQL.")raise HTTPException(status_code=404, detail=f"Prompt template '{request.tool}' not found.")# Extract template stringtemplate = result.get("template")if not template:logger.error(f"Template field is missing or empty for tool '{request.tool}'.")raise HTTPException(status_code=500, detail=f"Prompt template for '{request.tool}' is empty or invalid.")except mysql.connector.Error as err:logger.exception(f"Error retrieving prompt from MySQL: {err}")raise HTTPException(status_code=500, detail=f"Failed to retrieve prompt from database: {str(err)}")finally:# Close DB connection and cursorif conn and conn.is_connected():cursor.close()conn.close()# --- Step 2: Inject user input into template ---try:# Replace `{text}` in template with user's inputprompt = template.format(text=request.text)except KeyError as e:# Handle formatting errors (e.g., missing `text` placeholder)logger.error(f"Prompt template formatting error: Expected: 'text', Got template: '{template}'")raise HTTPException(status_code=500, detail="Internal server error: Prompt template mismatch.")# --- Step 3: Call RelaxAI API with Constructed Prompt ---headers = {"Authorization": f"Bearer {api_key}", # Bearer token authentication"Content-Type": "application/json"}payload = {"model": "Llama-4-Maverick-17B-128E", # AI model to use"messages": [{"role": "user", "content": prompt}], # Message format"max_tokens": 500 # Limit the response length}try:# Make POST request to the RelaxAI APIresponse = requests.post("https://api.relax.ai/v1/chat/completions",headers=headers,json=payload,timeout=30 # 30-second timeout)response.raise_for_status() # Raise exception for non-200 responsesdata = response.json()# Extract the AI-generated contentai_output = data.get("choices", [])[0].get("message", {}).get("content", "")if not ai_output:logger.warning(f"RelaxAI API returned no content for '{request.tool}'. Full response: {data}")raise HTTPException(status_code=500, detail="RelaxAI API returned empty response content.")return {"result": ai_output} # Send AI output back to client# --- Error Handling ---except requests.exceptions.HTTPError as http_err:logger.error(f"HTTP error from relax.ai API (Status: {response.status_code}): {http_err}. Body: {response.text}")raise HTTPException(status_code=500, detail=f"Error from relaxAI API: {response.status_code} - {response.text}")except requests.exceptions.ConnectionError as conn_err:logger.error(f"Connection error to relax.ai API: {conn_err}")raise HTTPException(status_code=500, detail=f"Network error connecting to relaxAI API: {str(conn_err)}")except requests.exceptions.Timeout as timeout_err:logger.error(f"Timeout error from relax.ai API: {timeout_err}")raise HTTPException(status_code=504, detail=f"RelaxAI API did not respond in time: {str(timeout_err)}")except IndexError:logger.error(f"Unexpected response structure from relax.ai API for '{request.tool}'. Full response: {data}")raise HTTPException(status_code=500, detail="Unexpected response structure from relaxAI API.")except Exception as e:logger.exception("Unexpected error during relaxAI API call processing:")raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
Save this as app.py. Next, create a requirements.txt listing the Python libs:
fastapiuvicornmysql-connector-pythonrequestspydantic
These include FastAPI, the Civo Database admin SDK, and requests for HTTP. A Dockerfile is required to containerize the backend service. Here’s a simple example Dockerfile:
FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt# Copy the Python codeCOPY . .EXPOSE 8000CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
This Dockerfile uses Python 3.11 slim image, installs requirements, copies the code, and runs Uvicorn to serve FastAPI on port 8000.
Step 4: Build the frontend
Next, let’s create the React frontend. Vite will be used to scaffold a new React app:
npm create vite@latest relaxai-app --template reactcd relaxai-appnpm install axios
Inside the new app, edit src/App.jsx (or App.js) to include a form and call the backend. For example:
1. State & setup
Initial setup: Imports and React component with state:
// ---------- 1. State & Setup ----------import React, { useState } from 'react';import axios from 'axios';function App() {const [inputText, setInputText] = useState('');const [tool, setTool] = useState('summarizer');const [result, setResult] = useState('');const [loading, setLoading] = useState(false);const [error, setError] = useState('');
Explanation: Imports dependencies and sets up five pieces of state: for text input, tool choice, result output, loading status, and error message.
2. Form & user input
UI to accept text and select a tool.
return (<div style={{padding: '20px',maxWidth: '800px',margin: 'auto',fontFamily: 'Inter, Arial, sans-serif',backgroundColor: '#1a1a1a',color: '#ffffff',minHeight: '100vh',borderRadius: '8px',boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)'}}><h1 style={{ color: '#61dafb', textAlign: 'center', marginBottom: '30px' }}>AI Writing Assistant</h1>{/* ---------- 2. Form & User Input ---------- */}<form onSubmit={handleSubmit} style={{display: 'flex',flexDirection: 'column',gap: '20px',backgroundColor: '#282c34',padding: '25px',borderRadius: '8px',boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)'}}><div><label htmlFor="inputText" style={{ display: 'block', marginBottom: '8px' }}>Enter your text:</label><textareaid="inputText"rows="8"value={inputText}placeholder="Enter some text here..."onChange={e => setInputText(e.target.value)}style={{width: '100%',padding: '10px',border: '1px solid #444',borderRadius: '6px',backgroundColor: '#3a3a3a',color: '#ffffff',resize: 'vertical'}}></textarea></div><div><label htmlFor="toolSelect" style={{ display: 'block', marginBottom: '8px' }}>Select Tool:</label><selectid="toolSelect"value={tool}onChange={e => setTool(e.target.value)}style={{padding: '10px',border: '1px solid #444',borderRadius: '6px',backgroundColor: '#3a3a3a',color: '#ffffff',cursor: 'pointer'}}><option value="summarizer">Summarizer</option><option value="rewriter">Rewriter (Polite Tone)</option></select></div><buttontype="submit"disabled={loading || !inputText.trim()}style={{padding: '12px 20px',backgroundColor: '#61dafb',color: '#1a1a1a',border: 'none',borderRadius: '6px',cursor: loading || !inputText.trim() ? 'not-allowed' : 'pointer',opacity: loading || !inputText.trim() ? 0.8 : 1,fontWeight: 'bold',transition: 'background-color 0.2s, opacity 0.2s',boxShadow: '0 2px 4px rgba(0, 0, 0, 0.3)'}}>{loading ? 'Processing...' : 'Process Text'}</button></form></div>);
Renders a styled form with a large text area for user input, a dropdown to choose between two tools, and a button that triggers processing.
3. API call logic
Handles backend requests and error/results.
// ---------- 3. API Call Logic ----------const handleSubmit = async (e) => {e.preventDefault();setLoading(true);setResult('');setError('');try {const backendUrl = import.meta.env.VITE_APP_BACKEND_URL || '';const res = await axios.post(`${backendUrl}/process`, {text: inputText,tool});setResult(res.data.result);} catch (err) {console.error('Error calling backend:', err);if (err.response?.data?.detail) {setError(`Error: ${err.response.data.detail}`);} else if (err.message) {setError(`Network Error: ${err.message}`);} else {setError('Error processing request. Please try again.');}} finally {setLoading(false);}};
On form submission, it sends the input and tool to the backend using Axios, then updates the UI with the result or error.
4. Output display
{/* ---------- 4. Output Display ---------- */}{error && (<p style={{ color: '#ff6b6b', marginTop: '20px', textAlign: 'center' }}>{error}</p>)}<div style={{ marginTop: '40px', borderTop: '1px solid #444', paddingTop: '30px' }}><h2 style={{ color: '#61dafb', marginBottom: '15px' }}>Result:</h2>{loading && <p style={{ color: '#aaa' }}>Waiting for AI response...</p>}{result ? (<pre style={{whiteSpace: 'pre-wrap',wordBreak: 'break-word',backgroundColor: '#000000',color: '#ffffff',padding: '20px',borderRadius: '8px',border: '1px solid #555',boxShadow: 'inset 0 1px 3px rgba(0, 0, 0, 0.4)'}}>{result}</pre>) : (!loading && !error && <p style={{ color: '#aaa' }}>No result yet. Enter text and select a tool.</p>)}</div></div>);}export default App;
Displays errors in red, a loading message when waiting, and the AI-generated result in a styled box once available.
This React component has:
- A textarea bound to
inputText. - A dropdown to choose the prompt type (
summarizerorrewriter). - On form submit, it POSTs to
/processwith{input, tool}(just like the backend expects). - It then displays the AI’s response.
5. Create the .env file
Before building the frontend image, you need to create a .env file inside the root of the frontend folder. Inside that file, add the following line:
VITE_APP_BACKEND_URL=http://<your-backend-loadbalancer-url>
Important note: This value gets hardcoded into the app during the Docker build, so make sure the backend is already deployed and the LoadBalancer URL is live before building the frontend image.
As the backend and frontend will be separate services in Kubernetes, you might need to adjust the API URL (for now, let’s assume they can talk, e.g,. via a shared domain or proxy). In development, you could set a proxy in vite.config.js or use localhost.
Now build and Dockerize the frontend. First, create a Dockerfile:
FROM node:18-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run build# Use a lightweight web server to serve the built filesFROM node:18-alpineWORKDIR /appRUN npm install -g serveCOPY --from=build /app/dist ./distCMD ["serve", "-s", "dist", "-l", "80"]
This Dockerfile does a multi-stage build: it builds the Vite app, then serves the static files with the serve package on port 80 (you could also use Nginx).
Once the image is built and deployed, your frontend should appear like this:

Source: Image by author
Step 5: Push Docker images
With both backend and frontend Dockerfiles ready, build and push them to Docker Hub (or any registry):
# Replace 'yourhub' with your Docker Hub username or registrydocker build -t yourhub/relaxai-backend:latest .docker push yourhub/relaxai-backend:latestdocker build -t yourhub/relaxai-frontend:latest .docker push yourhub/relaxai-frontend:latest
Make sure you docker login first, and that yourhub/relaxai-* is a repo you control. Once pushed, these images will be accessible to Kubernetes.
Step 6: Deploy to Civo Kubernetes
Now deploy the services to a Civo K3s cluster. First, create a Civo cluster (via dashboard or civo kubernetes create mycluster). Once it’s up, download the kubeconfig and kubectl-connect to it.
Secrets: Two secrets need to be supplied: the Civo Database key and the relaxAI API key. Create a Kubernetes secret with them.
Civo MySQL Database credentials secret (mysql-creds)
This secret holds the host, username, password, and database name for your Civo MySQL instance.
kubectl create secret generic mysql-creds \--from-literal=host=YOUR_MYSQL_HOST \--from-literal=username=YOUR_MYSQL_USERNAME \--from-literal=password=YOUR_MYSQL_PASSWORD \--from-literal=database=ai_tools
relaxAI API key secret (relaxai-secrets): This secret holds your relax.ai API key.
kubectl create secret generic relaxai-secrets \--from-literal=relaxai-api-key=YOUR_RELAXAI_API_KEY
Backend deployment: Make a file backend-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata:name: relaxai-backendspec:replicas: 1selector:matchLabels: { app: relaxai-backend }template:metadata:labels: { app: relaxai-backend }spec:containers:- name: relaxai-backendimage: yourhub/relaxai-backend:latestports:- containerPort: 8000env:- name: RELAXAI_API_KEYvalueFrom:secretKeyRef:name: relaxai-secretskey: relaxai-api-key# --- New MySQL Environment Variables ---- name: MYSQL_HOSTvalueFrom:secretKeyRef:name: mysql-credskey: host- name: MYSQL_USERvalueFrom:secretKeyRef:name: mysql-credskey: username- name: MYSQL_PASSWORDvalueFrom:secretKeyRef:name: mysql-credskey: password- name: MYSQL_DATABASEvalueFrom:secretKeyRef:name: mysql-credskey: database
And a service for the backend (it needs to be externally accessible, so use LoadBalancer):
apiVersion: v1kind: Servicemetadata:name: relaxai-backendspec:selector:app: relaxai-backendports:- port: 80targetPort: 8000type: LoadBalancer
The backend now receives its credentials via environment variables from two Kubernetes secrets: mysql-creds and relaxai-secrets. RELAXAIAPIKEY is passed directly, along with the MYSQLHOST, MYSQLUSER, MYSQLPASSWORD, and MYSQLDATABASE.
Frontend deployment: Create frontend-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata:name: relaxai-frontendspec:replicas: 1selector:matchLabels: { app: relaxai-frontend }template:metadata:labels: { app: relaxai-frontend }spec:containers:- name: relaxai-frontendimage: yourhub/relaxai-frontend:latestports:- containerPort: 80
And its service (it needs to be externally accessible, so use LoadBalancer):
apiVersion: v1kind: Servicemetadata:name: relaxai-frontendspec:type: LoadBalancerselector:app: relaxai-frontendports:- port: 80targetPort: 80
Apply these with kubectl apply -f on your cluster. The LoadBalancer service will get an external IP (Civo supports LoadBalancer in all regions).
Now your AI app is live! Visiting the frontend’s IP in a browser should show your form, talking to your Python backend, which in turn talks to relaxAI and Civo Database.
Step 7: Use case – AI-powered writing assistant for internal teams
The key advantage here is the combination of prompt templates in Civo Database and a simple API client. The templates are “live” configuration, not hardcoded, so your small AI utility is extremely flexible.
Step 8: Quick debugging guide
Key takeaways
This tutorial provides a deployable blueprint for creating small, prompt-powered AI tools using relaxAI, Civo Database, and Civo Kubernetes. It’s simple, cost-effective, and ready for use cases like summarization, rewriting, or code explanation. Here’s how it all comes together:
To explore more about deploying AI tools and managing databases on Civo, the following resources are recommended:

Software Engineer @ GoCardless
Mostafa Ibrahim is a software engineer and technical writer specializing in developer-focused content for SaaS and AI platforms. He currently works as a Software Engineer at GoCardless, contributing to production systems and scalable payment infrastructure.
Alongside his engineering work, Mostafa has written more than 200 technical articles reaching over 500,000 readers. His content covers topics including Kubernetes deployments, AI infrastructure, authentication systems, and retrieval-augmented generation (RAG) architectures.
Share this article