Skip to content

Deploy a containerized web application

Tutorial · Updated Jun 2026
Before this

Deploy a containerized web application

In this tutorial, we deploy a containerized web application to Quake AI. By the end, you will have a running Docker container serving a web page on a cloud instance, accessible from the internet through the instance's PublicEphemeral public IP.

What you will learn:

  • How to install Docker on a Quake AI instance
  • How to write a simple Dockerfile for a static website
  • How to build and run a Docker container on a remote server
  • How to open HTTP ports in a security group for web traffic
  • How to verify your application is accessible from the internet

Time estimate: 30 minutes

VisitorPublic IP(PublicEphemeral)Security groupSSH + HTTPUbuntu instanceDocker engineWeb container(Dockerfile build) runsHTTPport mapping
Click to zoom
What you'll build: a Docker container running on an Ubuntu instance, exposed to the internet through a security group and its PublicEphemeral public IP

Entry-tier footprint

Pricing calculator ↗

Sized as a custom package on shared vCPU.

Starting template$3.25/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

s1a.micro

s1a.micro · 1 shared vCPU, 1 GiB RAM, 0.5 Gbps

$8.25/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

s1a.micro

1 shared vCPU, 1 GiB RAM, 0.5 Gbps

$8.25

Compute + RAM rate basis

1 vCPU + 1 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Pricing data last validated: . For current rates, check quake.ai/pricing.

Prerequisites#

You need:

  • Completed the Launch your first server tutorial
  • SSH access to your instance (or create a new one following that tutorial)
  • Basic familiarity with the command line

This tutorial assumes Ubuntu 24.04 on the instance reachable over its PublicEphemeral public IP, which the network assigns at boot with no floating IP add-on. We refer to that address as YOUR_INSTANCE_IP throughout.

Step 1: Allow SSH and HTTP, then launch the instance#

We need inbound TCP 22 for administration and 80 for HTTP. Security groups enforce those rules at the network edge before traffic reaches the instance.

If you are creating a new instance:

  1. Go to Network > Security Groups > Create Security Group. Name it tutorial-web and add a short description.
  2. Add an ingress rule for SSH (port 22) from a CIDR you trust. For learning, 0.0.0.0/0 is common; tighten this in production.
  3. Add an ingress rule for HTTP (port 80) from 0.0.0.0/0 so browsers and health checks can reach nginx.
  4. Launch an instance at Compute > Instances > Create Instance:
    • Image: Ubuntu-24.04
    • Flavor: s1a.micro (1 vCPU, 1 GiB RAM), the entry-tier footprint
    • Network: attach PublicEphemeral, which assigns a public IP at boot
    • Security groups: include default and tutorial-web (or equivalent) so both platform defaults and your SSH/HTTP rules apply
    • Key pair: the same key you use for SSH
  5. Copy the public IP the instance shows once it is Active. Note it as YOUR_INSTANCE_IP.

If you reuse the instance from Deploy your first server:

  1. Edit your security group (or create tutorial-web as above) and add an ingress rule for HTTP on port 80.
  2. Attach the updated group to the instance if it is not already applied.
  3. Use the instance's existing PublicEphemeral public IP as YOUR_INSTANCE_IP.

Wait until the instance shows Active before continuing.

Step 2: SSH into the instance#

We connect over SSH because Docker commands run on the server, not on your laptop. Replace YOUR_INSTANCE_IP with the address you recorded.

bash
ssh ubuntu@YOUR_INSTANCE_IP

Accept the host key prompt on first connect. If your image uses a different default user, substitute that username.

Step 3: install Docker#

Docker packages your app and its runtime together so nginx runs the same way on every host. We use the official Docker apt repository so updates track Docker's supported packages on Ubuntu 24.04.

If you are already logged in from Step 2, run the install and group commands below. The first block repeats the SSH line for anyone joining mid-tutorial.

SSH into your VM:

ssh ubuntu@YOUR_FLOATING_IP

Install Docker using the official repository:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Add your user to the Docker group so you can run commands without sudo:

sudo usermod -aG docker ubuntu
newgrp docker

Group membership changes take effect on your next login. The newgrp docker line above activates the group in your current shell. If you skip it or open a new shell, log out and SSH back in before you run docker commands.

Confirm the install with a command that needs the daemon socket:

docker run --rm hello-world

A daemon-less check such as docker --version passes even when the group is not active yet, so it hides this gotcha.

Confirm Docker can reach its daemon:

bash
docker run --rm hello-world

Docker pulls a small test image and prints a message that starts with Hello from Docker!, confirming the daemon is reachable from your user account.

Step 3a: Configure swap (Developer Plan and other low-RAM tiers)#

If you are on the Developer Plan or any flavor with 2 GiB or less of RAM, add 1 GiB of swap before running containers. This prevents out-of-memory kills when a container spikes:

bash
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

The output of free -h should show 1 GiB of swap. Skip this step if you launched a flavor with 4 GiB or more.

Step 4: Create the project files#

We keep a small static site so we focus on containers, not application frameworks. nginx serves index.html on port 80 inside the container.

