Deploy a Next.js app with the nextjs-app template
Deploy a Next.js app with the nextjs-app template
Stand up a Next.js app on a single Quake AI instance using the validated OpenTofu template nextjs-app. You build a standalone container image, push it to a registry, apply the template, serve the app over a domain with HTTPS, and connect a Postgres database.
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
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
App tier
s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps
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.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (20 GiB)
20 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
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 morePrivate 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.
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.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Prerequisites#
You need:
- Familiarity with Docker images and the command line. If containers are new to you, work through Deploy a containerized web application first.
- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (
source openrc.sh). See the OpenStack CLI guide. - An SSH keypair that already exists in your project. Record its name for the
key_namevariable. - A container registry the instance can pull from on first boot (a public registry, or one your project can reach). Record the credentials if it is private.
- A copy of the
nextjs-apptemplate directory from the template reference page, including itsdocker/Dockerfile.
A domain and a Postgres database are optional. You add the domain in step 4 and the database in step 5.
Step 1: Build a standalone Next.js image#
The template runs your app as a prebuilt container, so the image has to be self-contained. Next.js produces a minimal server bundle when you set output: "standalone".
Add the option to next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
module.exports = nextConfig;The template ships a multi-stage docker/Dockerfile that installs dependencies, builds the standalone output, and copies it into a small runtime image:
# 1. Install dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# 2. Build the standalone output
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 3. Minimal runtime image
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]The runtime stage sets HOSTNAME=0.0.0.0 so the server binds on all interfaces inside the container, and PORT=3000 to match the template's app_port default. Build the image from your project root, tagging it for your registry:
docker build -t REGISTRY_HOST/nextjs-app:latest -f docker/Dockerfile .Run it locally to confirm it serves:
docker run --rm -p 3000:3000 REGISTRY_HOST/nextjs-app:latest
curl -sS http://127.0.0.1:3000/ | head -n 5You should see the start of your app's HTML. Stop the local container before continuing.
Step 2: Push the image and set the variables#
The instance pulls the image at first boot, so the image has to live in a registry it can reach. Log in and push:
docker login REGISTRY_HOST
docker push REGISTRY_HOST/nextjs-app:latestCopy the template's example variables file and open it:
cp terraform.tfvars.example terraform.tfvarsSet the two required values. key_name is the SSH keypair already in your project, and container_image is the reference you pushed above:
key_name = "YOUR_KEY_NAME"
container_image = "REGISTRY_HOST/nextjs-app:latest"Leave domain commented out for now. You deploy over HTTP first, then add TLS in step 4.
Step 3: Apply the template and reach the app#
Initialize the working directory, preview the plan, and apply:
tofu init
tofu plan
tofu applyOpenTofu provisions a private network, a security group that allows SSH, HTTP, and HTTPS, an instance, and a floating IP. On first boot, cloud-init installs Docker and starts two containers on a private Docker network: your app on port 3000, and a Caddy reverse proxy in front of it.
When the apply finishes, read the outputs:
tofu outputRecord floating_ip, and open app_url in your browser or request it with curl:
curl -sS http://YOUR_FLOATING_IP/ | head -n 5Cloud-init pulls the image and starts the containers after the instance reaches ACTIVE, so allow a minute or two on the first boot. If the request fails, SSH in with ssh ubuntu@YOUR_FLOATING_IP and check sudo docker ps and sudo docker logs app.
Step 4: Serve the app over HTTPS with your domain#
Caddy obtains and renews a TLS certificate automatically once a domain resolves to the instance. Cloud-init runs on first boot only: changing domain in terraform.tfvars after the instance exists does not re-run the bootstrap script on a plain tofu apply.
Before the first apply: set domain in terraform.tfvars if you already know the hostname you will use, then run the initial tofu apply from Step 3 once with that value in place.
After the instance is already running: point DNS at the floating IP, set domain, then replace the instance so cloud-init runs again:
- Create a DNS A record for your domain (for example
app.example.com) pointing atYOUR_FLOATING_IP. Follow How to point a domain at a Quake AI resource. Wait until the record resolves:
dig +short app.example.comThe command returns your floating IP once the record propagates.
- Set
domaininterraform.tfvars:
domain = "app.example.com"- Taint the instance so the next apply recreates it and cloud-init runs again:
tofu taint openstack_compute_instance_v2.app
tofu applyCaddy requests a certificate from Let's Encrypt on the new boot and serves the app over HTTPS. The app_url output shows https://app.example.com. Open it in your browser to confirm the padlock. For background on how the certificate issuance and renewal work, see How to issue and auto-renew a TLS certificate with Let's Encrypt.
Step 5: Connect a Postgres database#
A Next.js app usually needs a database. Run one with the self-managed Postgres template, then inject its connection string into the app through the template's container_env map.
Each template provisions its own private network by default (nextjs-app uses 10.10.10.0/24; self-managed-postgres uses 192.168.40.0/24). The app reaches Postgres only after you allow the app subnet in Postgres security rules and connect the two networks at the router.
- Apply the Postgres template with
allowed_cidrsset to the app subnet and assemble the connection string from its outputs:
# postgres/terraform.tfvars (excerpt)
allowed_cidrs = ["10.10.10.0/24"]Read private_ip from the Postgres stack outputs and build postgres://USER:PASSWORD@POSTGRES_HOST:5432/DBNAME.
- Connect the app router to the Postgres private subnet (default resource names shown; adjust if you changed
app_nameorinstance_name):
openstack router add subnet nextjs-app-router postgres-private-sn- Add the connection string to
container_envin thenextjs-appstack before you apply it, or taint the app instance after you change the map:
container_env = {
DATABASE_URL = "postgres://USER:PASSWORD@POSTGRES_HOST:5432/DBNAME"
}If the app instance already exists, taint and recreate it so cloud-init picks up the new environment:
tofu taint openstack_compute_instance_v2.app
tofu applyCloud-init starts the app container with DATABASE_URL set in its environment on that boot, where your Next.js data layer reads it.
What you built#
- Built a standalone image from a multi-stage Dockerfile that bundles a Next.js server with
output: "standalone" - Pushed the image to a registry the instance pulls from at first boot
- Applied the
nextjs-apptemplate to provision a network, instance, security group, and floating IP, and reached the app over HTTP - Added HTTPS by pointing a domain at the floating IP and letting Caddy obtain a certificate
- Connected Postgres by injecting
DATABASE_URLthroughcontainer_env
Scope of this deployment#
This template runs a single-VM origin, not a global edge deployment. The instance is CPU-only, and there is no native CDN. If you need edge caching for static or incrementally regenerated assets, put a third-party CDN in front of the floating IP. You operate the instance, the containers, and the database yourself.
Next steps#
- How to deploy an application to a Quake AI VM from CI: build and push the image and run
tofu applyfrom GitHub Actions or GitLab CI - How to use a container registry with Quake AI: push images to GHCR, Docker Hub, or another registry the instance can pull from
- self-managed Postgres template: size and operate the database behind your app
- Containerized app template: the general-purpose template the
nextjs-apptemplate specializes - Security hardening checklist: tighten SSH access and exposure before you serve real traffic
Clean up#
When you no longer need the deployment, destroy everything the template created:
tofu destroyIf you applied the Postgres template separately, run tofu destroy in that directory too. Then remove the DNS A record you created in step 4, and delete the image from your registry if you pushed it only for this walkthrough.
Quick answers
- Why does `openstack image save` write a 0-byte file for my boot-from-volume instance?CLI
- Why does `openstack server create` fail with "Only volume-backed servers are allowed for flavors with zero disk"?CLIAPITerraform
- Why does my project still have a 10 GiB Cinder volume after I deleted my instance?CLIAPI
See Also
Instances
Prerequisite
Migrate a Docker container app from AWS to Quake AI
Shares: Docker, Containers
Deploy Airbyte with the airbyte template
Shares: Docker, Containers
Deploy Airflow with the airflow template
Shares: Docker, Containers
Deploy Umami with the analytics-umami template
Shares: Docker, Containers