# Host a static website on Quake AI

Source: https://docs.quake.ai/docs/quickstart/host-static-website
Markdown: https://docs.quake.ai/docs/quickstart/host-static-website.md

---

# Host a static website on Quake AI

In this tutorial, we deploy a static website served by Nginx on a Quake AI instance. By the end, you will have a publicly accessible web page served from your own cloud server, with a public IP you can share or point a domain name to.

**What you will learn:**

- How to configure an instance for web hosting
- How to install and configure Nginx as a web server
- How to deploy static HTML content
- How security groups control which ports are accessible from the internet
- How to verify your website is publicly accessible



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. Nginx serving static files uses ~250 MB RAM total (OS + Nginx), well within the Developer plan's 1 GiB, and a typical static site fits comfortably in the 20 GiB block storage allocation. See [Resource tiers](/docs/account/resource-tiers) for plan details and upgrade paths.



<Figure size="md" caption="What you'll build: visitor traffic reaches Nginx on an Ubuntu instance via its PublicEphemeral public IP, gated by a security group">

```d2
direction: right

user: Visitor {shape: person}
pip: Public IP\n(PublicEphemeral)
sg: Security group\nSSH, HTTP, HTTPS
instance: Ubuntu instance\nNginx + HTML

user -> pip: HTTP/HTTPS
pip -> sg
sg -> instance
```

</Figure>

## Prerequisites

You need:

- A Quake AI account with an active project
- An SSH key pair uploaded to Quake AI (the OpenStack name is your **key pair** name)
- Completed the [Quickstart](/docs/quickstart)

The `PublicEphemeral` network assigns a public IP to the instance at boot, so this tutorial needs no private network, router, or floating IP. We use the OpenStack CLI so you can reproduce every step from your terminal.

<SourceOpenrc />

Substitute these placeholders in the commands: `YOUR_KEYPAIR_NAME` for your uploaded key pair, and `UBUNTU_22_IMAGE_NAME` from `openstack image list` for Ubuntu 22.04 (names vary by region, for example `Ubuntu 22.04` or `Ubuntu-22.04`).

## Step 1: Create a security group for SSH, HTTP, and HTTPS

Security groups filter traffic to instance ports. Without ingress rules for 22, 80, and 443, clients cannot reach SSH or Nginx from the internet. We use a dedicated group so the rules stay organized and auditable.

```bash
openstack security group create tutorial-static-web \
  --description "SSH, HTTP, and HTTPS for static site tutorial"
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip 0.0.0.0/0 \
  tutorial-static-web
openstack security group rule create \
  --protocol tcp --dst-port 80 --remote-ip 0.0.0.0/0 \
  tutorial-static-web
openstack security group rule create \
  --protocol tcp --dst-port 443 --remote-ip 0.0.0.0/0 \
  tutorial-static-web
```

We use `0.0.0.0/0` so any client can reach the tutorial instance. In production, narrow SSH sources and put a self-managed reverse proxy in front of HTTP services.

## Step 2: Launch an Ubuntu 22.04 instance

We attach the instance to `PublicEphemeral`, which assigns a public IP at boot, and combine `default` and `tutorial-static-web` so baseline project behavior stays in place while our rules open the web ports.

```bash
openstack server create \
  --flavor s1a.micro \
  --image UBUNTU_22_IMAGE_NAME \
  --network PublicEphemeral \
  --key-name YOUR_KEYPAIR_NAME \
  --security-group default \
  --security-group tutorial-static-web \
  --wait \
  tutorial-web-vm
```

The command returns when the server reaches `ACTIVE`.

## Step 3: Find the instance public IP

`PublicEphemeral` assigns the public address at boot, so there is no floating IP to allocate. Read the address from the instance and save it for the SSH and browser checks:

```bash
INSTANCE_IP=$(openstack server show tutorial-web-vm -f value -c addresses | sed 's/.*=//')
echo "$INSTANCE_IP"
```

## Step 4: SSH into the instance

Ubuntu cloud images use the `ubuntu` user. Connect with the private key that matches `YOUR_KEYPAIR_NAME`:

```bash
ssh -i /path/to/YOUR_PRIVATE_KEY ubuntu@$INSTANCE_IP
```

Accept the host key the first time you connect.

## Step 5: Install Nginx

Nginx is a small, efficient choice for static files.

```bash
sudo apt update
sudo apt install -y nginx
```