bash
mkdir -p ~/container-demo && cd ~/container-demo

Create index.html:

HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Quake AI container demo</title>
  </head>
  <body>
    <h1>Hello from Docker on Quake AI</h1>
    <p>If you see this page, your container and security group are configured correctly.</p>
  </body>
</html>

Create Dockerfile:

Dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html

nginx:alpine is a small image with a production-grade web server, which keeps pull and build times short.

Step 5: Build the image#

The build reads the Dockerfile and produces a local image tag we can run.

bash
docker build -t YOUR_IMAGE_NAME .

Use a meaningful tag (for example rumble-static-demo:latest) in place of YOUR_IMAGE_NAME.

Step 6: Run the container#

We publish container port 80 to the host's port 80 so traffic that reaches the VM on port 80 (allowed by the security group) reaches nginx.

bash
docker run -d --name YOUR_CONTAINER_NAME -p 80:80 YOUR_IMAGE_NAME

Replace YOUR_CONTAINER_NAME with a unique name such as static-web.

Step 7: Test locally on the instance#

curl checks the stack from the server itself before we involve the public network path.

bash
curl -sS http://127.0.0.1/ | head -n 5

You should see the HTML title or heading from index.html. If the command hangs or fails, run docker ps and docker logs YOUR_CONTAINER_NAME to confirm the container is running.

Step 8: Reach the app from the internet#

From your local terminal (not inside SSH), request the instance public IP:

bash
curl -sS http://YOUR_INSTANCE_IP/ | head -n 5

If this fails but Step 7 passed, verify the security group allows ingress on port 80 (and that no host firewall blocks port 80).

Step 9: Verify in a browser#

Open http://YOUR_INSTANCE_IP in a desktop or mobile browser. You should see the demo heading and paragraph. Browsers follow redirects and cache behavior differently than curl, so this step confirms real user traffic works.

Multi-service stacks with Docker Compose (optional)#

Once your single-container app works, you can compose multiple services. Create ~/compose-demo/compose.yaml with explicit memory limits:

bash
mkdir -p ~/compose-demo && cd ~/compose-demo
cat > compose.yaml <<'EOF'
services:
  app:
    image: node:22-alpine
    working_dir: /app
    volumes:
      - ./app:/app
    ports:
      - "3000:3000"
    command: ["node", "server.js"]
    deploy:
      resources:
        limits:
          memory: 256M

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    deploy:
      resources:
        limits:
          memory: 64M
EOF

Add a minimal Node.js entry point:

bash
mkdir -p app
cat > app/server.js <<'EOF'
const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ status: "ok", container: true }));
});
server.listen(3000, () => console.log("Listening on :3000"));
EOF

Bring the stack up:

bash
docker compose up -d
docker compose ps
curl http://localhost:3000

On the Developer Plan, docker stats shows real-time memory consumption per container, useful when tuning limits.

Resource management tips#

On any small tier, memory is the primary constraint. Apply these practices:

  • Always set --memory limits on containers to prevent runaway usage.
  • Use Alpine-based images when available. They are 5–10× smaller than full images.
  • Run one service at a time during development instead of a full stack.
  • Monitor usage with docker stats to watch real-time memory per container.
  • Clean up on a schedule: stopped containers and unused images consume disk:
bash
docker system prune -f
docker image prune -a -f

Cloud-init for zero-touch Docker setup (optional)#

To launch a VM with Docker and swap pre-configured, pass this cloud-init script as --user-data on openstack server create:

YAML
#cloud-config
package_update: true

write_files:
  - path: /etc/sysctl.d/99-swap.conf
    content: |
      vm.swappiness=60

runcmd:
  - fallocate -l 1G /swapfile
  - chmod 600 /swapfile
  - mkswap /swapfile
  - swapon /swapfile
  - echo '/swapfile none swap sw 0 0' >> /etc/fstab
  - apt-get update
  - apt-get install -y ca-certificates curl
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
  - chmod a+r /etc/apt/keyrings/docker.asc
  - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
  - apt-get update
  - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  - usermod -aG docker ubuntu

Cloud-init runs at first boot, so the instance reaches ACTIVE with Docker already installed.

What you learned#

In this tutorial, we:

  • Configured security groups so SSH and HTTP reach the instance through explicit rules
  • Installed Docker using the official repository on Ubuntu 24.04
  • Built an image from a Dockerfile that layers static content on nginx:alpine
  • Ran a container with port publishing so host port 80 maps to the web server
  • Validated the deployment with curl and a browser through YOUR_INSTANCE_IP

Next steps#

Clean up#

When you no longer need the demo:

  1. On the instance (SSH): stop and remove the container:
bash
docker stop YOUR_CONTAINER_NAME
docker rm YOUR_CONTAINER_NAME
  1. In the console: delete the instance at Compute > Instances to stop compute charges. The PublicEphemeral public IP is released with the instance, so there is no floating IP to release.
  2. Optional: remove the tutorial-web security group if you created it for this walkthrough.

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 04.06.2026

Quick answers

Was this page helpful?