# Deploy an API service

Source: https://docs.quake.ai/docs/quickstart/deploy-api-service
Markdown: https://docs.quake.ai/docs/quickstart/deploy-api-service.md

---

# Deploy an API service

Run a lightweight API server on a Quake AI VM: either Node.js (Express) or Python (Flask). The API runs as a systemd service so it starts automatically on boot and restarts on failure.

**What you will learn:**

- How to open a custom application port in a security group
- How to install a Node.js or Python runtime on Ubuntu
- How to register a long-running process as a systemd service
- How to update and restart the service from your local machine
- How to put Nginx in front of the API as a reverse proxy



This tutorial launches a single `s1a.micro` instance (1 vCPU, 1 GiB RAM) on the `PublicEphemeral` network, which assigns a public IP at boot with no floating IP add-on. A single Node.js or Flask process uses ~300–400 MB RAM including the OS, well within the Developer plan's 1 GiB. Keep dependencies minimal to stay in the tier. See [Resource tiers](/docs/account/resource-tiers) for upgrade paths if you outgrow it.



<Figure size="md" caption="What you'll build: an API process behind Nginx on a single Ubuntu instance, reachable through its PublicEphemeral public IP and a security group">

```d2
direction: right

client: API client {shape: person}
pip: Public IP\n(PublicEphemeral)
sg: Security group\nSSH + API port
instance: Ubuntu instance {
  nginx: Nginx\n(reverse proxy)
  api: API process\nNode.js or Flask\n(systemd)
  nginx -> api: localhost
}

client -> pip: HTTPS
pip -> sg
sg -> instance.nginx
```

</Figure>

## Prerequisites

- A Quake AI account with an active project, see the [Quickstart](/docs/quickstart) if you have not signed up
- An [SSH key pair](/docs/tools/add-ssh-key) uploaded to your account
- A working VM you can SSH into, or follow [Launch your first server](/docs/quickstart/launch-your-first-server) to launch one

## Step 1. Create a security group

Create a security group allowing SSH and your API port:

<MethodTabs>
<Method label="Console">

1. Go to **Network** > **Security Groups** > **Create Security Group**.
2. Name it `dev-api` and create it.
3. Add rules for:
   - **SSH**: TCP port 22
   - **Custom TCP**: port 3000 (or whichever port your API uses)

</Method>
<Method label="CLI">

```bash
openstack security group create dev-api
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip 0.0.0.0/0 dev-api
openstack security group rule create \
  --protocol tcp --dst-port 3000 --remote-ip 0.0.0.0/0 dev-api
```

</Method>
<Method label="API">

```bash
curl -X POST "$OS_NETWORK_URL/v2.0/security-groups" \
  -H "X-Auth-Token: $OS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"security_group": {"name": "dev-api", "description": "Allow SSH and API traffic"}}'
```

Add rules for SSH (port 22) and your API port (port 3000), substituting `SECURITY_GROUP_ID` from the response:

```bash
for PORT in 22 3000; do
  curl -X POST "$OS_NETWORK_URL/v2.0/security-group-rules" \
    -H "X-Auth-Token: $OS_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"security_group_rule\": {\"security_group_id\": \"SECURITY_GROUP_ID\", \"direction\": \"ingress\", \"ethertype\": \"IPv4\", \"protocol\": \"tcp\", \"port_range_min\": $PORT, \"port_range_max\": $PORT, \"remote_ip_prefix\": \"0.0.0.0/0\"}}"
done
```

</Method>
</MethodTabs>

## Step 2. Launch the instance

Follow [Launch your first server](/docs/quickstart/launch-your-first-server) to launch an Ubuntu 24.04 instance on the `PublicEphemeral` network with the `s1a.micro` flavor, but attach the `dev-api` security group from Step 1 instead of `web-access`. The `PublicEphemeral` network gives the instance a public IP at boot, so no floating IP is needed.

## Step 3. Install a runtime and deploy your API

SSH into the instance and choose one of the two paths below.




Install Node.js and create a minimal API:

```bash
# Install Node.js 22 LTS
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

# Create the app
mkdir -p ~/api && cd ~/api
npm init -y
npm install express
```

Create `~/api/server.js`:

```bash
cat > ~/api/server.js << 'EOF'
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({ status: "ok", message: "Hello from Quake AI" });
});

app.get("/health", (req, res) => {
  res.json({ status: "healthy", uptime: process.uptime() });
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`API listening on port ${PORT}`);
});
EOF
```

Create a systemd service:

```bash
sudo tee /etc/systemd/system/api.service > /dev/null << 'EOF'
[Unit]
Description=Node.js API
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/api
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=PORT=3000
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable api
sudo systemctl start api
```




Install Python and create a minimal API:

```bash
# Python 3 is pre-installed on Ubuntu 24.04
sudo apt update && sudo apt install -y python3-pip python3-venv

# Create the app
mkdir -p ~/api && cd ~/api
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn
```

Create `~/api/app.py`:

```bash
cat > ~/api/app.py << 'EOF'
from flask import Flask, jsonify
import time

app = Flask(__name__)
start_time = time.time()

@app.route("/")
def index():
    return jsonify(status="ok", message="Hello from Quake AI")

@app.route("/health")
def health():
    return jsonify(status="healthy", uptime=time.time() - start_time)
EOF
```

Create a systemd service using Gunicorn:

```bash
sudo tee /etc/systemd/system/api.service > /dev/null << 'EOF'
[Unit]
Description=Python API (Gunicorn)
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/api
ExecStart=/home/ubuntu/api/venv/bin/gunicorn --bind 0.0.0.0:3000 --workers 2 app:app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable api
sudo systemctl start api
```



With 1 GB RAM, use `--workers 2` for Gunicorn. More workers risk running out of memory.






## Step 4. Verify

Test the API from your local machine:

```bash
curl http://YOUR_INSTANCE_IP:3000/
# {"status":"ok","message":"Hello from Quake AI"}

curl http://YOUR_INSTANCE_IP:3000/health
# {"status":"healthy","uptime":42.7}
```

Check the service status on the instance:

```bash
sudo systemctl status api
journalctl -u api -f
```

## Step 5. Deploy updates

Push new code to the instance and restart the service:

```bash
# From your local machine
scp -r ./api/* ubuntu@YOUR_INSTANCE_IP:~/api/

# On the instance
ssh ubuntu@YOUR_INSTANCE_IP
sudo systemctl restart api
```

## Adding a reverse proxy (optional)

For production-style deployments, put Nginx in front of your API to serve it on port 80. This example uses plain HTTP; add a TLS certificate and a port 443 listener as a follow-up.

The `dev-api` security group from Step 1 allows only SSH and port 3000, so external requests to plain `http://YOUR_INSTANCE_IP/` time out until you open port 80. Add the rule from your local machine, where the OpenStack CLI is configured:

```bash
# Open port 80 to allow external HTTP through Nginx
openstack security group rule create \
  --protocol tcp --dst-port 80 --remote-ip 0.0.0.0/0 dev-api
```

Then install and configure Nginx on the instance:

```bash
sudo apt install -y nginx
sudo tee /etc/nginx/sites-available/api > /dev/null << 'EOF'
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
EOF

sudo ln -sf /etc/nginx/sites-available/api /etc/nginx/sites-enabled/api
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
```

## Next steps

- [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai): put the API behind your own hostname
- [How to issue and auto-renew a TLS certificate with Let's Encrypt](/docs/network/how-to/lets-encrypt-certificate): terminate HTTPS on the VM or a self-managed reverse proxy
- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm): automate deploys after you outgrow manual `systemctl` restarts
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): move API keys out of unit files and into env files
- [Deploy a database sandbox](/docs/quickstart/deploy-db-sandbox): add a database backend to your API
- [Deploy a containerized web application](/docs/quickstart/deploy-containerized-app): containerize this setup
- [Automate your infrastructure with OpenTofu](/docs/quickstart/automate-infrastructure-opentofu): automate provisioning

## See also

- [How to create a VM on a public network](/docs/compute/how-to/create-vm-public-network)
- [Security groups concepts](/docs/network/concepts/security-groups)
- [Floating IPs](/docs/network/concepts/floating-ips)
- [Resource tiers](/docs/account/resource-tiers)