If `apt` cannot resolve the archives, your subnet is missing DNS resolvers. Run the DNS check in [Prerequisites](#confirm-your-subnet-has-dns-resolvers) before you retry.

## Step 6: Add a simple HTML page

Ubuntu's default document root is `/var/www/html/`. Replace the stock page:

```bash
sudo tee /var/www/html/index.html > /dev/null <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Hosted on Quake AI</title></head>
<body><h1>Hello from Quake AI</h1><p>Static HTML served by Nginx.</p></body>
</html>
EOF
```

## Step 7: Verify Nginx locally on the instance

Confirm the service and HTTP response on the instance before testing from the internet.

```bash
systemctl is-active nginx
curl -s http://127.0.0.1/ | head -n 5
```

Expect `active` and your heading in the `curl` output.

## Step 8: Open the site in your browser

Open `http://$INSTANCE_IP` in a browser. If it fails, recheck security group rules, confirm the public address with `openstack server show tutorial-web-vm -c addresses`, and rule out local port blocking.

## Step 9: (optional) self-signed HTTPS for learning

Self-signed certificates trigger browser warnings; they only demonstrate TLS termination in Nginx before you use a public CA.

```bash
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/nginx-selfsigned.key \
  -out /etc/ssl/certs/nginx-selfsigned.crt \
  -subj "/CN=TUTORIAL_HOSTNAME"
sudo tee /etc/nginx/sites-available/tutorial-tls.conf > /dev/null <<'EOF'
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name _;
    ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
    ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
    root /var/www/html;
    index index.html;
}
EOF
sudo ln -sf /etc/nginx/sites-available/tutorial-tls.conf /etc/nginx/sites-enabled/tutorial-tls.conf
sudo nginx -t && sudo systemctl reload nginx
```

Visit `https://$INSTANCE_IP` and accept the expected certificate warning.

## Step 10: (optional) production HTTPS with Let's Encrypt

For HTTPS with a custom domain and a trusted certificate, install Certbot after pointing your domain's DNS A record to the instance public IP:

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
```

Certbot adds HTTPS configuration to Nginx and sets up automatic certificate renewal. You will need to add an HTTPS rule (TCP port 443) to your `tutorial-static-web` security group before HTTPS will work, use the same `openstack security group rule create` pattern as Step 1.

## Cloud-init alternative (zero-touch setup)

If you want to skip Steps 4–6 and have Nginx installed automatically at first boot, save this as `cloud-init-nginx.yaml` on your local machine:

```yaml
#cloud-config
package_update: true
packages:
  - nginx

write_files:
  - path: /var/www/html/index.html
    content: |
      <!doctype html>
      <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>My Quake AI Site</title>
      </head>
      <body>
        <h1>Hello from Quake AI</h1>
        <p>This static site is served by Nginx, configured by cloud-init.</p>
      </body>
      </html>

runcmd:
  - systemctl enable nginx
  - systemctl start nginx
```

Pass it on `openstack server create` with `--user-data cloud-init-nginx.yaml`. Cloud-init takes 1–2 minutes after boot to finish; once the instance is `ACTIVE` and cloud-init completes, the site is live.

## Deploy your own content

Replace the placeholder page with your site files from your local machine:

```bash
scp -r ./my-site/* ubuntu@$INSTANCE_IP:/var/www/html/
```

Or use `rsync` for incremental updates:

```bash
rsync -avz --delete ./my-site/ ubuntu@$INSTANCE_IP:/var/www/html/
```

## What you learned

- **Security groups** for SSH, HTTP, and HTTPS, and why each port is opened
- **Instance launch** with Ubuntu 22.04, flavor `s1a.micro`, the `PublicEphemeral` public network, and the web security group
- **Public addressing** with the public IP `PublicEphemeral` assigns at boot
- **Nginx** install, static content under `/var/www/html/`, local and remote checks
- **Optional TLS** with a self-signed certificate

## Next steps

- [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai): map a hostname at your registrar to the instance public IP
- [How to issue and auto-renew a TLS certificate with Let's Encrypt](/docs/network/how-to/lets-encrypt-certificate): production HTTPS with Certbot on the instance
- [How to put a CDN in front of a Quake AI workload](/docs/network/how-to/front-with-cdn): edge caching when traffic grows beyond a single VM
- [How to host a static site on object storage](/docs/object/how-to/host-static-site): serve files from a bucket instead of Nginx on a VM
- [Edge reverse proxy template](/resources/iac-templates/edge-reverse-proxy): route traffic to several application instances
- [Resource tiers](/docs/account/resource-tiers): plan your upgrade path if traffic grows beyond the Developer Plan's 0.5 Gbps cap

## Clean up

Delete the server, then remove the security group. The `PublicEphemeral` public IP is released with the instance, so there is no floating IP to delete:

```bash
openstack server delete tutorial-web-vm
openstack security group delete tutorial-static-web
```

Confirm `openstack server list` no longer shows `tutorial-web-vm`.
